~guillaume86/exaile/context-dev

« back to all changes in this revision

Viewing changes to plugins/podcasts/_feedparser.py

  • Committer: guillaume86
  • Date: 2009-07-25 14:58:51 UTC
  • mfrom: (2141.1.50 exaile-0.3.0)
  • Revision ID: guillaume86-20090725145851-5mtd72f9ze9u1ma6
Added a little hack to adjust zoom level for the new webkit version (and yeah it's working with the ppa version now!)

Show diffs side-by-side

added added

removed removed

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