1 # Copyright 2010-2023 Kurt McKee <contactme@kurtmckee.org> 2 # Copyright 2002-2008 Mark Pilgrim 3 # All rights reserved. 4 # 5 # This file is a part of feedparser. 6 # 7 # Redistribution and use in source and binary forms, with or without 8 # modification, are permitted provided that the following conditions are met: 9 # 10 # * Redistributions of source code must retain the above copyright notice, 11 # this list of conditions and the following disclaimer. 12 # * Redistributions in binary form must reproduce the above copyright notice, 13 # this list of conditions and the following disclaimer in the documentation 14 # and/or other materials provided with the distribution. 15 # 16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' 17 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 # POSSIBILITY OF SUCH DAMAGE. 27 28 import datetime 29 30 timezone_names = { 31 'ut': 0, 'gmt': 0, 'z': 0, 32 'adt': -3, 'ast': -4, 'at': -4, 33 'edt': -4, 'est': -5, 'et': -5, 34 'cdt': -5, 'cst': -6, 'ct': -6, 35 'mdt': -6, 'mst': -7, 'mt': -7, 36 'pdt': -7, 'pst': -8, 'pt': -8, 37 'a': -1, 'n': 1, 38 'm': -12, 'y': 12, 39 'met': 1, 'mest': 2, 40 } 41 day_names = {'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'} 42 months = { 43 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 44 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12, 45 } 46 47 48 def _parse_date_rfc822(date): 49 """Parse RFC 822 dates and times 50 http://tools.ietf.org/html/rfc822#section-5 51 52 There are some formatting differences that are accounted for: 53 1. Years may be two or four digits. 54 2. The month and day can be swapped. 55 3. Additional timezone names are supported. 56 4. A default time and timezone are assumed if only a date is present. 57 58 :param str date: a date/time string that will be converted to a time tuple 59 :returns: a UTC time tuple, or None 60 :rtype: time.struct_time | None 61 """ 62 63 parts = date.lower().split() 64 if len(parts) < 5: 65 # Assume that the time and timezone are missing 66 parts.extend(('00:00:00', '0000')) 67 # Remove the day name 68 if parts[0][:3] in day_names: 69 parts = parts[1:] 70 if len(parts) < 5: 71 # If there are still fewer than five parts, there's not enough 72 # information to interpret this. 73 return None 74 75 # Handle the day and month name. 76 month = months.get(parts[1][:3]) 77 try: 78 day = int(parts[0]) 79 except ValueError: 80 # Check if the day and month are swapped. 81 if months.get(parts[0][:3]): 82 try: 83 day = int(parts[1]) 84 except ValueError: 85 return None 86 month = months.get(parts[0][:3]) 87 else: 88 return None 89 if not month: 90 return None 91 92 # Handle the year. 93 try: 94 year = int(parts[2]) 95 except ValueError: 96 return None 97 # Normalize two-digit years: 98 # Anything in the 90's is interpreted as 1990 and on. 99 # Anything 89 or less is interpreted as 2089 or before. 100 if len(parts[2]) <= 2: 101 year += (1900, 2000)[year < 90] 102 103 # Handle the time (default to 00:00:00). 104 time_parts = parts[3].split(':') 105 time_parts.extend(('0',) * (3 - len(time_parts))) 106 try: 107 (hour, minute, second) = [int(i) for i in time_parts] 108 except ValueError: 109 return None 110 111 # Handle the timezone information, if any (default to +0000). 112 # Strip 'Etc/' from the timezone. 113 if parts[4].startswith('etc/'): 114 parts[4] = parts[4][4:] 115 # Normalize timezones that start with 'gmt': 116 # GMT-05:00 => -0500 117 # GMT => GMT 118 if parts[4].startswith('gmt'): 119 parts[4] = ''.join(parts[4][3:].split(':')) or 'gmt' 120 # Handle timezones like '-0500', '+0500', and 'EST' 121 if parts[4] and parts[4][0] in ('-', '+'): 122 try: 123 if ':' in parts[4]: 124 timezone_hours = int(parts[4][1:3]) 125 timezone_minutes = int(parts[4][4:]) 126 else: 127 timezone_hours = int(parts[4][1:3]) 128 timezone_minutes = int(parts[4][3:]) 129 except ValueError: 130 return None 131 if parts[4].startswith('-'): 132 timezone_hours *= -1 133 timezone_minutes *= -1 134 else: 135 timezone_hours = timezone_names.get(parts[4], 0) 136 timezone_minutes = 0 137 138 # Create the datetime object and timezone delta objects 139 try: 140 stamp = datetime.datetime(year, month, day, hour, minute, second) 141 except ValueError: 142 return None 143 delta = datetime.timedelta(0, 0, 0, 0, timezone_minutes, timezone_hours) 144 145 # Return the date and timestamp in a UTC 9-tuple 146 try: 147 return (stamp - delta).utctimetuple() 148 except (OverflowError, ValueError): 149 # IronPython throws ValueErrors instead of OverflowErrors 150 return None 151