2009-03-13 Xan Lopez <xlopez@igalia.com>
[WebKit-https.git] / PlanetWebKit / planet / planet / feedparser.py
1 #!/usr/bin/env python
2 """Universal feed parser
3
4 Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds
5
6 Visit http://feedparser.org/ for the latest version
7 Visit http://feedparser.org/docs/ for the latest documentation
8
9 Required: Python 2.1 or later
10 Recommended: Python 2.3 or later
11 Recommended: CJKCodecs and iconv_codec <http://cjkpython.i18n.org/>
12 """
13
14 __version__ = "4.1"# + "$Revision: 1.92 $"[11:15] + "-cvs"
15 __license__ = """Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
16
17 Redistribution and use in source and binary forms, with or without modification,
18 are permitted provided that the following conditions are met:
19
20 * Redistributions of source code must retain the above copyright notice,
21   this list of conditions and the following disclaimer.
22 * Redistributions in binary form must reproduce the above copyright notice,
23   this list of conditions and the following disclaimer in the documentation
24   and/or other materials provided with the distribution.
25
26 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
27 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
30 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36 POSSIBILITY OF SUCH DAMAGE."""
37 __author__ = "Mark Pilgrim <http://diveintomark.org/>"
38 __contributors__ = ["Jason Diamond <http://injektilo.org/>",
39                     "John Beimler <http://john.beimler.org/>",
40                     "Fazal Majid <http://www.majid.info/mylos/weblog/>",
41                     "Aaron Swartz <http://aaronsw.com/>",
42                     "Kevin Marks <http://epeus.blogspot.com/>"]
43 _debug = 0
44
45 # HTTP "User-Agent" header to send to servers when downloading feeds.
46 # If you are embedding feedparser in a larger application, you should
47 # change this to your application name and URL.
48 USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__
49
50 # HTTP "Accept" header to send to servers when downloading feeds.  If you don't
51 # want to send an Accept header, set this to None.
52 ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
53
54 # List of preferred XML parsers, by SAX driver name.  These will be tried first,
55 # but if they're not installed, Python will keep searching through its own list
56 # of pre-installed parsers until it finds one that supports everything we need.
57 PREFERRED_XML_PARSERS = ["drv_libxml2"]
58
59 # If you want feedparser to automatically run HTML markup through HTML Tidy, set
60 # this to 1.  Requires mxTidy <http://www.egenix.com/files/python/mxTidy.html>
61 # or utidylib <http://utidylib.berlios.de/>.
62 TIDY_MARKUP = 0
63
64 # List of Python interfaces for HTML Tidy, in order of preference.  Only useful
65 # if TIDY_MARKUP = 1
66 PREFERRED_TIDY_INTERFACES = ["uTidy", "mxTidy"]
67
68 # ---------- required modules (should come with any Python distribution) ----------
69 import sgmllib, re, sys, copy, urlparse, time, rfc822, types, cgi, urllib, urllib2
70 try:
71     from cStringIO import StringIO as _StringIO
72 except:
73     from StringIO import StringIO as _StringIO
74
75 # ---------- optional modules (feedparser will work without these, but with reduced functionality) ----------
76
77 # gzip is included with most Python distributions, but may not be available if you compiled your own
78 try:
79     import gzip
80 except:
81     gzip = None
82 try:
83     import zlib
84 except:
85     zlib = None
86
87 # If a real XML parser is available, feedparser will attempt to use it.  feedparser has
88 # been tested with the built-in SAX parser, PyXML, and libxml2.  On platforms where the
89 # Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some
90 # versions of FreeBSD), feedparser will quietly fall back on regex-based parsing.
91 try:
92     import xml.sax
93     xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers
94     from xml.sax.saxutils import escape as _xmlescape
95     _XML_AVAILABLE = 1
96 except:
97     _XML_AVAILABLE = 0
98     def _xmlescape(data,entities={}):
99         data = data.replace('&', '&amp;')
100         data = data.replace('>', '&gt;')
101         data = data.replace('<', '&lt;')
102         for char, entity in entities:
103             data = data.replace(char, entity)
104         return data
105
106 # base64 support for Atom feeds that contain embedded binary data
107 try:
108     import base64, binascii
109 except:
110     base64 = binascii = None
111
112 # cjkcodecs and iconv_codec provide support for more character encodings.
113 # Both are available from http://cjkpython.i18n.org/
114 try:
115     import cjkcodecs.aliases
116 except:
117     pass
118 try:
119     import iconv_codec
120 except:
121     pass
122
123 # chardet library auto-detects character encodings
124 # Download from http://chardet.feedparser.org/
125 try:
126     import chardet
127     if _debug:
128         import chardet.constants
129         chardet.constants._debug = 1
130 except:
131     chardet = None
132
133 # ---------- don't touch these ----------
134 class ThingsNobodyCaresAboutButMe(Exception): pass
135 class CharacterEncodingOverride(ThingsNobodyCaresAboutButMe): pass
136 class CharacterEncodingUnknown(ThingsNobodyCaresAboutButMe): pass
137 class NonXMLContentType(ThingsNobodyCaresAboutButMe): pass
138 class UndeclaredNamespace(Exception): pass
139
140 sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
141 sgmllib.special = re.compile('<!')
142 sgmllib.charref = re.compile('&#(x?[0-9A-Fa-f]+)[^0-9A-Fa-f]')
143
144 SUPPORTED_VERSIONS = {'': 'unknown',
145                       'rss090': 'RSS 0.90',
146                       'rss091n': 'RSS 0.91 (Netscape)',
147                       'rss091u': 'RSS 0.91 (Userland)',
148                       'rss092': 'RSS 0.92',
149                       'rss093': 'RSS 0.93',
150                       'rss094': 'RSS 0.94',
151                       'rss20': 'RSS 2.0',
152                       'rss10': 'RSS 1.0',
153                       'rss': 'RSS (unknown version)',
154                       'atom01': 'Atom 0.1',
155                       'atom02': 'Atom 0.2',
156                       'atom03': 'Atom 0.3',
157                       'atom10': 'Atom 1.0',
158                       'atom': 'Atom (unknown version)',
159                       'cdf': 'CDF',
160                       'hotrss': 'Hot RSS'
161                       }
162
163 try:
164     UserDict = dict
165 except NameError:
166     # Python 2.1 does not have dict
167     from UserDict import UserDict
168     def dict(aList):
169         rc = {}
170         for k, v in aList:
171             rc[k] = v
172         return rc
173
174 class FeedParserDict(UserDict):
175     keymap = {'channel': 'feed',
176               'items': 'entries',
177               'guid': 'id',
178               'date': 'updated',
179               'date_parsed': 'updated_parsed',
180               'description': ['subtitle', 'summary'],
181               'url': ['href'],
182               'modified': 'updated',
183               'modified_parsed': 'updated_parsed',
184               'issued': 'published',
185               'issued_parsed': 'published_parsed',
186               'copyright': 'rights',
187               'copyright_detail': 'rights_detail',
188               'tagline': 'subtitle',
189               'tagline_detail': 'subtitle_detail'}
190     def __getitem__(self, key):
191         if key == 'category':
192             return UserDict.__getitem__(self, 'tags')[0]['term']
193         if key == 'categories':
194             return [(tag['scheme'], tag['term']) for tag in UserDict.__getitem__(self, 'tags')]
195         realkey = self.keymap.get(key, key)
196         if type(realkey) == types.ListType:
197             for k in realkey:
198                 if UserDict.has_key(self, k):
199                     return UserDict.__getitem__(self, k)
200         if UserDict.has_key(self, key):
201             return UserDict.__getitem__(self, key)
202         return UserDict.__getitem__(self, realkey)
203
204     def __setitem__(self, key, value):
205         for k in self.keymap.keys():
206             if key == k:
207                 key = self.keymap[k]
208                 if type(key) == types.ListType:
209                     key = key[0]
210         return UserDict.__setitem__(self, key, value)
211
212     def get(self, key, default=None):
213         if self.has_key(key):
214             return self[key]
215         else:
216             return default
217
218     def setdefault(self, key, value):
219         if not self.has_key(key):
220             self[key] = value
221         return self[key]
222         
223     def has_key(self, key):
224         try:
225             return hasattr(self, key) or UserDict.has_key(self, key)
226         except AttributeError:
227             return False
228         
229     def __getattr__(self, key):
230         try:
231             return self.__dict__[key]
232         except KeyError:
233             pass
234         try:
235             assert not key.startswith('_')
236             return self.__getitem__(key)
237         except:
238             raise AttributeError, "object has no attribute '%s'" % key
239
240     def __setattr__(self, key, value):
241         if key.startswith('_') or key == 'data':
242             self.__dict__[key] = value
243         else:
244             return self.__setitem__(key, value)
245
246     def __contains__(self, key):
247         return self.has_key(key)
248
249 def zopeCompatibilityHack():
250     global FeedParserDict
251     del FeedParserDict
252     def FeedParserDict(aDict=None):
253         rc = {}
254         if aDict:
255             rc.update(aDict)
256         return rc
257
258 _ebcdic_to_ascii_map = None
259 def _ebcdic_to_ascii(s):
260     global _ebcdic_to_ascii_map
261     if not _ebcdic_to_ascii_map:
262         emap = (
263             0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
264             16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
265             128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
266             144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
267             32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
268             38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
269             45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
270             186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
271             195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,201,
272             202,106,107,108,109,110,111,112,113,114,203,204,205,206,207,208,
273             209,126,115,116,117,118,119,120,121,122,210,211,212,213,214,215,
274             216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,
275             123,65,66,67,68,69,70,71,72,73,232,233,234,235,236,237,
276             125,74,75,76,77,78,79,80,81,82,238,239,240,241,242,243,
277             92,159,83,84,85,86,87,88,89,90,244,245,246,247,248,249,
278             48,49,50,51,52,53,54,55,56,57,250,251,252,253,254,255
279             )
280         import string
281         _ebcdic_to_ascii_map = string.maketrans( \
282             ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
283     return s.translate(_ebcdic_to_ascii_map)
284  
285 cp1252 = {
286   unichr(128): unichr(8364), # euro sign
287   unichr(130): unichr(8218), # single low-9 quotation mark
288   unichr(131): unichr( 402), # latin small letter f with hook
289   unichr(132): unichr(8222), # double low-9 quotation mark
290   unichr(133): unichr(8230), # horizontal ellipsis
291   unichr(134): unichr(8224), # dagger
292   unichr(135): unichr(8225), # double dagger
293   unichr(136): unichr( 710), # modifier letter circumflex accent
294   unichr(137): unichr(8240), # per mille sign
295   unichr(138): unichr( 352), # latin capital letter s with caron
296   unichr(139): unichr(8249), # single left-pointing angle quotation mark
297   unichr(140): unichr( 338), # latin capital ligature oe
298   unichr(142): unichr( 381), # latin capital letter z with caron
299   unichr(145): unichr(8216), # left single quotation mark
300   unichr(146): unichr(8217), # right single quotation mark
301   unichr(147): unichr(8220), # left double quotation mark
302   unichr(148): unichr(8221), # right double quotation mark
303   unichr(149): unichr(8226), # bullet
304   unichr(150): unichr(8211), # en dash
305   unichr(151): unichr(8212), # em dash
306   unichr(152): unichr( 732), # small tilde
307   unichr(153): unichr(8482), # trade mark sign
308   unichr(154): unichr( 353), # latin small letter s with caron
309   unichr(155): unichr(8250), # single right-pointing angle quotation mark
310   unichr(156): unichr( 339), # latin small ligature oe
311   unichr(158): unichr( 382), # latin small letter z with caron
312   unichr(159): unichr( 376)} # latin capital letter y with diaeresis
313
314 _urifixer = re.compile('^([A-Za-z][A-Za-z0-9+-.]*://)(/*)(.*?)')
315 def _urljoin(base, uri):
316     uri = _urifixer.sub(r'\1\3', uri)
317     return urlparse.urljoin(base, uri)
318
319 class _FeedParserMixin:
320     namespaces = {'': '',
321                   'http://backend.userland.com/rss': '',
322                   'http://blogs.law.harvard.edu/tech/rss': '',
323                   'http://purl.org/rss/1.0/': '',
324                   'http://my.netscape.com/rdf/simple/0.9/': '',
325                   'http://example.com/newformat#': '',
326                   'http://example.com/necho': '',
327                   'http://purl.org/echo/': '',
328                   'uri/of/echo/namespace#': '',
329                   'http://purl.org/pie/': '',
330                   'http://purl.org/atom/ns#': '',
331                   'http://www.w3.org/2005/Atom': '',
332                   'http://purl.org/rss/1.0/modules/rss091#': '',
333                   
334                   'http://webns.net/mvcb/':                               'admin',
335                   'http://purl.org/rss/1.0/modules/aggregation/':         'ag',
336                   'http://purl.org/rss/1.0/modules/annotate/':            'annotate',
337                   'http://media.tangent.org/rss/1.0/':                    'audio',
338                   'http://backend.userland.com/blogChannelModule':        'blogChannel',
339                   'http://web.resource.org/cc/':                          'cc',
340                   'http://backend.userland.com/creativeCommonsRssModule': 'creativeCommons',
341                   'http://purl.org/rss/1.0/modules/company':              'co',
342                   'http://purl.org/rss/1.0/modules/content/':             'content',
343                   'http://my.theinfo.org/changed/1.0/rss/':               'cp',
344                   'http://purl.org/dc/elements/1.1/':                     'dc',
345                   'http://purl.org/dc/terms/':                            'dcterms',
346                   'http://purl.org/rss/1.0/modules/email/':               'email',
347                   'http://purl.org/rss/1.0/modules/event/':               'ev',
348                   'http://rssnamespace.org/feedburner/ext/1.0':           'feedburner',
349                   'http://freshmeat.net/rss/fm/':                         'fm',
350                   'http://xmlns.com/foaf/0.1/':                           'foaf',
351                   'http://www.w3.org/2003/01/geo/wgs84_pos#':             'geo',
352                   'http://postneo.com/icbm/':                             'icbm',
353                   'http://purl.org/rss/1.0/modules/image/':               'image',
354                   'http://www.itunes.com/DTDs/PodCast-1.0.dtd':           'itunes',
355                   'http://example.com/DTDs/PodCast-1.0.dtd':              'itunes',
356                   'http://purl.org/rss/1.0/modules/link/':                'l',
357                   'http://search.yahoo.com/mrss':                         'media',
358                   'http://madskills.com/public/xml/rss/module/pingback/': 'pingback',
359                   'http://prismstandard.org/namespaces/1.2/basic/':       'prism',
360                   'http://www.w3.org/1999/02/22-rdf-syntax-ns#':          'rdf',
361                   'http://www.w3.org/2000/01/rdf-schema#':                'rdfs',
362                   'http://purl.org/rss/1.0/modules/reference/':           'ref',
363                   'http://purl.org/rss/1.0/modules/richequiv/':           'reqv',
364                   'http://purl.org/rss/1.0/modules/search/':              'search',
365                   'http://purl.org/rss/1.0/modules/slash/':               'slash',
366                   'http://schemas.xmlsoap.org/soap/envelope/':            'soap',
367                   'http://purl.org/rss/1.0/modules/servicestatus/':       'ss',
368                   'http://hacks.benhammersley.com/rss/streaming/':        'str',
369                   'http://purl.org/rss/1.0/modules/subscription/':        'sub',
370                   'http://purl.org/rss/1.0/modules/syndication/':         'sy',
371                   'http://purl.org/rss/1.0/modules/taxonomy/':            'taxo',
372                   'http://purl.org/rss/1.0/modules/threading/':           'thr',
373                   'http://purl.org/rss/1.0/modules/textinput/':           'ti',
374                   'http://madskills.com/public/xml/rss/module/trackback/':'trackback',
375                   'http://wellformedweb.org/commentAPI/':                 'wfw',
376                   'http://purl.org/rss/1.0/modules/wiki/':                'wiki',
377                   'http://www.w3.org/1999/xhtml':                         'xhtml',
378                   'http://www.w3.org/XML/1998/namespace':                 'xml',
379                   'http://schemas.pocketsoap.com/rss/myDescModule/':      'szf'
380 }
381     _matchnamespaces = {}
382
383     can_be_relative_uri = ['link', 'id', 'wfw_comment', 'wfw_commentrss', 'docs', 'url', 'href', 'comments', 'license', 'icon', 'logo']
384     can_contain_relative_uris = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
385     can_contain_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
386     html_types = ['text/html', 'application/xhtml+xml']
387     
388     def __init__(self, baseuri=None, baselang=None, encoding='utf-8'):
389         if _debug: sys.stderr.write('initializing FeedParser\n')
390         if not self._matchnamespaces:
391             for k, v in self.namespaces.items():
392                 self._matchnamespaces[k.lower()] = v
393         self.feeddata = FeedParserDict() # feed-level data
394         self.encoding = encoding # character encoding
395         self.entries = [] # list of entry-level data
396         self.version = '' # feed type/version, see SUPPORTED_VERSIONS
397         self.namespacesInUse = {} # dictionary of namespaces defined by the feed
398
399         # the following are used internally to track state;
400         # this is really out of control and should be refactored
401         self.infeed = 0
402         self.inentry = 0
403         self.incontent = 0
404         self.intextinput = 0
405         self.inimage = 0
406         self.inauthor = 0
407         self.incontributor = 0
408         self.inpublisher = 0
409         self.insource = 0
410         self.sourcedata = FeedParserDict()
411         self.contentparams = FeedParserDict()
412         self._summaryKey = None
413         self.namespacemap = {}
414         self.elementstack = []
415         self.basestack = []
416         self.langstack = []
417         self.baseuri = baseuri or ''
418         self.lang = baselang or None
419         if baselang:
420             self.feeddata['language'] = baselang
421
422     def unknown_starttag(self, tag, attrs):
423         if _debug: sys.stderr.write('start %s with %s\n' % (tag, attrs))
424         # normalize attrs
425         attrs = [(k.lower(), v) for k, v in attrs]
426         attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
427         
428         # track xml:base and xml:lang
429         attrsD = dict(attrs)
430         baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri
431         self.baseuri = _urljoin(self.baseuri, baseuri)
432         lang = attrsD.get('xml:lang', attrsD.get('lang'))
433         if lang == '':
434             # xml:lang could be explicitly set to '', we need to capture that
435             lang = None
436         elif lang is None:
437             # if no xml:lang is specified, use parent lang
438             lang = self.lang
439         if lang:
440             if tag in ('feed', 'rss', 'rdf:RDF'):
441                 self.feeddata['language'] = lang
442         self.lang = lang
443         self.basestack.append(self.baseuri)
444         self.langstack.append(lang)
445         
446         # track namespaces
447         for prefix, uri in attrs:
448             if prefix.startswith('xmlns:'):
449                 self.trackNamespace(prefix[6:], uri)
450             elif prefix == 'xmlns':
451                 self.trackNamespace(None, uri)
452
453         # track inline content
454         if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
455             # element declared itself as escaped markup, but it isn't really
456             self.contentparams['type'] = 'application/xhtml+xml'
457         if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
458             # Note: probably shouldn't simply recreate localname here, but
459             # our namespace handling isn't actually 100% correct in cases where
460             # the feed redefines the default namespace (which is actually
461             # the usual case for inline content, thanks Sam), so here we
462             # cheat and just reconstruct the element based on localname
463             # because that compensates for the bugs in our namespace handling.
464             # This will horribly munge inline content with non-empty qnames,
465             # but nobody actually does that, so I'm not fixing it.
466             tag = tag.split(':')[-1]
467             return self.handle_data('<%s%s>' % (tag, self.strattrs(attrs)), escape=0)
468
469         # match namespaces
470         if tag.find(':') <> -1:
471             prefix, suffix = tag.split(':', 1)
472         else:
473             prefix, suffix = '', tag
474         prefix = self.namespacemap.get(prefix, prefix)
475         if prefix:
476             prefix = prefix + '_'
477
478         # special hack for better tracking of empty textinput/image elements in illformed feeds
479         if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
480             self.intextinput = 0
481         if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
482             self.inimage = 0
483         
484         # call special handler (if defined) or default handler
485         methodname = '_start_' + prefix + suffix
486         try:
487             method = getattr(self, methodname)
488             return method(attrsD)
489         except AttributeError:
490             return self.push(prefix + suffix, 1)
491
492     def unknown_endtag(self, tag):
493         if _debug: sys.stderr.write('end %s\n' % tag)
494         # match namespaces
495         if tag.find(':') <> -1:
496             prefix, suffix = tag.split(':', 1)
497         else:
498             prefix, suffix = '', tag
499         prefix = self.namespacemap.get(prefix, prefix)
500         if prefix:
501             prefix = prefix + '_'
502
503         # call special handler (if defined) or default handler
504         methodname = '_end_' + prefix + suffix
505         try:
506             method = getattr(self, methodname)
507             method()
508         except AttributeError:
509             self.pop(prefix + suffix)
510
511         # track inline content
512         if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
513             # element declared itself as escaped markup, but it isn't really
514             self.contentparams['type'] = 'application/xhtml+xml'
515         if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
516             tag = tag.split(':')[-1]
517             self.handle_data('</%s>' % tag, escape=0)
518
519         # track xml:base and xml:lang going out of scope
520         if self.basestack:
521             self.basestack.pop()
522             if self.basestack and self.basestack[-1]:
523                 self.baseuri = self.basestack[-1]
524         if self.langstack:
525             self.langstack.pop()
526             if self.langstack: # and (self.langstack[-1] is not None):
527                 self.lang = self.langstack[-1]
528
529     def handle_charref(self, ref):
530         # called for each character reference, e.g. for '&#160;', ref will be '160'
531         if not self.elementstack: return
532         ref = ref.lower()
533         if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
534             text = '&#%s;' % ref
535         else:
536             if ref[0] == 'x':
537                 c = int(ref[1:], 16)
538             else:
539                 c = int(ref)
540             text = unichr(c).encode('utf-8')
541         self.elementstack[-1][2].append(text)
542
543     def handle_entityref(self, ref):
544         # called for each entity reference, e.g. for '&copy;', ref will be 'copy'
545         if not self.elementstack: return
546         if _debug: sys.stderr.write('entering handle_entityref with %s\n' % ref)
547         if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
548             text = '&%s;' % ref
549         else:
550             # entity resolution graciously donated by Aaron Swartz
551             def name2cp(k):
552                 import htmlentitydefs
553                 if hasattr(htmlentitydefs, 'name2codepoint'): # requires Python 2.3
554                     return htmlentitydefs.name2codepoint[k]
555                 k = htmlentitydefs.entitydefs[k]
556                 if k.startswith('&#') and k.endswith(';'):
557                     return int(k[2:-1]) # not in latin-1
558                 return ord(k)
559             try: name2cp(ref)
560             except KeyError: text = '&%s;' % ref
561             else: text = unichr(name2cp(ref)).encode('utf-8')
562         self.elementstack[-1][2].append(text)
563
564     def handle_data(self, text, escape=1):
565         # called for each block of plain text, i.e. outside of any tag and
566         # not containing any character or entity references
567         if not self.elementstack: return
568         if escape and self.contentparams.get('type') == 'application/xhtml+xml':
569             text = _xmlescape(text)
570         self.elementstack[-1][2].append(text)
571
572     def handle_comment(self, text):
573         # called for each comment, e.g. <!-- insert message here -->
574         pass
575
576     def handle_pi(self, text):
577         # called for each processing instruction, e.g. <?instruction>
578         pass
579
580     def handle_decl(self, text):
581         pass
582
583     def parse_declaration(self, i):
584         # override internal declaration handler to handle CDATA blocks
585         if _debug: sys.stderr.write('entering parse_declaration\n')
586         if self.rawdata[i:i+9] == '<![CDATA[':
587             k = self.rawdata.find(']]>', i)
588             if k == -1: k = len(self.rawdata)
589             self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0)
590             return k+3
591         else:
592             k = self.rawdata.find('>', i)
593             return k+1
594
595     def mapContentType(self, contentType):
596         contentType = contentType.lower()
597         if contentType == 'text':
598             contentType = 'text/plain'
599         elif contentType == 'html':
600             contentType = 'text/html'
601         elif contentType == 'xhtml':
602             contentType = 'application/xhtml+xml'
603         return contentType
604     
605     def trackNamespace(self, prefix, uri):
606         loweruri = uri.lower()
607         if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version:
608             self.version = 'rss090'
609         if loweruri == 'http://purl.org/rss/1.0/' and not self.version:
610             self.version = 'rss10'
611         if loweruri == 'http://www.w3.org/2005/atom' and not self.version:
612             self.version = 'atom10'
613         if loweruri.find('backend.userland.com/rss') <> -1:
614             # match any backend.userland.com namespace
615             uri = 'http://backend.userland.com/rss'
616             loweruri = uri
617         if self._matchnamespaces.has_key(loweruri):
618             self.namespacemap[prefix] = self._matchnamespaces[loweruri]
619             self.namespacesInUse[self._matchnamespaces[loweruri]] = uri
620         else:
621             self.namespacesInUse[prefix or ''] = uri
622
623     def resolveURI(self, uri):
624         return _urljoin(self.baseuri or '', uri)
625     
626     def decodeEntities(self, element, data):
627         return data
628
629     def strattrs(self, attrs):
630         return ''.join([' %s="%s"' % (t[0],_xmlescape(t[1],{'"':'&quot;'})) for t in attrs])
631
632     def push(self, element, expectingText):
633         self.elementstack.append([element, expectingText, []])
634
635     def pop(self, element, stripWhitespace=1):
636         if not self.elementstack: return
637         if self.elementstack[-1][0] != element: return
638         
639         element, expectingText, pieces = self.elementstack.pop()
640
641         if self.version == 'atom10' and self.contentparams.get('type','text') == 'application/xhtml+xml':
642             # remove enclosing child element, but only if it is a <div> and
643             # only if all the remaining content is nested underneath it.
644             # This means that the divs would be retained in the following:
645             #    <div>foo</div><div>bar</div>
646             if pieces and (pieces[0] == '<div>' or pieces[0].startswith('<div ')) and pieces[-1]=='</div>':
647                 depth = 0
648                 for piece in pieces[:-1]:
649                     if piece.startswith('</'):
650                         depth -= 1
651                         if depth == 0: break
652                     elif piece.startswith('<') and not piece.endswith('/>'):
653                         depth += 1
654                 else:
655                     pieces = pieces[1:-1]
656
657         output = ''.join(pieces)
658         if stripWhitespace:
659             output = output.strip()
660         if not expectingText: return output
661
662         # decode base64 content
663         if base64 and self.contentparams.get('base64', 0):
664             try:
665                 output = base64.decodestring(output)
666             except binascii.Error:
667                 pass
668             except binascii.Incomplete:
669                 pass
670                 
671         # resolve relative URIs
672         if (element in self.can_be_relative_uri) and output:
673             output = self.resolveURI(output)
674         
675         # decode entities within embedded markup
676         if not self.contentparams.get('base64', 0):
677             output = self.decodeEntities(element, output)
678
679         # remove temporary cruft from contentparams
680         try:
681             del self.contentparams['mode']
682         except KeyError:
683             pass
684         try:
685             del self.contentparams['base64']
686         except KeyError:
687             pass
688
689         # resolve relative URIs within embedded markup
690         if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
691             if element in self.can_contain_relative_uris:
692                 output = _resolveRelativeURIs(output, self.baseuri, self.encoding)
693         
694         # sanitize embedded markup
695         if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
696             if element in self.can_contain_dangerous_markup:
697                 output = _sanitizeHTML(output, self.encoding)
698
699         if self.encoding and type(output) != type(u''):
700             try:
701                 output = unicode(output, self.encoding)
702             except:
703                 pass
704
705         # address common error where people take data that is already
706         # utf-8, presume that it is iso-8859-1, and re-encode it.
707         if self.encoding=='utf-8' and type(output) == type(u''):
708             try:
709                 output = unicode(output.encode('iso-8859-1'), 'utf-8')
710             except:
711                 pass
712
713         # map win-1252 extensions to the proper code points
714         if type(output) == type(u''):
715             output = u''.join([c in cp1252 and cp1252[c] or c for c in output])
716
717         # categories/tags/keywords/whatever are handled in _end_category
718         if element == 'category':
719             return output
720         
721         # store output in appropriate place(s)
722         if self.inentry and not self.insource:
723             if element == 'content':
724                 self.entries[-1].setdefault(element, [])
725                 contentparams = copy.deepcopy(self.contentparams)
726                 contentparams['value'] = output
727                 self.entries[-1][element].append(contentparams)
728             elif element == 'link':
729                 self.entries[-1][element] = output
730                 if output:
731                     self.entries[-1]['links'][-1]['href'] = output
732             else:
733                 if element == 'description':
734                     element = 'summary'
735                 self.entries[-1][element] = output
736                 if self.incontent:
737                     contentparams = copy.deepcopy(self.contentparams)
738                     contentparams['value'] = output
739                     self.entries[-1][element + '_detail'] = contentparams
740         elif (self.infeed or self.insource) and (not self.intextinput) and (not self.inimage):
741             context = self._getContext()
742             if element == 'description':
743                 element = 'subtitle'
744             context[element] = output
745             if element == 'link':
746                 context['links'][-1]['href'] = output
747             elif self.incontent:
748                 contentparams = copy.deepcopy(self.contentparams)
749                 contentparams['value'] = output
750                 context[element + '_detail'] = contentparams
751         return output
752
753     def pushContent(self, tag, attrsD, defaultContentType, expectingText):
754         self.incontent += 1
755         self.contentparams = FeedParserDict({
756             'type': self.mapContentType(attrsD.get('type', defaultContentType)),
757             'language': self.lang,
758             'base': self.baseuri})
759         self.contentparams['base64'] = self._isBase64(attrsD, self.contentparams)
760         self.push(tag, expectingText)
761
762     def popContent(self, tag):
763         value = self.pop(tag)
764         self.incontent -= 1
765         self.contentparams.clear()
766         return value
767         
768     def _mapToStandardPrefix(self, name):
769         colonpos = name.find(':')
770         if colonpos <> -1:
771             prefix = name[:colonpos]
772             suffix = name[colonpos+1:]
773             prefix = self.namespacemap.get(prefix, prefix)
774             name = prefix + ':' + suffix
775         return name
776         
777     def _getAttribute(self, attrsD, name):
778         return attrsD.get(self._mapToStandardPrefix(name))
779
780     def _isBase64(self, attrsD, contentparams):
781         if attrsD.get('mode', '') == 'base64':
782             return 1
783         if self.contentparams['type'].startswith('text/'):
784             return 0
785         if self.contentparams['type'].endswith('+xml'):
786             return 0
787         if self.contentparams['type'].endswith('/xml'):
788             return 0
789         return 1
790
791     def _itsAnHrefDamnIt(self, attrsD):
792         href = attrsD.get('url', attrsD.get('uri', attrsD.get('href', None)))
793         if href:
794             try:
795                 del attrsD['url']
796             except KeyError:
797                 pass
798             try:
799                 del attrsD['uri']
800             except KeyError:
801                 pass
802             attrsD['href'] = href
803         return attrsD
804     
805     def _save(self, key, value):
806         context = self._getContext()
807         context.setdefault(key, value)
808
809     def _start_rss(self, attrsD):
810         versionmap = {'0.91': 'rss091u',
811                       '0.92': 'rss092',
812                       '0.93': 'rss093',
813                       '0.94': 'rss094'}
814         if not self.version:
815             attr_version = attrsD.get('version', '')
816             version = versionmap.get(attr_version)
817             if version:
818                 self.version = version
819             elif attr_version.startswith('2.'):
820                 self.version = 'rss20'
821             else:
822                 self.version = 'rss'
823     
824     def _start_dlhottitles(self, attrsD):
825         self.version = 'hotrss'
826
827     def _start_channel(self, attrsD):
828         self.infeed = 1
829         self._cdf_common(attrsD)
830     _start_feedinfo = _start_channel
831
832     def _cdf_common(self, attrsD):
833         if attrsD.has_key('lastmod'):
834             self._start_modified({})
835             self.elementstack[-1][-1] = attrsD['lastmod']
836             self._end_modified()
837         if attrsD.has_key('href'):
838             self._start_link({})
839             self.elementstack[-1][-1] = attrsD['href']
840             self._end_link()
841     
842     def _start_feed(self, attrsD):
843         self.infeed = 1
844         versionmap = {'0.1': 'atom01',
845                       '0.2': 'atom02',
846                       '0.3': 'atom03'}
847         if not self.version:
848             attr_version = attrsD.get('version')
849             version = versionmap.get(attr_version)
850             if version:
851                 self.version = version
852             else:
853                 self.version = 'atom'
854
855     def _end_channel(self):
856         self.infeed = 0
857     _end_feed = _end_channel
858     
859     def _start_image(self, attrsD):
860         self.inimage = 1
861         self.push('image', 0)
862         context = self._getContext()
863         context.setdefault('image', FeedParserDict())
864             
865     def _end_image(self):
866         self.pop('image')
867         self.inimage = 0
868
869     def _start_textinput(self, attrsD):
870         self.intextinput = 1
871         self.push('textinput', 0)
872         context = self._getContext()
873         context.setdefault('textinput', FeedParserDict())
874     _start_textInput = _start_textinput
875     
876     def _end_textinput(self):
877         self.pop('textinput')
878         self.intextinput = 0
879     _end_textInput = _end_textinput
880
881     def _start_author(self, attrsD):
882         self.inauthor = 1
883         self.push('author', 1)
884     _start_managingeditor = _start_author
885     _start_dc_author = _start_author
886     _start_dc_creator = _start_author
887     _start_itunes_author = _start_author
888
889     def _end_author(self):
890         self.pop('author')
891         self.inauthor = 0
892         self._sync_author_detail()
893     _end_managingeditor = _end_author
894     _end_dc_author = _end_author
895     _end_dc_creator = _end_author
896     _end_itunes_author = _end_author
897
898     def _start_itunes_owner(self, attrsD):
899         self.inpublisher = 1
900         self.push('publisher', 0)
901
902     def _end_itunes_owner(self):
903         self.pop('publisher')
904         self.inpublisher = 0
905         self._sync_author_detail('publisher')
906
907     def _start_contributor(self, attrsD):
908         self.incontributor = 1
909         context = self._getContext()
910         context.setdefault('contributors', [])
911         context['contributors'].append(FeedParserDict())
912         self.push('contributor', 0)
913
914     def _end_contributor(self):
915         self.pop('contributor')
916         self.incontributor = 0
917
918     def _start_dc_contributor(self, attrsD):
919         self.incontributor = 1
920         context = self._getContext()
921         context.setdefault('contributors', [])
922         context['contributors'].append(FeedParserDict())
923         self.push('name', 0)
924
925     def _end_dc_contributor(self):
926         self._end_name()
927         self.incontributor = 0
928
929     def _start_name(self, attrsD):
930         self.push('name', 0)
931     _start_itunes_name = _start_name
932
933     def _end_name(self):
934         value = self.pop('name')
935         if self.inpublisher:
936             self._save_author('name', value, 'publisher')
937         elif self.inauthor:
938             self._save_author('name', value)
939         elif self.incontributor:
940             self._save_contributor('name', value)
941         elif self.intextinput:
942             context = self._getContext()
943             context['textinput']['name'] = value
944     _end_itunes_name = _end_name
945
946     def _start_width(self, attrsD):
947         self.push('width', 0)
948
949     def _end_width(self):
950         value = self.pop('width')
951         try:
952             value = int(value)
953         except:
954             value = 0
955         if self.inimage:
956             context = self._getContext()
957             context['image']['width'] = value
958
959     def _start_height(self, attrsD):
960         self.push('height', 0)
961
962     def _end_height(self):
963         value = self.pop('height')
964         try:
965             value = int(value)
966         except:
967             value = 0
968         if self.inimage:
969             context = self._getContext()
970             context['image']['height'] = value
971
972     def _start_url(self, attrsD):
973         self.push('href', 1)
974     _start_homepage = _start_url
975     _start_uri = _start_url
976
977     def _end_url(self):
978         value = self.pop('href')
979         if self.inauthor:
980             self._save_author('href', value)
981         elif self.incontributor:
982             self._save_contributor('href', value)
983         elif self.inimage:
984             context = self._getContext()
985             context['image']['href'] = value
986         elif self.intextinput:
987             context = self._getContext()
988             context['textinput']['link'] = value
989     _end_homepage = _end_url
990     _end_uri = _end_url
991
992     def _start_email(self, attrsD):
993         self.push('email', 0)
994     _start_itunes_email = _start_email
995
996     def _end_email(self):
997         value = self.pop('email')
998         if self.inpublisher:
999             self._save_author('email', value, 'publisher')
1000         elif self.inauthor:
1001             self._save_author('email', value)
1002         elif self.incontributor:
1003             self._save_contributor('email', value)
1004     _end_itunes_email = _end_email
1005
1006     def _getContext(self):
1007         if self.insource:
1008             context = self.sourcedata
1009         elif self.inentry:
1010             context = self.entries[-1]
1011         else:
1012             context = self.feeddata
1013         return context
1014
1015     def _save_author(self, key, value, prefix='author'):
1016         context = self._getContext()
1017         context.setdefault(prefix + '_detail', FeedParserDict())
1018         context[prefix + '_detail'][key] = value
1019         self._sync_author_detail()
1020
1021     def _save_contributor(self, key, value):
1022         context = self._getContext()
1023         context.setdefault('contributors', [FeedParserDict()])
1024         context['contributors'][-1][key] = value
1025
1026     def _sync_author_detail(self, key='author'):
1027         context = self._getContext()
1028         detail = context.get('%s_detail' % key)
1029         if detail:
1030             name = detail.get('name')
1031             email = detail.get('email')
1032             if name and email:
1033                 context[key] = '%s (%s)' % (name, email)
1034             elif name:
1035                 context[key] = name
1036             elif email:
1037                 context[key] = email
1038         else:
1039             author = context.get(key)
1040             if not author: return
1041             emailmatch = re.search(r'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))''', author)
1042             if not emailmatch: return
1043             email = emailmatch.group(0)
1044             # probably a better way to do the following, but it passes all the tests
1045             author = author.replace(email, '')
1046             author = author.replace('()', '')
1047             author = author.strip()
1048             if author and (author[0] == '('):
1049                 author = author[1:]
1050             if author and (author[-1] == ')'):
1051                 author = author[:-1]
1052             author = author.strip()
1053             context.setdefault('%s_detail' % key, FeedParserDict())
1054             context['%s_detail' % key]['name'] = author
1055             context['%s_detail' % key]['email'] = email
1056
1057     def _start_subtitle(self, attrsD):
1058         self.pushContent('subtitle', attrsD, 'text/plain', 1)
1059     _start_tagline = _start_subtitle
1060     _start_itunes_subtitle = _start_subtitle
1061
1062     def _end_subtitle(self):
1063         self.popContent('subtitle')
1064     _end_tagline = _end_subtitle
1065     _end_itunes_subtitle = _end_subtitle
1066             
1067     def _start_rights(self, attrsD):
1068         self.pushContent('rights', attrsD, 'text/plain', 1)
1069     _start_dc_rights = _start_rights
1070     _start_copyright = _start_rights
1071
1072     def _end_rights(self):
1073         self.popContent('rights')
1074     _end_dc_rights = _end_rights
1075     _end_copyright = _end_rights
1076
1077     def _start_item(self, attrsD):
1078         self.entries.append(FeedParserDict())
1079         self.push('item', 0)
1080         self.inentry = 1
1081         self.guidislink = 0
1082         id = self._getAttribute(attrsD, 'rdf:about')
1083         if id:
1084             context = self._getContext()
1085             context['id'] = id
1086         self._cdf_common(attrsD)
1087     _start_entry = _start_item
1088     _start_product = _start_item
1089
1090     def _end_item(self):
1091         self.pop('item')
1092         self.inentry = 0
1093     _end_entry = _end_item
1094
1095     def _start_dc_language(self, attrsD):
1096         self.push('language', 1)
1097     _start_language = _start_dc_language
1098
1099     def _end_dc_language(self):
1100         self.lang = self.pop('language')
1101     _end_language = _end_dc_language
1102
1103     def _start_dc_publisher(self, attrsD):
1104         self.push('publisher', 1)
1105     _start_webmaster = _start_dc_publisher
1106
1107     def _end_dc_publisher(self):
1108         self.pop('publisher')
1109         self._sync_author_detail('publisher')
1110     _end_webmaster = _end_dc_publisher
1111
1112     def _start_published(self, attrsD):
1113         self.push('published', 1)
1114     _start_dcterms_issued = _start_published
1115     _start_issued = _start_published
1116
1117     def _end_published(self):
1118         value = self.pop('published')
1119         self._save('published_parsed', _parse_date(value))
1120     _end_dcterms_issued = _end_published
1121     _end_issued = _end_published
1122
1123     def _start_updated(self, attrsD):
1124         self.push('updated', 1)
1125     _start_modified = _start_updated
1126     _start_dcterms_modified = _start_updated
1127     _start_pubdate = _start_updated
1128     _start_dc_date = _start_updated
1129
1130     def _end_updated(self):
1131         value = self.pop('updated')
1132         parsed_value = _parse_date(value)
1133         self._save('updated_parsed', parsed_value)
1134     _end_modified = _end_updated
1135     _end_dcterms_modified = _end_updated
1136     _end_pubdate = _end_updated
1137     _end_dc_date = _end_updated
1138
1139     def _start_created(self, attrsD):
1140         self.push('created', 1)
1141     _start_dcterms_created = _start_created
1142
1143     def _end_created(self):
1144         value = self.pop('created')
1145         self._save('created_parsed', _parse_date(value))
1146     _end_dcterms_created = _end_created
1147
1148     def _start_expirationdate(self, attrsD):
1149         self.push('expired', 1)
1150
1151     def _end_expirationdate(self):
1152         self._save('expired_parsed', _parse_date(self.pop('expired')))
1153
1154     def _start_cc_license(self, attrsD):
1155         self.push('license', 1)
1156         value = self._getAttribute(attrsD, 'rdf:resource')
1157         if value:
1158             self.elementstack[-1][2].append(value)
1159         self.pop('license')
1160         
1161     def _start_creativecommons_license(self, attrsD):
1162         self.push('license', 1)
1163
1164     def _end_creativecommons_license(self):
1165         self.pop('license')
1166
1167     def _addTag(self, term, scheme, label):
1168         context = self._getContext()
1169         tags = context.setdefault('tags', [])
1170         if (not term) and (not scheme) and (not label): return
1171         value = FeedParserDict({'term': term, 'scheme': scheme, 'label': label})
1172         if value not in tags:
1173             tags.append(FeedParserDict({'term': term, 'scheme': scheme, 'label': label}))
1174
1175     def _start_category(self, attrsD):
1176         if _debug: sys.stderr.write('entering _start_category with %s\n' % repr(attrsD))
1177         term = attrsD.get('term')
1178         scheme = attrsD.get('scheme', attrsD.get('domain'))
1179         label = attrsD.get('label')
1180         self._addTag(term, scheme, label)
1181         self.push('category', 1)
1182     _start_dc_subject = _start_category
1183     _start_keywords = _start_category
1184         
1185     def _end_itunes_keywords(self):
1186         for term in self.pop('itunes_keywords').split():
1187             self._addTag(term, 'http://www.itunes.com/', None)
1188         
1189     def _start_itunes_category(self, attrsD):
1190         self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
1191         self.push('category', 1)
1192         
1193     def _end_category(self):
1194         value = self.pop('category')
1195         if not value: return
1196         context = self._getContext()
1197         tags = context['tags']
1198         if value and len(tags) and not tags[-1]['term']:
1199             tags[-1]['term'] = value
1200         else:
1201             self._addTag(value, None, None)
1202     _end_dc_subject = _end_category
1203     _end_keywords = _end_category
1204     _end_itunes_category = _end_category
1205
1206     def _start_cloud(self, attrsD):
1207         self._getContext()['cloud'] = FeedParserDict(attrsD)
1208         
1209     def _start_link(self, attrsD):
1210         attrsD.setdefault('rel', 'alternate')
1211         attrsD.setdefault('type', 'text/html')
1212         attrsD = self._itsAnHrefDamnIt(attrsD)
1213         if attrsD.has_key('href'):
1214             attrsD['href'] = self.resolveURI(attrsD['href'])
1215         expectingText = self.infeed or self.inentry or self.insource
1216         context = self._getContext()
1217         context.setdefault('links', [])
1218         context['links'].append(FeedParserDict(attrsD))
1219         if attrsD['rel'] == 'enclosure':
1220             self._start_enclosure(attrsD)
1221         if attrsD.has_key('href'):
1222             expectingText = 0
1223             if (attrsD.get('rel') == 'alternate') and (self.mapContentType(attrsD.get('type')) in self.html_types):
1224                 context['link'] = attrsD['href']
1225         else:
1226             self.push('link', expectingText)
1227     _start_producturl = _start_link
1228
1229     def _end_link(self):
1230         value = self.pop('link')
1231         context = self._getContext()
1232         if self.intextinput:
1233             context['textinput']['link'] = value
1234         if self.inimage:
1235             context['image']['link'] = value
1236     _end_producturl = _end_link
1237
1238     def _start_guid(self, attrsD):
1239         self.guidislink = (attrsD.get('ispermalink', 'true') == 'true')
1240         self.push('id', 1)
1241
1242     def _end_guid(self):
1243         value = self.pop('id')
1244         self._save('guidislink', self.guidislink and not self._getContext().has_key('link'))
1245         if self.guidislink:
1246             # guid acts as link, but only if 'ispermalink' is not present or is 'true',
1247             # and only if the item doesn't already have a link element
1248             self._save('link', value)
1249
1250     def _start_title(self, attrsD):
1251         self.pushContent('title', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
1252     _start_dc_title = _start_title
1253     _start_media_title = _start_title
1254
1255     def _end_title(self):
1256         value = self.popContent('title')
1257         context = self._getContext()
1258         if self.intextinput:
1259             context['textinput']['title'] = value
1260         elif self.inimage:
1261             context['image']['title'] = value
1262     _end_dc_title = _end_title
1263     _end_media_title = _end_title
1264
1265     def _start_description(self, attrsD):
1266         context = self._getContext()
1267         if context.has_key('summary'):
1268             self._summaryKey = 'content'
1269             self._start_content(attrsD)
1270         else:
1271             self.pushContent('description', attrsD, 'text/html', self.infeed or self.inentry or self.insource)
1272
1273     def _start_abstract(self, attrsD):
1274         self.pushContent('description', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
1275
1276     def _end_description(self):
1277         if self._summaryKey == 'content':
1278             self._end_content()
1279         else:
1280             value = self.popContent('description')
1281             context = self._getContext()
1282             if self.intextinput:
1283                 context['textinput']['description'] = value
1284             elif self.inimage:
1285                 context['image']['description'] = value
1286         self._summaryKey = None
1287     _end_abstract = _end_description
1288
1289     def _start_info(self, attrsD):
1290         self.pushContent('info', attrsD, 'text/plain', 1)
1291     _start_feedburner_browserfriendly = _start_info
1292
1293     def _end_info(self):
1294         self.popContent('info')
1295     _end_feedburner_browserfriendly = _end_info
1296
1297     def _start_generator(self, attrsD):
1298         if attrsD:
1299             attrsD = self._itsAnHrefDamnIt(attrsD)
1300             if attrsD.has_key('href'):
1301                 attrsD['href'] = self.resolveURI(attrsD['href'])
1302         self._getContext()['generator_detail'] = FeedParserDict(attrsD)
1303         self.push('generator', 1)
1304
1305     def _end_generator(self):
1306         value = self.pop('generator')
1307         context = self._getContext()
1308         if context.has_key('generator_detail'):
1309             context['generator_detail']['name'] = value
1310             
1311     def _start_admin_generatoragent(self, attrsD):
1312         self.push('generator', 1)
1313         value = self._getAttribute(attrsD, 'rdf:resource')
1314         if value:
1315             self.elementstack[-1][2].append(value)
1316         self.pop('generator')
1317         self._getContext()['generator_detail'] = FeedParserDict({'href': value})
1318
1319     def _start_admin_errorreportsto(self, attrsD):
1320         self.push('errorreportsto', 1)
1321         value = self._getAttribute(attrsD, 'rdf:resource')
1322         if value:
1323             self.elementstack[-1][2].append(value)
1324         self.pop('errorreportsto')
1325         
1326     def _start_summary(self, attrsD):
1327         context = self._getContext()
1328         if context.has_key('summary'):
1329             self._summaryKey = 'content'
1330             self._start_content(attrsD)
1331         else:
1332             self._summaryKey = 'summary'
1333             self.pushContent(self._summaryKey, attrsD, 'text/plain', 1)
1334     _start_itunes_summary = _start_summary
1335
1336     def _end_summary(self):
1337         if self._summaryKey == 'content':
1338             self._end_content()
1339         else:
1340             self.popContent(self._summaryKey or 'summary')
1341         self._summaryKey = None
1342     _end_itunes_summary = _end_summary
1343         
1344     def _start_enclosure(self, attrsD):
1345         attrsD = self._itsAnHrefDamnIt(attrsD)
1346         self._getContext().setdefault('enclosures', []).append(FeedParserDict(attrsD))
1347         href = attrsD.get('href')
1348         if href:
1349             context = self._getContext()
1350             if not context.get('id'):
1351                 context['id'] = href
1352             
1353     def _start_source(self, attrsD):
1354         self.insource = 1
1355
1356     def _end_source(self):
1357         self.insource = 0
1358         self._getContext()['source'] = copy.deepcopy(self.sourcedata)
1359         self.sourcedata.clear()
1360
1361     def _start_content(self, attrsD):
1362         self.pushContent('content', attrsD, 'text/plain', 1)
1363         src = attrsD.get('src')
1364         if src:
1365             self.contentparams['src'] = src
1366         self.push('content', 1)
1367
1368     def _start_prodlink(self, attrsD):
1369         self.pushContent('content', attrsD, 'text/html', 1)
1370
1371     def _start_body(self, attrsD):
1372         self.pushContent('content', attrsD, 'application/xhtml+xml', 1)
1373     _start_xhtml_body = _start_body
1374
1375     def _start_content_encoded(self, attrsD):
1376         self.pushContent('content', attrsD, 'text/html', 1)
1377     _start_fullitem = _start_content_encoded
1378
1379     def _end_content(self):
1380         copyToDescription = self.mapContentType(self.contentparams.get('type')) in (['text/plain'] + self.html_types)
1381         value = self.popContent('content')
1382         if copyToDescription:
1383             self._save('description', value)
1384     _end_body = _end_content
1385     _end_xhtml_body = _end_content
1386     _end_content_encoded = _end_content
1387     _end_fullitem = _end_content
1388     _end_prodlink = _end_content
1389
1390     def _start_itunes_image(self, attrsD):
1391         self.push('itunes_image', 0)
1392         self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
1393     _start_itunes_link = _start_itunes_image
1394         
1395     def _end_itunes_block(self):
1396         value = self.pop('itunes_block', 0)
1397         self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0
1398
1399     def _end_itunes_explicit(self):
1400         value = self.pop('itunes_explicit', 0)
1401         self._getContext()['itunes_explicit'] = (value == 'yes') and 1 or 0
1402
1403 if _XML_AVAILABLE:
1404     class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler):
1405         def __init__(self, baseuri, baselang, encoding):
1406             if _debug: sys.stderr.write('trying StrictFeedParser\n')
1407             xml.sax.handler.ContentHandler.__init__(self)
1408             _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
1409             self.bozo = 0
1410             self.exc = None
1411         
1412         def startPrefixMapping(self, prefix, uri):
1413             self.trackNamespace(prefix, uri)
1414         
1415         def startElementNS(self, name, qname, attrs):
1416             namespace, localname = name
1417             lowernamespace = str(namespace or '').lower()
1418             if lowernamespace.find('backend.userland.com/rss') <> -1:
1419                 # match any backend.userland.com namespace
1420                 namespace = 'http://backend.userland.com/rss'
1421                 lowernamespace = namespace
1422             if qname and qname.find(':') > 0:
1423                 givenprefix = qname.split(':')[0]
1424             else:
1425                 givenprefix = None
1426             prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
1427             if givenprefix and (prefix == None or (prefix == '' and lowernamespace == '')) and not self.namespacesInUse.has_key(givenprefix):
1428                     raise UndeclaredNamespace, "'%s' is not associated with a namespace" % givenprefix
1429             if prefix:
1430                 localname = prefix + ':' + localname
1431             localname = str(localname).lower()
1432             if _debug: sys.stderr.write('startElementNS: qname = %s, namespace = %s, givenprefix = %s, prefix = %s, attrs = %s, localname = %s\n' % (qname, namespace, givenprefix, prefix, attrs.items(), localname))
1433
1434             # qname implementation is horribly broken in Python 2.1 (it
1435             # doesn't report any), and slightly broken in Python 2.2 (it
1436             # doesn't report the xml: namespace). So we match up namespaces
1437             # with a known list first, and then possibly override them with
1438             # the qnames the SAX parser gives us (if indeed it gives us any
1439             # at all).  Thanks to MatejC for helping me test this and
1440             # tirelessly telling me that it didn't work yet.
1441             attrsD = {}
1442             for (namespace, attrlocalname), attrvalue in attrs._attrs.items():
1443                 lowernamespace = (namespace or '').lower()
1444                 prefix = self._matchnamespaces.get(lowernamespace, '')
1445                 if prefix:
1446                     attrlocalname = prefix + ':' + attrlocalname
1447                 attrsD[str(attrlocalname).lower()] = attrvalue
1448             for qname in attrs.getQNames():
1449                 attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
1450             self.unknown_starttag(localname, attrsD.items())
1451
1452         def characters(self, text):
1453             self.handle_data(text)
1454
1455         def endElementNS(self, name, qname):
1456             namespace, localname = name
1457             lowernamespace = str(namespace or '').lower()
1458             if qname and qname.find(':') > 0:
1459                 givenprefix = qname.split(':')[0]
1460             else:
1461                 givenprefix = ''
1462             prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
1463             if prefix:
1464                 localname = prefix + ':' + localname
1465             localname = str(localname).lower()
1466             self.unknown_endtag(localname)
1467
1468         def error(self, exc):
1469             self.bozo = 1
1470             self.exc = exc
1471             
1472         def fatalError(self, exc):
1473             self.error(exc)
1474             raise exc
1475
1476 class _BaseHTMLProcessor(sgmllib.SGMLParser):
1477     elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr',
1478       'img', 'input', 'isindex', 'link', 'meta', 'param']
1479     
1480     def __init__(self, encoding):
1481         self.encoding = encoding
1482         if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding)
1483         sgmllib.SGMLParser.__init__(self)
1484         
1485     def reset(self):
1486         self.pieces = []
1487         sgmllib.SGMLParser.reset(self)
1488
1489     def _shorttag_replace(self, match):
1490         tag = match.group(1)
1491         if tag in self.elements_no_end_tag:
1492             return '<' + tag + ' />'
1493         else:
1494             return '<' + tag + '></' + tag + '>'
1495         
1496     def feed(self, data):
1497         data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'&lt;!\1', data)
1498         #data = re.sub(r'<(\S+?)\s*?/>', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace
1499         data = re.sub(r'<([^<\s]+?)\s*/>', self._shorttag_replace, data) 
1500         data = data.replace('&#39;', "'")
1501         data = data.replace('&#34;', '"')
1502         if self.encoding and type(data) == type(u''):
1503             data = data.encode(self.encoding)
1504         sgmllib.SGMLParser.feed(self, data)
1505         sgmllib.SGMLParser.close(self)
1506
1507     def normalize_attrs(self, attrs):
1508         # utility method to be called by descendants
1509         attrs = [(k.lower(), v) for k, v in attrs]
1510         attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
1511         return attrs
1512
1513     def unknown_starttag(self, tag, attrs):
1514         # called for each start tag
1515         # attrs is a list of (attr, value) tuples
1516         # e.g. for <pre class='screen'>, tag='pre', attrs=[('class', 'screen')]
1517         if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag)
1518         uattrs = []
1519         # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
1520         for key, value in attrs:
1521             if type(value) != type(u''):
1522                 value = unicode(value, self.encoding)
1523             uattrs.append((unicode(key, self.encoding), value))
1524         strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs]).encode(self.encoding)
1525         if tag in self.elements_no_end_tag:
1526             self.pieces.append('<%(tag)s%(strattrs)s />' % locals())
1527         else:
1528             self.pieces.append('<%(tag)s%(strattrs)s>' % locals())
1529
1530     def unknown_endtag(self, tag):
1531         # called for each end tag, e.g. for </pre>, tag will be 'pre'
1532         # Reconstruct the original end tag.
1533         if tag not in self.elements_no_end_tag:
1534             self.pieces.append("</%(tag)s>" % locals())
1535
1536     def handle_charref(self, ref):
1537         # called for each character reference, e.g. for '&#160;', ref will be '160'
1538         # Reconstruct the original character reference.
1539         self.pieces.append('&#%(ref)s;' % locals())
1540         
1541     def handle_entityref(self, ref):
1542         # called for each entity reference, e.g. for '&copy;', ref will be 'copy'
1543         # Reconstruct the original entity reference.
1544         import htmlentitydefs
1545         if not hasattr(htmlentitydefs, 'name2codepoint') or htmlentitydefs.name2codepoint.has_key(ref):
1546             self.pieces.append('&%(ref)s;' % locals())
1547         else:
1548             self.pieces.append('&amp;%(ref)s' % locals())
1549
1550     def handle_data(self, text):
1551         # called for each block of plain text, i.e. outside of any tag and
1552         # not containing any character or entity references
1553         # Store the original text verbatim.
1554         if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text)
1555         self.pieces.append(text)
1556         
1557     def handle_comment(self, text):
1558         # called for each HTML comment, e.g. <!-- insert Javascript code here -->
1559         # Reconstruct the original comment.
1560         self.pieces.append('<!--%(text)s-->' % locals())
1561         
1562     def handle_pi(self, text):
1563         # called for each processing instruction, e.g. <?instruction>
1564         # Reconstruct original processing instruction.
1565         self.pieces.append('<?%(text)s>' % locals())
1566
1567     def handle_decl(self, text):
1568         # called for the DOCTYPE, if present, e.g.
1569         # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
1570         #     "http://www.w3.org/TR/html4/loose.dtd">
1571         # Reconstruct original DOCTYPE
1572         self.pieces.append('<!%(text)s>' % locals())
1573         
1574     _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
1575     def _scan_name(self, i, declstartpos):
1576         rawdata = self.rawdata
1577         n = len(rawdata)
1578         if i == n:
1579             return None, -1
1580         m = self._new_declname_match(rawdata, i)
1581         if m:
1582             s = m.group()
1583             name = s.strip()
1584             if (i + len(s)) == n:
1585                 return None, -1  # end of buffer
1586             return name.lower(), m.end()
1587         else:
1588             self.handle_data(rawdata)
1589 #            self.updatepos(declstartpos, i)
1590             return None, -1
1591
1592     def output(self):
1593         '''Return processed HTML as a single string'''
1594         return ''.join([str(p) for p in self.pieces])
1595
1596 class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
1597     def __init__(self, baseuri, baselang, encoding):
1598         sgmllib.SGMLParser.__init__(self)
1599         _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
1600
1601     def decodeEntities(self, element, data):
1602         data = data.replace('&#60;', '&lt;')
1603         data = data.replace('&#x3c;', '&lt;')
1604         data = data.replace('&#x3C;', '&lt;')
1605         data = data.replace('&#62;', '&gt;')
1606         data = data.replace('&#x3e;', '&gt;')
1607         data = data.replace('&#x3E;', '&gt;')
1608         data = data.replace('&#38;', '&amp;')
1609         data = data.replace('&#x26;', '&amp;')
1610         data = data.replace('&#34;', '&quot;')
1611         data = data.replace('&#x22;', '&quot;')
1612         data = data.replace('&#39;', '&apos;')
1613         data = data.replace('&#x27;', '&apos;')
1614         if self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
1615             data = data.replace('&lt;', '<')
1616             data = data.replace('&gt;', '>')
1617             data = data.replace('&amp;', '&')
1618             data = data.replace('&quot;', '"')
1619             data = data.replace('&apos;', "'")
1620         return data
1621         
1622     def strattrs(self, attrs):
1623         return ''.join([' %s="%s"' % t for t in attrs])
1624  
1625 class _RelativeURIResolver(_BaseHTMLProcessor):
1626     relative_uris = [('a', 'href'),
1627                      ('applet', 'codebase'),
1628                      ('area', 'href'),
1629                      ('blockquote', 'cite'),
1630                      ('body', 'background'),
1631                      ('del', 'cite'),
1632                      ('form', 'action'),
1633                      ('frame', 'longdesc'),
1634                      ('frame', 'src'),
1635                      ('iframe', 'longdesc'),
1636                      ('iframe', 'src'),
1637                      ('head', 'profile'),
1638                      ('img', 'longdesc'),
1639                      ('img', 'src'),
1640                      ('img', 'usemap'),
1641                      ('input', 'src'),
1642                      ('input', 'usemap'),
1643                      ('ins', 'cite'),
1644                      ('link', 'href'),
1645                      ('object', 'classid'),
1646                      ('object', 'codebase'),
1647                      ('object', 'data'),
1648                      ('object', 'usemap'),
1649                      ('q', 'cite'),
1650                      ('script', 'src')]
1651
1652     def __init__(self, baseuri, encoding):
1653         _BaseHTMLProcessor.__init__(self, encoding)
1654         self.baseuri = baseuri
1655
1656     def resolveURI(self, uri):
1657         return _urljoin(self.baseuri, uri)
1658     
1659     def unknown_starttag(self, tag, attrs):
1660         attrs = self.normalize_attrs(attrs)
1661         attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs]
1662         _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
1663         
1664 def _resolveRelativeURIs(htmlSource, baseURI, encoding):
1665     if _debug: sys.stderr.write('entering _resolveRelativeURIs\n')
1666     p = _RelativeURIResolver(baseURI, encoding)
1667     p.feed(htmlSource)
1668     return p.output()
1669
1670 class _HTMLSanitizer(_BaseHTMLProcessor):
1671     acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
1672       'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col',
1673       'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset',
1674       'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
1675       'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
1676       'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small', 'span', 'strike',
1677       'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
1678       'thead', 'tr', 'tt', 'u', 'ul', 'var']
1679
1680     acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
1681       'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing',
1682       'char', 'charoff', 'charset', 'checked', 'cite', 'class', 'clear', 'cols',
1683       'colspan', 'color', 'compact', 'coords', 'datetime', 'dir', 'disabled',
1684       'enctype', 'for', 'frame', 'headers', 'height', 'href', 'hreflang', 'hspace',
1685       'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'media', 'method',
1686       'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly',
1687       'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
1688       'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type',
1689       'usemap', 'valign', 'value', 'vspace', 'width', 'xml:lang']
1690
1691     unacceptable_elements_with_end_tag = ['script', 'applet']
1692
1693     def reset(self):
1694         _BaseHTMLProcessor.reset(self)
1695         self.unacceptablestack = 0
1696         
1697     def unknown_starttag(self, tag, attrs):
1698         if not tag in self.acceptable_elements:
1699             if tag in self.unacceptable_elements_with_end_tag:
1700                 self.unacceptablestack += 1
1701             return
1702         attrs = self.normalize_attrs(attrs)
1703         attrs = [(key, value) for key, value in attrs if key in self.acceptable_attributes]
1704         _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
1705         
1706     def unknown_endtag(self, tag):
1707         if not tag in self.acceptable_elements:
1708             if tag in self.unacceptable_elements_with_end_tag:
1709                 self.unacceptablestack -= 1
1710             return
1711         _BaseHTMLProcessor.unknown_endtag(self, tag)
1712
1713     def handle_pi(self, text):
1714         pass
1715
1716     def handle_decl(self, text):
1717         pass
1718
1719     def handle_data(self, text):
1720         if not self.unacceptablestack:
1721             _BaseHTMLProcessor.handle_data(self, text)
1722
1723 def _sanitizeHTML(htmlSource, encoding):
1724     p = _HTMLSanitizer(encoding)
1725     p.feed(htmlSource)
1726     data = p.output()
1727     if TIDY_MARKUP:
1728         # loop through list of preferred Tidy interfaces looking for one that's installed,
1729         # then set up a common _tidy function to wrap the interface-specific API.
1730         _tidy = None
1731         for tidy_interface in PREFERRED_TIDY_INTERFACES:
1732             try:
1733                 if tidy_interface == "uTidy":
1734                     from tidy import parseString as _utidy
1735                     def _tidy(data, **kwargs):
1736                         return str(_utidy(data, **kwargs))
1737                     break
1738                 elif tidy_interface == "mxTidy":
1739                     from mx.Tidy import Tidy as _mxtidy
1740                     def _tidy(data, **kwargs):
1741                         nerrors, nwarnings, data, errordata = _mxtidy.tidy(data, **kwargs)
1742                         return data
1743                     break
1744             except:
1745                 pass
1746         if _tidy:
1747             utf8 = type(data) == type(u'')
1748             if utf8:
1749                 data = data.encode('utf-8')
1750             data = _tidy(data, output_xhtml=1, numeric_entities=1, wrap=0, char_encoding="utf8")
1751             if utf8:
1752                 data = unicode(data, 'utf-8')
1753             if data.count('<body'):
1754                 data = data.split('<body', 1)[1]
1755                 if data.count('>'):
1756                     data = data.split('>', 1)[1]
1757             if data.count('</body'):
1758                 data = data.split('</body', 1)[0]
1759     data = data.strip().replace('\r\n', '\n')
1760     return data
1761
1762 class _FeedURLHandler(urllib2.HTTPDigestAuthHandler, urllib2.HTTPRedirectHandler, urllib2.HTTPDefaultErrorHandler):
1763     def http_error_default(self, req, fp, code, msg, headers):
1764         if ((code / 100) == 3) and (code != 304):
1765             return self.http_error_302(req, fp, code, msg, headers)
1766         infourl = urllib.addinfourl(fp, headers, req.get_full_url())
1767         infourl.status = code
1768         return infourl
1769
1770     def http_error_302(self, req, fp, code, msg, headers):
1771         if headers.dict.has_key('location'):
1772             infourl = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
1773         else:
1774             infourl = urllib.addinfourl(fp, headers, req.get_full_url())
1775         if not hasattr(infourl, 'status'):
1776             infourl.status = code
1777         return infourl
1778
1779     def http_error_301(self, req, fp, code, msg, headers):
1780         if headers.dict.has_key('location'):
1781             infourl = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers)
1782         else:
1783             infourl = urllib.addinfourl(fp, headers, req.get_full_url())
1784         if not hasattr(infourl, 'status'):
1785             infourl.status = code
1786         return infourl
1787
1788     http_error_300 = http_error_302
1789     http_error_303 = http_error_302
1790     http_error_307 = http_error_302
1791         
1792     def http_error_401(self, req, fp, code, msg, headers):
1793         # Check if
1794         # - server requires digest auth, AND
1795         # - we tried (unsuccessfully) with basic auth, AND
1796         # - we're using Python 2.3.3 or later (digest auth is irreparably broken in earlier versions)
1797         # If all conditions hold, parse authentication information
1798         # out of the Authorization header we sent the first time
1799         # (for the username and password) and the WWW-Authenticate
1800         # header the server sent back (for the realm) and retry
1801         # the request with the appropriate digest auth headers instead.
1802         # This evil genius hack has been brought to you by Aaron Swartz.
1803         host = urlparse.urlparse(req.get_full_url())[1]
1804         try:
1805             assert sys.version.split()[0] >= '2.3.3'
1806             assert base64 != None
1807             user, passw = base64.decodestring(req.headers['Authorization'].split(' ')[1]).split(':')
1808             realm = re.findall('realm="([^"]*)"', headers['WWW-Authenticate'])[0]
1809             self.add_password(realm, host, user, passw)
1810             retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
1811             self.reset_retry_count()
1812             return retry
1813         except:
1814             return self.http_error_default(req, fp, code, msg, headers)
1815
1816 def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers):
1817     """URL, filename, or string --> stream
1818
1819     This function lets you define parsers that take any input source
1820     (URL, pathname to local or network file, or actual data as a string)
1821     and deal with it in a uniform manner.  Returned object is guaranteed
1822     to have all the basic stdio read methods (read, readline, readlines).
1823     Just .close() the object when you're done with it.
1824
1825     If the etag argument is supplied, it will be used as the value of an
1826     If-None-Match request header.
1827
1828     If the modified argument is supplied, it must be a tuple of 9 integers
1829     as returned by gmtime() in the standard Python time module. This MUST
1830     be in GMT (Greenwich Mean Time). The formatted date/time will be used
1831     as the value of an If-Modified-Since request header.
1832
1833     If the agent argument is supplied, it will be used as the value of a
1834     User-Agent request header.
1835
1836     If the referrer argument is supplied, it will be used as the value of a
1837     Referer[sic] request header.
1838
1839     If handlers is supplied, it is a list of handlers used to build a
1840     urllib2 opener.
1841     """
1842
1843     if hasattr(url_file_stream_or_string, 'read'):
1844         return url_file_stream_or_string
1845
1846     if url_file_stream_or_string == '-':
1847         return sys.stdin
1848
1849     if urlparse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp'):
1850         if not agent:
1851             agent = USER_AGENT
1852         # test for inline user:password for basic auth
1853         auth = None
1854         if base64:
1855             urltype, rest = urllib.splittype(url_file_stream_or_string)
1856             realhost, rest = urllib.splithost(rest)
1857             if realhost:
1858                 user_passwd, realhost = urllib.splituser(realhost)
1859                 if user_passwd:
1860                     url_file_stream_or_string = '%s://%s%s' % (urltype, realhost, rest)
1861                     auth = base64.encodestring(user_passwd).strip()
1862         # try to open with urllib2 (to use optional headers)
1863         request = urllib2.Request(url_file_stream_or_string)
1864         request.add_header('User-Agent', agent)
1865         if etag:
1866             request.add_header('If-None-Match', etag)
1867         if modified:
1868             # format into an RFC 1123-compliant timestamp. We can't use
1869             # time.strftime() since the %a and %b directives can be affected
1870             # by the current locale, but RFC 2616 states that dates must be
1871             # in English.
1872             short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
1873             months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
1874             request.add_header('If-Modified-Since', '%s, %02d %s %04d %02d:%02d:%02d GMT' % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5]))
1875         if referrer:
1876             request.add_header('Referer', referrer)
1877         if gzip and zlib:
1878             request.add_header('Accept-encoding', 'gzip, deflate')
1879         elif gzip:
1880             request.add_header('Accept-encoding', 'gzip')
1881         elif zlib:
1882             request.add_header('Accept-encoding', 'deflate')
1883         else:
1884             request.add_header('Accept-encoding', '')
1885         if auth:
1886             request.add_header('Authorization', 'Basic %s' % auth)
1887         if ACCEPT_HEADER:
1888             request.add_header('Accept', ACCEPT_HEADER)
1889         request.add_header('A-IM', 'feed') # RFC 3229 support
1890         opener = apply(urllib2.build_opener, tuple([_FeedURLHandler()] + handlers))
1891         opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent
1892         try:
1893             return opener.open(request)
1894         finally:
1895             opener.close() # JohnD
1896     
1897     # try to open with native open function (if url_file_stream_or_string is a filename)
1898     try:
1899         return open(url_file_stream_or_string)
1900     except:
1901         pass
1902
1903     # treat url_file_stream_or_string as string
1904     return _StringIO(str(url_file_stream_or_string))
1905
1906 _date_handlers = []
1907 def registerDateHandler(func):
1908     '''Register a date handler function (takes string, returns 9-tuple date in GMT)'''
1909     _date_handlers.insert(0, func)
1910     
1911 # ISO-8601 date parsing routines written by Fazal Majid.
1912 # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601
1913 # parser is beyond the scope of feedparser and would be a worthwhile addition
1914 # to the Python library.
1915 # A single regular expression cannot parse ISO 8601 date formats into groups
1916 # as the standard is highly irregular (for instance is 030104 2003-01-04 or
1917 # 0301-04-01), so we use templates instead.
1918 # Please note the order in templates is significant because we need a
1919 # greedy match.
1920 _iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-MM', 'YYYY-?OOO',
1921                 'YY-?MM-?DD', 'YY-?OOO', 'YYYY', 
1922                 '-YY-?MM', '-OOO', '-YY',
1923                 '--MM-?DD', '--MM',
1924                 '---DD',
1925                 'CC', '']
1926 _iso8601_re = [
1927     tmpl.replace(
1928     'YYYY', r'(?P<year>\d{4})').replace(
1929     'YY', r'(?P<year>\d\d)').replace(
1930     'MM', r'(?P<month>[01]\d)').replace(
1931     'DD', r'(?P<day>[0123]\d)').replace(
1932     'OOO', r'(?P<ordinal>[0123]\d\d)').replace(
1933     'CC', r'(?P<century>\d\d$)')
1934     + r'(T?(?P<hour>\d{2}):(?P<minute>\d{2})'
1935     + r'(:(?P<second>\d{2}))?'
1936     + r'(?P<tz>[+-](?P<tzhour>\d{2})(:(?P<tzmin>\d{2}))?|Z)?)?'
1937     for tmpl in _iso8601_tmpl]
1938 del tmpl
1939 _iso8601_matches = [re.compile(regex).match for regex in _iso8601_re]
1940 del regex
1941 def _parse_date_iso8601(dateString):
1942     '''Parse a variety of ISO-8601-compatible formats like 20040105'''
1943     m = None
1944     for _iso8601_match in _iso8601_matches:
1945         m = _iso8601_match(dateString)
1946         if m: break
1947     if not m: return
1948     if m.span() == (0, 0): return
1949     params = m.groupdict()
1950     ordinal = params.get('ordinal', 0)
1951     if ordinal:
1952         ordinal = int(ordinal)
1953     else:
1954         ordinal = 0
1955     year = params.get('year', '--')
1956     if not year or year == '--':
1957         year = time.gmtime()[0]
1958     elif len(year) == 2:
1959         # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993
1960         year = 100 * int(time.gmtime()[0] / 100) + int(year)
1961     else:
1962         year = int(year)
1963     month = params.get('month', '-')
1964     if not month or month == '-':
1965         # ordinals are NOT normalized by mktime, we simulate them
1966         # by setting month=1, day=ordinal
1967         if ordinal:
1968             month = 1
1969         else:
1970             month = time.gmtime()[1]
1971     month = int(month)
1972     day = params.get('day', 0)
1973     if not day:
1974         # see above
1975         if ordinal:
1976             day = ordinal
1977         elif params.get('century', 0) or \
1978                  params.get('year', 0) or params.get('month', 0):
1979             day = 1
1980         else:
1981             day = time.gmtime()[2]
1982     else:
1983         day = int(day)
1984     # special case of the century - is the first year of the 21st century
1985     # 2000 or 2001 ? The debate goes on...
1986     if 'century' in params.keys():
1987         year = (int(params['century']) - 1) * 100 + 1
1988     # in ISO 8601 most fields are optional
1989     for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']:
1990         if not params.get(field, None):
1991             params[field] = 0
1992     hour = int(params.get('hour', 0))
1993     minute = int(params.get('minute', 0))
1994     second = int(params.get('second', 0))
1995     # weekday is normalized by mktime(), we can ignore it
1996     weekday = 0
1997     # daylight savings is complex, but not needed for feedparser's purposes
1998     # as time zones, if specified, include mention of whether it is active
1999     # (e.g. PST vs. PDT, CET). Using -1 is implementation-dependent and
2000     # and most implementations have DST bugs
2001     daylight_savings_flag = 0
2002     tm = [year, month, day, hour, minute, second, weekday,
2003           ordinal, daylight_savings_flag]
2004     # ISO 8601 time zone adjustments
2005     tz = params.get('tz')
2006     if tz and tz != 'Z':
2007         if tz[0] == '-':
2008             tm[3] += int(params.get('tzhour', 0))
2009             tm[4] += int(params.get('tzmin', 0))
2010         elif tz[0] == '+':
2011             tm[3] -= int(params.get('tzhour', 0))
2012             tm[4] -= int(params.get('tzmin', 0))
2013         else:
2014             return None
2015     # Python's time.mktime() is a wrapper around the ANSI C mktime(3c)
2016     # which is guaranteed to normalize d/m/y/h/m/s.
2017     # Many implementations have bugs, but we'll pretend they don't.
2018     return time.localtime(time.mktime(tm))
2019 registerDateHandler(_parse_date_iso8601)
2020     
2021 # 8-bit date handling routines written by ytrewq1.
2022 _korean_year  = u'\ub144' # b3e2 in euc-kr
2023 _korean_month = u'\uc6d4' # bff9 in euc-kr
2024 _korean_day   = u'\uc77c' # c0cf in euc-kr
2025 _korean_am    = u'\uc624\uc804' # bfc0 c0fc in euc-kr
2026 _korean_pm    = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr
2027
2028 _korean_onblog_date_re = \
2029     re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' % \
2030                (_korean_year, _korean_month, _korean_day))
2031 _korean_nate_date_re = \
2032     re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' % \
2033                (_korean_am, _korean_pm))
2034 def _parse_date_onblog(dateString):
2035     '''Parse a string according to the OnBlog 8-bit date format'''
2036     m = _korean_onblog_date_re.match(dateString)
2037     if not m: return
2038     w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
2039                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
2040                  'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
2041                  'zonediff': '+09:00'}
2042     if _debug: sys.stderr.write('OnBlog date parsed as: %s\n' % w3dtfdate)
2043     return _parse_date_w3dtf(w3dtfdate)
2044 registerDateHandler(_parse_date_onblog)
2045
2046 def _parse_date_nate(dateString):
2047     '''Parse a string according to the Nate 8-bit date format'''
2048     m = _korean_nate_date_re.match(dateString)
2049     if not m: return
2050     hour = int(m.group(5))
2051     ampm = m.group(4)
2052     if (ampm == _korean_pm):
2053         hour += 12
2054     hour = str(hour)
2055     if len(hour) == 1:
2056         hour = '0' + hour
2057     w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
2058                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
2059                  'hour': hour, 'minute': m.group(6), 'second': m.group(7),\
2060                  'zonediff': '+09:00'}
2061     if _debug: sys.stderr.write('Nate date parsed as: %s\n' % w3dtfdate)
2062     return _parse_date_w3dtf(w3dtfdate)
2063 registerDateHandler(_parse_date_nate)
2064
2065 _mssql_date_re = \
2066     re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?')
2067 def _parse_date_mssql(dateString):
2068     '''Parse a string according to the MS SQL date format'''
2069     m = _mssql_date_re.match(dateString)
2070     if not m: return
2071     w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
2072                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
2073                  'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
2074                  'zonediff': '+09:00'}
2075     if _debug: sys.stderr.write('MS SQL date parsed as: %s\n' % w3dtfdate)
2076     return _parse_date_w3dtf(w3dtfdate)
2077 registerDateHandler(_parse_date_mssql)
2078
2079 # Unicode strings for Greek date strings
2080 _greek_months = \
2081   { \
2082    u'\u0399\u03b1\u03bd': u'Jan',       # c9e1ed in iso-8859-7
2083    u'\u03a6\u03b5\u03b2': u'Feb',       # d6e5e2 in iso-8859-7
2084    u'\u039c\u03ac\u03ce': u'Mar',       # ccdcfe in iso-8859-7
2085    u'\u039c\u03b1\u03ce': u'Mar',       # cce1fe in iso-8859-7
2086    u'\u0391\u03c0\u03c1': u'Apr',       # c1f0f1 in iso-8859-7
2087    u'\u039c\u03ac\u03b9': u'May',       # ccdce9 in iso-8859-7
2088    u'\u039c\u03b1\u03ca': u'May',       # cce1fa in iso-8859-7
2089    u'\u039c\u03b1\u03b9': u'May',       # cce1e9 in iso-8859-7
2090    u'\u0399\u03bf\u03cd\u03bd': u'Jun', # c9effded in iso-8859-7
2091    u'\u0399\u03bf\u03bd': u'Jun',       # c9efed in iso-8859-7
2092    u'\u0399\u03bf\u03cd\u03bb': u'Jul', # c9effdeb in iso-8859-7
2093    u'\u0399\u03bf\u03bb': u'Jul',       # c9f9eb in iso-8859-7
2094    u'\u0391\u03cd\u03b3': u'Aug',       # c1fde3 in iso-8859-7
2095    u'\u0391\u03c5\u03b3': u'Aug',       # c1f5e3 in iso-8859-7
2096    u'\u03a3\u03b5\u03c0': u'Sep',       # d3e5f0 in iso-8859-7
2097    u'\u039f\u03ba\u03c4': u'Oct',       # cfeaf4 in iso-8859-7
2098    u'\u039d\u03bf\u03ad': u'Nov',       # cdefdd in iso-8859-7
2099    u'\u039d\u03bf\u03b5': u'Nov',       # cdefe5 in iso-8859-7
2100    u'\u0394\u03b5\u03ba': u'Dec',       # c4e5ea in iso-8859-7
2101   }
2102
2103 _greek_wdays = \
2104   { \
2105    u'\u039a\u03c5\u03c1': u'Sun', # caf5f1 in iso-8859-7
2106    u'\u0394\u03b5\u03c5': u'Mon', # c4e5f5 in iso-8859-7
2107    u'\u03a4\u03c1\u03b9': u'Tue', # d4f1e9 in iso-8859-7
2108    u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7
2109    u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7
2110    u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7
2111    u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7   
2112   }
2113
2114 _greek_date_format_re = \
2115     re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)')
2116
2117 def _parse_date_greek(dateString):
2118     '''Parse a string according to a Greek 8-bit date format.'''
2119     m = _greek_date_format_re.match(dateString)
2120     if not m: return
2121     try:
2122         wday = _greek_wdays[m.group(1)]
2123         month = _greek_months[m.group(3)]
2124     except:
2125         return
2126     rfc822date = '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s' % \
2127                  {'wday': wday, 'day': m.group(2), 'month': month, 'year': m.group(4),\
2128                   'hour': m.group(5), 'minute': m.group(6), 'second': m.group(7),\
2129                   'zonediff': m.group(8)}
2130     if _debug: sys.stderr.write('Greek date parsed as: %s\n' % rfc822date)
2131     return _parse_date_rfc822(rfc822date)
2132 registerDateHandler(_parse_date_greek)
2133
2134 # Unicode strings for Hungarian date strings
2135 _hungarian_months = \
2136   { \
2137     u'janu\u00e1r':   u'01',  # e1 in iso-8859-2
2138     u'febru\u00e1ri': u'02',  # e1 in iso-8859-2
2139     u'm\u00e1rcius':  u'03',  # e1 in iso-8859-2
2140     u'\u00e1prilis':  u'04',  # e1 in iso-8859-2
2141     u'm\u00e1ujus':   u'05',  # e1 in iso-8859-2
2142     u'j\u00fanius':   u'06',  # fa in iso-8859-2
2143     u'j\u00falius':   u'07',  # fa in iso-8859-2
2144     u'augusztus':     u'08',
2145     u'szeptember':    u'09',
2146     u'okt\u00f3ber':  u'10',  # f3 in iso-8859-2
2147     u'november':      u'11',
2148     u'december':      u'12',
2149   }
2150
2151 _hungarian_date_format_re = \
2152   re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))')
2153
2154 def _parse_date_hungarian(dateString):
2155     '''Parse a string according to a Hungarian 8-bit date format.'''
2156     m = _hungarian_date_format_re.match(dateString)
2157     if not m: return
2158     try:
2159         month = _hungarian_months[m.group(2)]
2160         day = m.group(3)
2161         if len(day) == 1:
2162             day = '0' + day
2163         hour = m.group(4)
2164         if len(hour) == 1:
2165             hour = '0' + hour
2166     except:
2167         return
2168     w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % \
2169                 {'year': m.group(1), 'month': month, 'day': day,\
2170                  'hour': hour, 'minute': m.group(5),\
2171                  'zonediff': m.group(6)}
2172     if _debug: sys.stderr.write('Hungarian date parsed as: %s\n' % w3dtfdate)
2173     return _parse_date_w3dtf(w3dtfdate)
2174 registerDateHandler(_parse_date_hungarian)
2175
2176 # W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
2177 # Drake and licensed under the Python license.  Removed all range checking
2178 # for month, day, hour, minute, and second, since mktime will normalize
2179 # these later
2180 def _parse_date_w3dtf(dateString):
2181     def __extract_date(m):
2182         year = int(m.group('year'))
2183         if year < 100:
2184             year = 100 * int(time.gmtime()[0] / 100) + int(year)
2185         if year < 1000:
2186             return 0, 0, 0
2187         julian = m.group('julian')
2188         if julian:
2189             julian = int(julian)
2190             month = julian / 30 + 1
2191             day = julian % 30 + 1
2192             jday = None
2193             while jday != julian:
2194                 t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
2195                 jday = time.gmtime(t)[-2]
2196                 diff = abs(jday - julian)
2197                 if jday > julian:
2198                     if diff < day:
2199                         day = day - diff
2200                     else:
2201                         month = month - 1
2202                         day = 31
2203                 elif jday < julian:
2204                     if day + diff < 28:
2205                        day = day + diff
2206                     else:
2207                         month = month + 1
2208             return year, month, day
2209         month = m.group('month')
2210         day = 1
2211         if month is None:
2212             month = 1
2213         else:
2214             month = int(month)
2215             day = m.group('day')
2216             if day:
2217                 day = int(day)
2218             else:
2219                 day = 1
2220         return year, month, day
2221
2222     def __extract_time(m):
2223         if not m:
2224             return 0, 0, 0
2225         hours = m.group('hours')
2226         if not hours:
2227             return 0, 0, 0
2228         hours = int(hours)
2229         minutes = int(m.group('minutes'))
2230         seconds = m.group('seconds')
2231         if seconds:
2232             seconds = int(seconds)
2233         else:
2234             seconds = 0
2235         return hours, minutes, seconds
2236
2237     def __extract_tzd(m):
2238         '''Return the Time Zone Designator as an offset in seconds from UTC.'''
2239         if not m:
2240             return 0
2241         tzd = m.group('tzd')
2242         if not tzd:
2243             return 0
2244         if tzd == 'Z':
2245             return 0
2246         hours = int(m.group('tzdhours'))
2247         minutes = m.group('tzdminutes')
2248         if minutes:
2249             minutes = int(minutes)
2250         else:
2251             minutes = 0
2252         offset = (hours*60 + minutes) * 60
2253         if tzd[0] == '+':
2254             return -offset
2255         return offset
2256
2257     __date_re = ('(?P<year>\d\d\d\d)'
2258                  '(?:(?P<dsep>-|)'
2259                  '(?:(?P<julian>\d\d\d)'
2260                  '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
2261     __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
2262     __tzd_rx = re.compile(__tzd_re)
2263     __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
2264                  '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
2265                  + __tzd_re)
2266     __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
2267     __datetime_rx = re.compile(__datetime_re)
2268     m = __datetime_rx.match(dateString)
2269     if (m is None) or (m.group() != dateString): return
2270     gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
2271     if gmt[0] == 0: return
2272     return time.gmtime(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
2273 registerDateHandler(_parse_date_w3dtf)
2274
2275 def _parse_date_rfc822(dateString):
2276     '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
2277     data = dateString.split()
2278     if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
2279         del data[0]
2280     if len(data) == 4:
2281         s = data[3]
2282         i = s.find('+')
2283         if i > 0:
2284             data[3:] = [s[:i], s[i+1:]]
2285         else:
2286             data.append('')
2287         dateString = " ".join(data)
2288     if len(data) < 5:
2289         dateString += ' 00:00:00 GMT'
2290     tm = rfc822.parsedate_tz(dateString)
2291     if tm:
2292         return time.gmtime(rfc822.mktime_tz(tm))
2293 # rfc822.py defines several time zones, but we define some extra ones.
2294 # 'ET' is equivalent to 'EST', etc.
2295 _additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
2296 rfc822._timezones.update(_additional_timezones)
2297 registerDateHandler(_parse_date_rfc822)    
2298
2299 def _parse_date(dateString):
2300     '''Parses a variety of date formats into a 9-tuple in GMT'''
2301     for handler in _date_handlers:
2302         try:
2303             date9tuple = handler(dateString)
2304             if not date9tuple: continue
2305             if len(date9tuple) != 9:
2306                 if _debug: sys.stderr.write('date handler function must return 9-tuple\n')
2307                 raise ValueError
2308             map(int, date9tuple)
2309             return date9tuple
2310         except Exception, e:
2311             if _debug: sys.stderr.write('%s raised %s\n' % (handler.__name__, repr(e)))
2312             pass
2313     return None
2314
2315 def _getCharacterEncoding(http_headers, xml_data):
2316     '''Get the character encoding of the XML document
2317
2318     http_headers is a dictionary
2319     xml_data is a raw string (not Unicode)
2320     
2321     This is so much trickier than it sounds, it's not even funny.
2322     According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type
2323     is application/xml, application/*+xml,
2324     application/xml-external-parsed-entity, or application/xml-dtd,
2325     the encoding given in the charset parameter of the HTTP Content-Type
2326     takes precedence over the encoding given in the XML prefix within the
2327     document, and defaults to 'utf-8' if neither are specified.  But, if
2328     the HTTP Content-Type is text/xml, text/*+xml, or
2329     text/xml-external-parsed-entity, the encoding given in the XML prefix
2330     within the document is ALWAYS IGNORED and only the encoding given in
2331     the charset parameter of the HTTP Content-Type header should be
2332     respected, and it defaults to 'us-ascii' if not specified.
2333
2334     Furthermore, discussion on the atom-syntax mailing list with the
2335     author of RFC 3023 leads me to the conclusion that any document
2336     served with a Content-Type of text/* and no charset parameter
2337     must be treated as us-ascii.  (We now do this.)  And also that it
2338     must always be flagged as non-well-formed.  (We now do this too.)
2339     
2340     If Content-Type is unspecified (input was local file or non-HTTP source)
2341     or unrecognized (server just got it totally wrong), then go by the
2342     encoding given in the XML prefix of the document and default to
2343     'iso-8859-1' as per the HTTP specification (RFC 2616).
2344     
2345     Then, assuming we didn't find a character encoding in the HTTP headers
2346     (and the HTTP Content-type allowed us to look in the body), we need
2347     to sniff the first few bytes of the XML data and try to determine
2348     whether the encoding is ASCII-compatible.  Section F of the XML
2349     specification shows the way here:
2350     http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
2351
2352     If the sniffed encoding is not ASCII-compatible, we need to make it
2353     ASCII compatible so that we can sniff further into the XML declaration
2354     to find the encoding attribute, which will tell us the true encoding.
2355
2356     Of course, none of this guarantees that we will be able to parse the
2357     feed in the declared character encoding (assuming it was declared
2358     correctly, which many are not).  CJKCodecs and iconv_codec help a lot;
2359     you should definitely install them if you can.
2360     http://cjkpython.i18n.org/
2361     '''
2362
2363     def _parseHTTPContentType(content_type):
2364         '''takes HTTP Content-Type header and returns (content type, charset)
2365
2366         If no charset is specified, returns (content type, '')
2367         If no content type is specified, returns ('', '')
2368         Both return parameters are guaranteed to be lowercase strings
2369         '''
2370         content_type = content_type or ''
2371         content_type, params = cgi.parse_header(content_type)
2372         return content_type, params.get('charset', '').replace("'", '')
2373
2374     sniffed_xml_encoding = ''
2375     xml_encoding = ''
2376     true_encoding = ''
2377     http_content_type, http_encoding = _parseHTTPContentType(http_headers.get('content-type'))
2378     # Must sniff for non-ASCII-compatible character encodings before
2379     # searching for XML declaration.  This heuristic is defined in
2380     # section F of the XML specification:
2381     # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
2382     try:
2383         if xml_data[:4] == '\x4c\x6f\xa7\x94':
2384             # EBCDIC
2385             xml_data = _ebcdic_to_ascii(xml_data)
2386         elif xml_data[:4] == '\x00\x3c\x00\x3f':
2387             # UTF-16BE
2388             sniffed_xml_encoding = 'utf-16be'
2389             xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
2390         elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') and (xml_data[2:4] != '\x00\x00'):
2391             # UTF-16BE with BOM
2392             sniffed_xml_encoding = 'utf-16be'
2393             xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
2394         elif xml_data[:4] == '\x3c\x00\x3f\x00':
2395             # UTF-16LE
2396             sniffed_xml_encoding = 'utf-16le'
2397             xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
2398         elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and (xml_data[2:4] != '\x00\x00'):
2399             # UTF-16LE with BOM
2400             sniffed_xml_encoding = 'utf-16le'
2401             xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
2402         elif xml_data[:4] == '\x00\x00\x00\x3c':
2403             # UTF-32BE
2404             sniffed_xml_encoding = 'utf-32be'
2405             xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
2406         elif xml_data[:4] == '\x3c\x00\x00\x00':
2407             # UTF-32LE
2408             sniffed_xml_encoding = 'utf-32le'
2409             xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
2410         elif xml_data[:4] == '\x00\x00\xfe\xff':
2411             # UTF-32BE with BOM
2412             sniffed_xml_encoding = 'utf-32be'
2413             xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
2414         elif xml_data[:4] == '\xff\xfe\x00\x00':
2415             # UTF-32LE with BOM
2416             sniffed_xml_encoding = 'utf-32le'
2417             xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
2418         elif xml_data[:3] == '\xef\xbb\xbf':
2419             # UTF-8 with BOM
2420             sniffed_xml_encoding = 'utf-8'
2421             xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
2422         else:
2423             # ASCII-compatible
2424             pass
2425         xml_encoding_match = re.compile('^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
2426     except:
2427         xml_encoding_match = None
2428     if xml_encoding_match:
2429         xml_encoding = xml_encoding_match.groups()[0].lower()
2430         if sniffed_xml_encoding and (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', 'iso-10646-ucs-4', 'ucs-4', 'csucs4', 'utf-16', 'utf-32', 'utf_16', 'utf_32', 'utf16', 'u16')):
2431             xml_encoding = sniffed_xml_encoding
2432     acceptable_content_type = 0
2433     application_content_types = ('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity')
2434     text_content_types = ('text/xml', 'text/xml-external-parsed-entity')
2435     if (http_content_type in application_content_types) or \
2436        (http_content_type.startswith('application/') and http_content_type.endswith('+xml')):
2437         acceptable_content_type = 1
2438         true_encoding = http_encoding or xml_encoding or 'utf-8'
2439     elif (http_content_type in text_content_types) or \
2440          (http_content_type.startswith('text/')) and http_content_type.endswith('+xml'):
2441         acceptable_content_type = 1
2442         true_encoding = http_encoding or 'us-ascii'
2443     elif http_content_type.startswith('text/'):
2444         true_encoding = http_encoding or 'us-ascii'
2445     elif http_headers and (not http_headers.has_key('content-type')):
2446         true_encoding = xml_encoding or 'iso-8859-1'
2447     else:
2448         true_encoding = xml_encoding or 'utf-8'
2449     return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
2450     
2451 def _toUTF8(data, encoding):
2452     '''Changes an XML data stream on the fly to specify a new encoding
2453
2454     data is a raw sequence of bytes (not Unicode) that is presumed to be in %encoding already
2455     encoding is a string recognized by encodings.aliases
2456     '''
2457     if _debug: sys.stderr.write('entering _toUTF8, trying encoding %s\n' % encoding)
2458     # strip Byte Order Mark (if present)
2459     if (len(data) >= 4) and (data[:2] == '\xfe\xff') and (data[2:4] != '\x00\x00'):
2460         if _debug:
2461             sys.stderr.write('stripping BOM\n')
2462             if encoding != 'utf-16be':
2463                 sys.stderr.write('trying utf-16be instead\n')
2464         encoding = 'utf-16be'
2465         data = data[2:]
2466     elif (len(data) >= 4) and (data[:2] == '\xff\xfe') and (data[2:4] != '\x00\x00'):
2467         if _debug:
2468             sys.stderr.write('stripping BOM\n')
2469             if encoding != 'utf-16le':
2470                 sys.stderr.write('trying utf-16le instead\n')
2471         encoding = 'utf-16le'
2472         data = data[2:]
2473     elif data[:3] == '\xef\xbb\xbf':
2474         if _debug:
2475             sys.stderr.write('stripping BOM\n')
2476             if encoding != 'utf-8':
2477                 sys.stderr.write('trying utf-8 instead\n')
2478         encoding = 'utf-8'
2479         data = data[3:]
2480     elif data[:4] == '\x00\x00\xfe\xff':
2481         if _debug:
2482             sys.stderr.write('stripping BOM\n')
2483             if encoding != 'utf-32be':
2484                 sys.stderr.write('trying utf-32be instead\n')
2485         encoding = 'utf-32be'
2486         data = data[4:]
2487     elif data[:4] == '\xff\xfe\x00\x00':
2488         if _debug:
2489             sys.stderr.write('stripping BOM\n')
2490             if encoding != 'utf-32le':
2491                 sys.stderr.write('trying utf-32le instead\n')
2492         encoding = 'utf-32le'
2493         data = data[4:]
2494     newdata = unicode(data, encoding)
2495     if _debug: sys.stderr.write('successfully converted %s data to unicode\n' % encoding)
2496     declmatch = re.compile('^<\?xml[^>]*?>')
2497     newdecl = '''<?xml version='1.0' encoding='utf-8'?>'''
2498     if declmatch.search(newdata):
2499         newdata = declmatch.sub(newdecl, newdata)
2500     else:
2501         newdata = newdecl + u'\n' + newdata
2502     return newdata.encode('utf-8')
2503
2504 def _stripDoctype(data):
2505     '''Strips DOCTYPE from XML document, returns (rss_version, stripped_data)
2506
2507     rss_version may be 'rss091n' or None
2508     stripped_data is the same XML document, minus the DOCTYPE
2509     '''
2510     entity_pattern = re.compile(r'<!ENTITY([^>]*?)>', re.MULTILINE)
2511     data = entity_pattern.sub('', data)
2512     doctype_pattern = re.compile(r'<!DOCTYPE([^>]*?)>', re.MULTILINE)
2513     doctype_results = doctype_pattern.findall(data)
2514     doctype = doctype_results and doctype_results[0] or ''
2515     if doctype.lower().count('netscape'):
2516         version = 'rss091n'
2517     else:
2518         version = None
2519     data = doctype_pattern.sub('', data)
2520     return version, data
2521     
2522 def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[]):
2523     '''Parse a feed from a URL, file, stream, or string'''
2524     result = FeedParserDict()
2525     result['feed'] = FeedParserDict()
2526     result['entries'] = []
2527     if _XML_AVAILABLE:
2528         result['bozo'] = 0
2529     if type(handlers) == types.InstanceType:
2530         handlers = [handlers]
2531     try:
2532         f = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers)
2533         data = f.read()
2534     except Exception, e:
2535         result['bozo'] = 1
2536         result['bozo_exception'] = e
2537         data = ''
2538         f = None
2539
2540     # if feed is gzip-compressed, decompress it
2541     if f and data and hasattr(f, 'headers'):
2542         if gzip and f.headers.get('content-encoding', '') == 'gzip':
2543             try:
2544                 data = gzip.GzipFile(fileobj=_StringIO(data)).read()
2545             except Exception, e:
2546                 # Some feeds claim to be gzipped but they're not, so
2547                 # we get garbage.  Ideally, we should re-request the
2548                 # feed without the 'Accept-encoding: gzip' header,
2549                 # but we don't.
2550                 result['bozo'] = 1
2551                 result['bozo_exception'] = e
2552                 data = ''
2553         elif zlib and f.headers.get('content-encoding', '') == 'deflate':
2554             try:
2555                 data = zlib.decompress(data, -zlib.MAX_WBITS)
2556             except Exception, e:
2557                 result['bozo'] = 1
2558                 result['bozo_exception'] = e
2559                 data = ''
2560
2561     # save HTTP headers
2562     if hasattr(f, 'info'):
2563         info = f.info()
2564         result['etag'] = info.getheader('ETag')
2565         last_modified = info.getheader('Last-Modified')
2566         if last_modified:
2567             result['modified'] = _parse_date(last_modified)
2568     if hasattr(f, 'url'):
2569         result['href'] = f.url
2570         result['status'] = 200
2571     if hasattr(f, 'status'):
2572         result['status'] = f.status
2573     if hasattr(f, 'headers'):
2574         result['headers'] = f.headers.dict
2575     if hasattr(f, 'close'):
2576         f.close()
2577
2578     # there are four encodings to keep track of:
2579     # - http_encoding is the encoding declared in the Content-Type HTTP header
2580     # - xml_encoding is the encoding declared in the <?xml declaration
2581     # - sniffed_encoding is the encoding sniffed from the first 4 bytes of the XML data
2582     # - result['encoding'] is the actual encoding, as per RFC 3023 and a variety of other conflicting specifications
2583     http_headers = result.get('headers', {})
2584     result['encoding'], http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type = \
2585         _getCharacterEncoding(http_headers, data)
2586     if http_headers and (not acceptable_content_type):
2587         if http_headers.has_key('content-type'):
2588             bozo_message = '%s is not an XML media type' % http_headers['content-type']
2589         else:
2590             bozo_message = 'no Content-type specified'
2591         result['bozo'] = 1
2592         result['bozo_exception'] = NonXMLContentType(bozo_message)
2593         
2594     result['version'], data = _stripDoctype(data)
2595
2596     baseuri = http_headers.get('content-location', result.get('href'))
2597     baselang = http_headers.get('content-language', None)
2598
2599     # if server sent 304, we're done
2600     if result.get('status', 0) == 304:
2601         result['version'] = ''
2602         result['debug_message'] = 'The feed has not changed since you last checked, ' + \
2603             'so the server sent no data.  This is a feature, not a bug!'
2604         return result
2605
2606     # if there was a problem downloading, we're done
2607     if not data:
2608         return result
2609
2610     # determine character encoding
2611     use_strict_parser = 0
2612     known_encoding = 0
2613     tried_encodings = []
2614     # try: HTTP encoding, declared XML encoding, encoding sniffed from BOM
2615     for proposed_encoding in (result['encoding'], xml_encoding, sniffed_xml_encoding):
2616         if not proposed_encoding: continue
2617         if proposed_encoding in tried_encodings: continue
2618         tried_encodings.append(proposed_encoding)
2619         try:
2620             data = _toUTF8(data, proposed_encoding)
2621             known_encoding = use_strict_parser = 1
2622             break
2623         except:
2624             pass
2625     # if no luck and we have auto-detection library, try that
2626     if (not known_encoding) and chardet:
2627         try:
2628             proposed_encoding = chardet.detect(data)['encoding']
2629             if proposed_encoding and (proposed_encoding not in tried_encodings):
2630                 tried_encodings.append(proposed_encoding)
2631                 data = _toUTF8(data, proposed_encoding)
2632                 known_encoding = use_strict_parser = 1
2633         except:
2634             pass
2635     # if still no luck and we haven't tried utf-8 yet, try that
2636     if (not known_encoding) and ('utf-8' not in tried_encodings):
2637         try:
2638             proposed_encoding = 'utf-8'
2639             tried_encodings.append(proposed_encoding)
2640             data = _toUTF8(data, proposed_encoding)
2641             known_encoding = use_strict_parser = 1
2642         except:
2643             pass
2644     # if still no luck and we haven't tried windows-1252 yet, try that
2645     if (not known_encoding) and ('windows-1252' not in tried_encodings):
2646         try:
2647             proposed_encoding = 'windows-1252'
2648             tried_encodings.append(proposed_encoding)
2649             data = _toUTF8(data, proposed_encoding)
2650             known_encoding = use_strict_parser = 1
2651         except:
2652             pass
2653     # if still no luck, give up
2654     if not known_encoding:
2655         result['bozo'] = 1
2656         result['bozo_exception'] = CharacterEncodingUnknown( \
2657             'document encoding unknown, I tried ' + \
2658             '%s, %s, utf-8, and windows-1252 but nothing worked' % \
2659             (result['encoding'], xml_encoding))
2660         result['encoding'] = ''
2661     elif proposed_encoding != result['encoding']:
2662         result['bozo'] = 1
2663         result['bozo_exception'] = CharacterEncodingOverride( \
2664             'documented declared as %s, but parsed as %s' % \
2665             (result['encoding'], proposed_encoding))
2666         result['encoding'] = proposed_encoding
2667
2668     if not _XML_AVAILABLE:
2669         use_strict_parser = 0
2670     if use_strict_parser:
2671         # initialize the SAX parser
2672         feedparser = _StrictFeedParser(baseuri, baselang, 'utf-8')
2673         saxparser = xml.sax.make_parser(PREFERRED_XML_PARSERS)
2674         saxparser.setFeature(xml.sax.handler.feature_namespaces, 1)
2675         saxparser.setContentHandler(feedparser)
2676         saxparser.setErrorHandler(feedparser)
2677         source = xml.sax.xmlreader.InputSource()
2678         source.setByteStream(_StringIO(data))
2679         if hasattr(saxparser, '_ns_stack'):
2680             # work around bug in built-in SAX parser (doesn't recognize xml: namespace)
2681             # PyXML doesn't have this problem, and it doesn't have _ns_stack either
2682             saxparser._ns_stack.append({'http://www.w3.org/XML/1998/namespace':'xml'})
2683         try:
2684             saxparser.parse(source)
2685         except Exception, e:
2686             if _debug:
2687                 import traceback
2688                 traceback.print_stack()
2689                 traceback.print_exc()
2690                 sys.stderr.write('xml parsing failed\n')
2691             result['bozo'] = 1
2692             result['bozo_exception'] = feedparser.exc or e
2693             use_strict_parser = 0
2694     if not use_strict_parser:
2695         feedparser = _LooseFeedParser(baseuri, baselang, known_encoding and 'utf-8' or '')
2696         feedparser.feed(data)
2697     result['feed'] = feedparser.feeddata
2698     result['entries'] = feedparser.entries
2699     result['version'] = result['version'] or feedparser.version
2700     result['namespaces'] = feedparser.namespacesInUse
2701     return result
2702
2703 if __name__ == '__main__':
2704     if not sys.argv[1:]:
2705         print __doc__
2706         sys.exit(0)
2707     else:
2708         urls = sys.argv[1:]
2709     zopeCompatibilityHack()
2710     from pprint import pprint
2711     for url in urls:
2712         print url
2713         print
2714         result = parse(url)
2715         pprint(result)
2716         print
2717
2718 #REVISION HISTORY
2719 #1.0 - 9/27/2002 - MAP - fixed namespace processing on prefixed RSS 2.0 elements,
2720 #  added Simon Fell's test suite
2721 #1.1 - 9/29/2002 - MAP - fixed infinite loop on incomplete CDATA sections
2722 #2.0 - 10/19/2002
2723 #  JD - use inchannel to watch out for image and textinput elements which can
2724 #  also contain title, link, and description elements
2725 #  JD - check for isPermaLink='false' attribute on guid elements
2726 #  JD - replaced openAnything with open_resource supporting ETag and
2727 #  If-Modified-Since request headers
2728 #  JD - parse now accepts etag, modified, agent, and referrer optional
2729 #  arguments
2730 #  JD - modified parse to return a dictionary instead of a tuple so that any
2731 #  etag or modified information can be returned and cached by the caller
2732 #2.0.1 - 10/21/2002 - MAP - changed parse() so that if we don't get anything
2733 #  because of etag/modified, return the old etag/modified to the caller to
2734 #  indicate why nothing is being returned
2735 #2.0.2 - 10/21/2002 - JB - added the inchannel to the if statement, otherwise its
2736 #  useless.  Fixes the problem JD was addressing by adding it.
2737 #2.1 - 11/14/2002 - MAP - added gzip support
2738 #2.2 - 1/27/2003 - MAP - added attribute support, admin:generatorAgent.
2739 #  start_admingeneratoragent is an example of how to handle elements with
2740 #  only attributes, no content.
2741 #2.3 - 6/11/2003 - MAP - added USER_AGENT for default (if caller doesn't specify);
2742 #  also, make sure we send the User-Agent even if urllib2 isn't available.
2743 #  Match any variation of backend.userland.com/rss namespace.
2744 #2.3.1 - 6/12/2003 - MAP - if item has both link and guid, return both as-is.
2745 #2.4 - 7/9/2003 - MAP - added preliminary Pie/Atom/Echo support based on Sam Ruby's
2746 #  snapshot of July 1 <http://www.intertwingly.net/blog/1506.html>; changed
2747 #  project name
2748 #2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
2749 #  removed unnecessary urllib code -- urllib2 should always be available anyway;
2750 #  return actual url, status, and full HTTP headers (as result['url'],
2751 #  result['status'], and result['headers']) if parsing a remote feed over HTTP --
2752 #  this should pass all the HTTP tests at <http://diveintomark.org/tests/client/http/>;
2753 #  added the latest namespace-of-the-week for RSS 2.0
2754 #2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
2755 #  User-Agent (otherwise urllib2 sends two, which confuses some servers)
2756 #2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
2757 #  inline <xhtml:body> and <xhtml:div> as used in some RSS 2.0 feeds
2758 #2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
2759 #  textInput, and also to return the character encoding (if specified)
2760 #2.6 - 1/1/2004 - MAP - dc:author support (MarekK); fixed bug tracking
2761 #  nested divs within content (JohnD); fixed missing sys import (JohanS);
2762 #  fixed regular expression to capture XML character encoding (Andrei);
2763 #  added support for Atom 0.3-style links; fixed bug with textInput tracking;
2764 #  added support for cloud (MartijnP); added support for multiple
2765 #  category/dc:subject (MartijnP); normalize content model: 'description' gets
2766 #  description (which can come from description, summary, or full content if no
2767 #  description), 'content' gets dict of base/language/type/value (which can come
2768 #  from content:encoded, xhtml:body, content, or fullitem);
2769 #  fixed bug matching arbitrary Userland namespaces; added xml:base and xml:lang
2770 #  tracking; fixed bug tracking unknown tags; fixed bug tracking content when
2771 #  <content> element is not in default namespace (like Pocketsoap feed);
2772 #  resolve relative URLs in link, guid, docs, url, comments, wfw:comment,
2773 #  wfw:commentRSS; resolve relative URLs within embedded HTML markup in
2774 #  description, xhtml:body, content, content:encoded, title, subtitle,
2775 #  summary, info, tagline, and copyright; added support for pingback and
2776 #  trackback namespaces
2777 #2.7 - 1/5/2004 - MAP - really added support for trackback and pingback
2778 #  namespaces, as opposed to 2.6 when I said I did but didn't really;
2779 #  sanitize HTML markup within some elements; added mxTidy support (if
2780 #  installed) to tidy HTML markup within some elements; fixed indentation
2781 #  bug in _parse_date (FazalM); use socket.setdefaulttimeout if available
2782 #  (FazalM); universal date parsing and normalization (FazalM): 'created', modified',
2783 #  'issued' are parsed into 9-tuple date format and stored in 'created_parsed',
2784 #  'modified_parsed', and 'issued_parsed'; 'date' is duplicated in 'modified'
2785 #  and vice-versa; 'date_parsed' is duplicated in 'modified_parsed' and vice-versa
2786 #2.7.1 - 1/9/2004 - MAP - fixed bug handling &quot; and &apos;.  fixed memory
2787 #  leak not closing url opener (JohnD); added dc:publisher support (MarekK);
2788 #  added admin:errorReportsTo support (MarekK); Python 2.1 dict support (MarekK)
2789 #2.7.4 - 1/14/2004 - MAP - added workaround for improperly formed <br/> tags in
2790 #  encoded HTML (skadz); fixed unicode handling in normalize_attrs (ChrisL);
2791 #  fixed relative URI processing for guid (skadz); added ICBM support; added
2792 #  base64 support
2793 #2.7.5 - 1/15/2004 - MAP - added workaround for malformed DOCTYPE (seen on many
2794 #  blogspot.com sites); added _debug variable
2795 #2.7.6 - 1/16/2004 - MAP - fixed bug with StringIO importing
2796 #3.0b3 - 1/23/2004 - MAP - parse entire feed with real XML parser (if available);
2797 #  added several new supported namespaces; fixed bug tracking naked markup in
2798 #  description; added support for enclosure; added support for source; re-added
2799 #  support for cloud which got dropped somehow; added support for expirationDate
2800 #3.0b4 - 1/26/2004 - MAP - fixed xml:lang inheritance; fixed multiple bugs tracking
2801 #  xml:base URI, one for documents that don't define one explicitly and one for
2802 #  documents that define an outer and an inner xml:base that goes out of scope
2803 #  before the end of the document
2804 #3.0b5 - 1/26/2004 - MAP - fixed bug parsing multiple links at feed level
2805 #3.0b6 - 1/27/2004 - MAP - added feed type and version detection, result['version']
2806 #  will be one of SUPPORTED_VERSIONS.keys() or empty string if unrecognized;
2807 #  added support for creativeCommons:license and cc:license; added support for
2808 #  full Atom content model in title, tagline, info, copyright, summary; fixed bug
2809 #  with gzip encoding (not always telling server we support it when we do)
2810 #3.0b7 - 1/28/2004 - MAP - support Atom-style author element in author_detail
2811 #  (dictionary of 'name', 'url', 'email'); map author to author_detail if author
2812 #  contains name + email address
2813 #3.0b8 - 1/28/2004 - MAP - added support for contributor
2814 #3.0b9 - 1/29/2004 - MAP - fixed check for presence of dict function; added
2815 #  support for summary
2816 #3.0b10 - 1/31/2004 - MAP - incorporated ISO-8601 date parsing routines from
2817 #  xml.util.iso8601
2818 #3.0b11 - 2/2/2004 - MAP - added 'rights' to list of elements that can contain
2819 #  dangerous markup; fiddled with decodeEntities (not right); liberalized
2820 #  date parsing even further
2821 #3.0b12 - 2/6/2004 - MAP - fiddled with decodeEntities (still not right);
2822 #  added support to Atom 0.2 subtitle; added support for Atom content model
2823 #  in copyright; better sanitizing of dangerous HTML elements with end tags
2824 #  (script, frameset)
2825 #3.0b13 - 2/8/2004 - MAP - better handling of empty HTML tags (br, hr, img,
2826 #  etc.) in embedded markup, in either HTML or XHTML form (<br>, <br/>, <br />)
2827 #3.0b14 - 2/8/2004 - MAP - fixed CDATA handling in non-wellformed feeds under
2828 #  Python 2.1
2829 #3.0b15 - 2/11/2004 - MAP - fixed bug resolving relative links in wfw:commentRSS;
2830 #  fixed bug capturing author and contributor URL; fixed bug resolving relative
2831 #  links in author and contributor URL; fixed bug resolvin relative links in
2832 #  generator URL; added support for recognizing RSS 1.0; passed Simon Fell's
2833 #  namespace tests, and included them permanently in the test suite with his
2834 #  permission; fixed namespace handling under Python 2.1
2835 #3.0b16 - 2/12/2004 - MAP - fixed support for RSS 0.90 (broken in b15)
2836 #3.0b17 - 2/13/2004 - MAP - determine character encoding as per RFC 3023
2837 #3.0b18 - 2/17/2004 - MAP - always map description to summary_detail (Andrei);
2838 #  use libxml2 (if available)
2839 #3.0b19 - 3/15/2004 - MAP - fixed bug exploding author information when author
2840 #  name was in parentheses; removed ultra-problematic mxTidy support; patch to
2841 #  workaround crash in PyXML/expat when encountering invalid entities
2842 #  (MarkMoraes); support for textinput/textInput
2843 #3.0b20 - 4/7/2004 - MAP - added CDF support
2844 #3.0b21 - 4/14/2004 - MAP - added Hot RSS support
2845 #3.0b22 - 4/19/2004 - MAP - changed 'channel' to 'feed', 'item' to 'entries' in
2846 #  results dict; changed results dict to allow getting values with results.key
2847 #  as well as results[key]; work around embedded illformed HTML with half
2848 #  a DOCTYPE; work around malformed Content-Type header; if character encoding
2849 #  is wrong, try several common ones before falling back to regexes (if this
2850 #  works, bozo_exception is set to CharacterEncodingOverride); fixed character
2851 #  encoding issues in BaseHTMLProcessor by tracking encoding and converting
2852 #  from Unicode to raw strings before feeding data to sgmllib.SGMLParser;
2853 #  convert each value in results to Unicode (if possible), even if using
2854 #  regex-based parsing
2855 #3.0b23 - 4/21/2004 - MAP - fixed UnicodeDecodeError for feeds that contain
2856 #  high-bit characters in attributes in embedded HTML in description (thanks
2857 #  Thijs van de Vossen); moved guid, date, and date_parsed to mapped keys in
2858 #  FeedParserDict; tweaked FeedParserDict.has_key to return True if asking
2859 #  about a mapped key
2860 #3.0fc1 - 4/23/2004 - MAP - made results.entries[0].links[0] and
2861 #  results.entries[0].enclosures[0] into FeedParserDict; fixed typo that could
2862 #  cause the same encoding to be tried twice (even if it failed the first time);
2863 #  fixed DOCTYPE stripping when DOCTYPE contained entity declarations;
2864 #  better textinput and image tracking in illformed RSS 1.0 feeds
2865 #3.0fc2 - 5/10/2004 - MAP - added and passed Sam's amp tests; added and passed
2866 #  my blink tag tests
2867 #3.0fc3 - 6/18/2004 - MAP - fixed bug in _changeEncodingDeclaration that
2868 #  failed to parse utf-16 encoded feeds; made source into a FeedParserDict;
2869 #  duplicate admin:generatorAgent/@rdf:resource in generator_detail.url;
2870 #  added support for image; refactored parse() fallback logic to try other
2871 #  encodings if SAX parsing fails (previously it would only try other encodings
2872 #  if re-encoding failed); remove unichr madness in normalize_attrs now that
2873 #  we're properly tracking encoding in and out of BaseHTMLProcessor; set
2874 #  feed.language from root-level xml:lang; set entry.id from rdf:about;
2875 #  send Accept header
2876 #3.0 - 6/21/2004 - MAP - don't try iso-8859-1 (can't distinguish between
2877 #  iso-8859-1 and windows-1252 anyway, and most incorrectly marked feeds are
2878 #  windows-1252); fixed regression that could cause the same encoding to be
2879 #  tried twice (even if it failed the first time)
2880 #3.0.1 - 6/22/2004 - MAP - default to us-ascii for all text/* content types;
2881 #  recover from malformed content-type header parameter with no equals sign
2882 #  ('text/xml; charset:iso-8859-1')
2883 #3.1 - 6/28/2004 - MAP - added and passed tests for converting HTML entities
2884 #  to Unicode equivalents in illformed feeds (aaronsw); added and
2885 #  passed tests for converting character entities to Unicode equivalents
2886 #  in illformed feeds (aaronsw); test for valid parsers when setting
2887 #  XML_AVAILABLE; make version and encoding available when server returns
2888 #  a 304; add handlers parameter to pass arbitrary urllib2 handlers (like
2889 #  digest auth or proxy support); add code to parse username/password
2890 #  out of url and send as basic authentication; expose downloading-related
2891 #  exceptions in bozo_exception (aaronsw); added __contains__ method to
2892 #  FeedParserDict (aaronsw); added publisher_detail (aaronsw)
2893 #3.2 - 7/3/2004 - MAP - use cjkcodecs and iconv_codec if available; always
2894 #  convert feed to UTF-8 before passing to XML parser; completely revamped
2895 #  logic for determining character encoding and attempting XML parsing
2896 #  (much faster); increased default timeout to 20 seconds; test for presence
2897 #  of Location header on redirects; added tests for many alternate character
2898 #  encodings; support various EBCDIC encodings; support UTF-16BE and
2899 #  UTF16-LE with or without a BOM; support UTF-8 with a BOM; support
2900 #  UTF-32BE and UTF-32LE with or without a BOM; fixed crashing bug if no
2901 #  XML parsers are available; added support for 'Content-encoding: deflate';
2902 #  send blank 'Accept-encoding: ' header if neither gzip nor zlib modules
2903 #  are available
2904 #3.3 - 7/15/2004 - MAP - optimize EBCDIC to ASCII conversion; fix obscure
2905 #  problem tracking xml:base and xml:lang if element declares it, child
2906 #  doesn't, first grandchild redeclares it, and second grandchild doesn't;
2907 #  refactored date parsing; defined public registerDateHandler so callers
2908 #  can add support for additional date formats at runtime; added support
2909 #  for OnBlog, Nate, MSSQL, Greek, and Hungarian dates (ytrewq1); added
2910 #  zopeCompatibilityHack() which turns FeedParserDict into a regular
2911 #  dictionary, required for Zope compatibility, and also makes command-
2912 #  line debugging easier because pprint module formats real dictionaries
2913 #  better than dictionary-like objects; added NonXMLContentType exception,
2914 #  which is stored in bozo_exception when a feed is served with a non-XML
2915 #  media type such as 'text/plain'; respect Content-Language as default
2916 #  language if not xml:lang is present; cloud dict is now FeedParserDict;
2917 #  generator dict is now FeedParserDict; better tracking of xml:lang,
2918 #  including support for xml:lang='' to unset the current language;
2919 #  recognize RSS 1.0 feeds even when RSS 1.0 namespace is not the default
2920 #  namespace; don't overwrite final status on redirects (scenarios:
2921 #  redirecting to a URL that returns 304, redirecting to a URL that
2922 #  redirects to another URL with a different type of redirect); add
2923 #  support for HTTP 303 redirects
2924 #4.0 - MAP - support for relative URIs in xml:base attribute; fixed
2925 #  encoding issue with mxTidy (phopkins); preliminary support for RFC 3229;
2926 #  support for Atom 1.0; support for iTunes extensions; new 'tags' for
2927 #  categories/keywords/etc. as array of dict
2928 #  {'term': term, 'scheme': scheme, 'label': label} to match Atom 1.0
2929 #  terminology; parse RFC 822-style dates with no time; lots of other
2930 #  bug fixes
2931 #4.1 - MAP - removed socket timeout; added support for chardet library