3 from optparse
import OptionParser
4 import sys
, shutil
, tempfile
, urlparse
6 import urllib2
, os
, httplib
8 import logging
, time
, traceback
9 from logging
import info
11 from zeroinstall
import SafeException
12 from zeroinstall
.injector
import model
, gpg
, reader
, namespaces
14 from display
import checking
, result
, error
, highlight
, error_new_line
20 WEEK
= 60 * 60 * 24 * 7
23 if hasattr(address
, 'hostname'):
24 return address
.hostname
26 return address
[1].split(':', 1)[0]
29 if hasattr(address
, 'port'):
32 port
= address
[1].split(':', 1)[1:]
38 assert port(('http', 'foo:81')) == 81
39 assert port(urlparse
.urlparse('http://foo:81')) == 81
41 parser
= OptionParser(usage
="usage: %prog [options] feed.xml")
42 parser
.add_option("-d", "--dependencies", help="also check feeds for dependencies", action
='store_true')
43 parser
.add_option("-s", "--skip-archives", help="don't check the archives are OK", action
='store_true')
44 parser
.add_option("-v", "--verbose", help="more verbose output", action
='count')
45 parser
.add_option("-V", "--version", help="display version information", action
='store_true')
47 (options
, args
) = parser
.parse_args()
50 print "FeedLint (zero-install) " + version
51 print "Copyright (C) 2007 Thomas Leonard"
52 print "This program comes with ABSOLUTELY NO WARRANTY,"
53 print "to the extent permitted by law."
54 print "You may redistribute copies of this program"
55 print "under the terms of the GNU General Public License."
56 print "For more information about these matters, see the file named COPYING."
60 logger
= logging
.getLogger()
61 if options
.verbose
== 1:
62 logger
.setLevel(logging
.INFO
)
64 logger
.setLevel(logging
.DEBUG
)
73 to_check
= [model
.canonical_iface_uri(a
) for a
in args
]
74 except SafeException
, ex
:
75 if options
.verbose
: raise
76 print >>sys
.stderr
, ex
79 def check_key(feed
, fingerprint
):
80 for line
in os
.popen('gpg --with-colons --list-keys %s' % s
.fingerprint
):
81 if line
.startswith('pub:'):
82 key_id
= line
.split(':')[4]
85 raise SafeException('Failed to find key with fingerprint %s on your keyring' % fingerprint
)
87 key_url
= urlparse
.urljoin(feed
, '%s.gpg' % key_id
)
89 if key_url
in checked
:
90 info("(already checked key URL %s)", key_url
)
92 checking("Checking key %s" % key_url
)
93 urllib2
.urlopen(key_url
).read()
97 def get_http_size(url
, ttl
= 1):
98 assert url
.lower().startswith('http://')
100 address
= urlparse
.urlparse(url
)
101 http
= httplib
.HTTPConnection(host(address
), port(address
) or 80)
103 parts
= url
.split('/', 3)
109 http
.request('HEAD', '/' + path
, headers
= {'Host': host(address
)})
110 response
= http
.getresponse()
112 if response
.status
== 200:
113 return response
.getheader('Content-Length')
114 elif response
.status
in (301, 302):
115 new_url_rel
= response
.getheader('Location') or response
.getheader('URI')
116 new_url
= urlparse
.urljoin(url
, new_url_rel
)
118 raise SafeException("HTTP error: got status code %s" % response
.status
)
123 result("Moved", 'YELLOW')
124 checking("Checking new URL %s" % new_url
)
126 return get_http_size(new_url
, ttl
- 1)
128 raise SafeException('Too many redirections.')
130 def get_ftp_size(url
):
131 address
= urlparse
.urlparse(url
)
132 ftp
= ftplib
.FTP(host(address
))
135 ftp
.voidcmd('TYPE I')
136 return ftp
.size(url
.split('/', 3)[3])
141 scheme
= urlparse
.urlparse(url
)[0].lower()
142 if scheme
.startswith('http'):
143 return get_http_size(url
)
144 elif scheme
.startswith('ftp'):
145 return get_ftp_size(url
)
147 raise SafeException("Unknown scheme '%s' in '%s'" % (scheme
, url
))
149 def check_source(source
):
150 if hasattr(source
, 'url'):
151 checking("Checking archive %s" % source
.url
)
152 actual_size
= get_size(source
.url
)
153 if actual_size
is None:
154 result("No Content-Length for archive; can't check", 'YELLOW')
156 actual_size
= int(actual_size
)
157 expected_size
= source
.size
+ (source
.start_offset
or 0)
158 if actual_size
!= expected_size
:
160 raise SafeException("Expected archive to have a size of %d, but server says it is %d" %
161 (expected_size
, actual_size
))
163 elif hasattr(source
, 'steps'):
164 for step
in source
.steps
:
167 existing_urls
= set()
168 def check_exists(url
):
169 if url
in existing_urls
: return # Already checked
171 checking("Checking URL exists %s" % url
)
174 existing_urls
.add(url
)
179 feed
= to_check
.pop()
181 info("Already checked feed %s", feed
)
186 checking("Checking " + feed
, indent
= 0)
189 iface
= model
.Interface(feed
)
191 if feed
.startswith('/'):
192 reader
.update(iface
, feed
, local
= True)
195 tmp
= tempfile
.TemporaryFile(prefix
= 'feedlint-')
197 stream
= urllib2
.urlopen(feed
)
198 shutil
.copyfileobj(stream
, tmp
)
203 elif start
== '-----':
204 result('Old sig', colour
= 'YELLOW')
205 error_new_line(' Feed has an old-style plain GPG signature. Use 0publish --xmlsign.',
209 error_new_line(' Unknown format. File starts "%s"' % start
)
212 data
, sigs
= gpg
.check_stream(tmp
)
215 if isinstance(s
, gpg
.ValidSig
):
216 check_key(feed
, s
.fingerprint
)
218 raise SafeException("Can't check sig: %s" % s
)
220 feed_tmp
= tempfile
.NamedTemporaryFile(prefix
= 'feedlint-')
222 shutil
.copyfileobj(data
, feed_tmp
)
224 reader
.update(iface
, feed_tmp
.name
)
226 for f
in iface
.feeds
:
227 info("Will check feed %s", f
.uri
)
228 to_check
.append(f
.uri
)
234 for impl
in iface
.implementations
.values():
235 if hasattr(impl
, 'dependencies'):
236 for r
in impl
.dependencies
.values():
237 if r
.interface
not in checked
:
238 info("Will check dependency %s", r
)
239 if options
.dependencies
:
240 to_check
.append(r
.interface
)
242 check_exists(r
.interface
)
243 if hasattr(impl
, 'download_sources') and not options
.skip_archives
:
244 for source
in impl
.download_sources
:
246 stability
= impl
.upstream_stability
or model
.testing
247 if stability
== model
.testing
:
249 if not impl
.released
:
250 testing_error
= "No release data on testing version"
253 released
= time
.strptime(impl
.released
, '%Y-%m-%d')
254 except ValueError, ex
:
255 testing_error
= "Can't parse date"
257 ago
= now
- time
.mktime(released
)
259 testing_error
= 'Release data is in the future!'
261 print highlight(' Version %s has been Testing for more than a week' % impl
.get_version(),
264 raise SafeException("Version %s: %s (released %s)" % (impl
.get_version(), testing_error
, impl
.released
))
266 for homepage
in iface
.get_metadata(namespaces
.XMLNS_IFACE
, 'homepage'):
267 check_exists(homepage
.content
)
269 for icon
in iface
.get_metadata(namespaces
.XMLNS_IFACE
, 'icon'):
270 check_exists(icon
.getAttribute('href'))
272 except (urllib2
.HTTPError
, httplib
.BadStatusLine
, socket
.error
, ftplib
.error_perm
), ex
:
273 err_msg
= str(ex
).strip() or str(type(ex
))
274 error_new_line(' ' + err_msg
)
276 if options
.verbose
: traceback
.print_exc()
277 except SafeException
, ex
:
278 if options
.verbose
: raise
279 error_new_line(' ' + str(ex
))
285 print "\nERRORS FOUND:", n_errors