32
from gettext import gettext as _
32
33
from threading import Thread
35
from softwareproperties.shortcuts import ShortcutException
35
38
import urllib.request
36
from urllib.error import URLError
39
from urllib.error import HTTPError, URLError
37
40
import urllib.parse
38
41
from http.client import HTTPException
39
42
NEED_PYCURL = False
40
43
except ImportError:
46
HTTPError = pycurl.error
44
49
DEFAULT_KEYSERVER = "hkp://keyserver.ubuntu.com:80/"
45
50
# maintained until 2015
46
LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/~%s/+archive/%s'
51
LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/%s/+archive/%s'
52
LAUNCHPAD_USER_API = 'https://launchpad.net/api/1.0/%s'
53
LAUNCHPAD_USER_PPAS_API = 'https://launchpad.net/api/1.0/%s/ppas'
54
LAUNCHPAD_DISTRIBUTION_API = 'https://launchpad.net/api/1.0/%s'
47
55
# Specify to use the system default SSL store; change to a different path
48
56
# to test with custom certificates.
49
57
LAUNCHPAD_PPA_CERT = "/etc/ssl/certs/ca-certificates.crt"
71
79
return re.sub("[^a-zA-Z0-9_-]", "_", s)
74
def expand_ppa_line(abrev, distro_codename):
75
""" Convert an abbreviated ppa name of the form 'ppa:$name' to a
76
proper sources.list line of the form 'deb ...' """
77
# leave non-ppa: lines unchanged
78
if not abrev.startswith("ppa:"):
80
# FIXME: add support for dependency PPAs too (once we can get them
81
# via some sort of API, see LP #385129)
82
abrev = abrev.split(":")[1]
83
ppa_owner = abrev.split("/")[0]
85
ppa_name = abrev.split("/")[1]
86
except IndexError as e:
88
sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
89
line = "deb http://ppa.launchpad.net/%s/%s/ubuntu %s main" % (
90
ppa_owner, ppa_name, distro_codename)
91
filename = os.path.join(sourceslistd, "%s-%s-%s.list" % (
92
encode(ppa_owner), encode(ppa_name), distro_codename))
93
return (line, filename)
96
def get_ppa_info_from_lp(owner_name, ppa_name):
97
lp_url = LAUNCHPAD_PPA_API % (owner_name, ppa_name)
81
def get_info_from_lp(lp_url):
99
83
# python2 has no cert verification so we need pycurl
100
84
return _get_https_content_pycurl(lp_url)
133
126
raise PPAException("Error reading %s: %s" % (lp_url, e), e)
134
127
return json.loads(json_data)
130
def mangle_ppa_shortcut(shortcut):
131
ppa_shortcut = shortcut.split(":")[1]
132
user = ppa_shortcut.split("/")[0]
135
ppa_path_objs = ppa_shortcut.split("/")[1:]
137
if (len(ppa_path_objs) < 1):
138
ppa_path = ['ubuntu', 'ppa']
139
elif (len(ppa_path_objs) == 1):
140
ppa_path.insert(0, "ubuntu")
141
ppa_path.extend(ppa_path_objs)
143
ppa_path = ppa_path_objs
144
ppa = "~%s/%s" % (user, "/".join(ppa_path))
136
147
def verify_keyid_is_v4(signing_key_fingerprint):
137
148
"""Verify that the keyid is a v4 fingerprint with at least 160bit"""
138
149
return len(signing_key_fingerprint) >= 160/8
141
class AddPPASigningKeyThread(Thread):
152
class AddPPASigningKey(object):
142
153
" thread class for adding the signing key in the background "
144
155
GPG_DEFAULT_OPTIONS = ["gpg", "--no-default-keyring", "--no-options"]
146
157
def __init__(self, ppa_path, keyserver=None):
147
Thread.__init__(self)
148
158
self.ppa_path = ppa_path
149
159
self.keyserver = (keyserver if keyserver is not None
150
160
else DEFAULT_KEYSERVER)
153
self.add_ppa_signing_key(self.ppa_path)
155
162
def _recv_key(self, keyring, secret_keyring, signing_key_fingerprint, keyring_dir):
157
164
# double check that the signing key is a v4 fingerprint (160bit)
261
271
return (res == 0)
274
class AddPPASigningKeyThread(Thread, AddPPASigningKey):
275
# This class is legacy. There are no users inside the software-properties
276
# codebase other than a test case. It was left in case there were outside
277
# users. Internally, we've changed from having a class implement the
278
# tread to explicitly launching a thread and invoking a method in it
279
# see check_and_add_key_for_whitelisted_shortcut for how.
280
def __init__(self, ppa_path, keyserver=None):
281
Thread.__init__(self)
282
AddPPASigningKey.__init__(self, ppa_path=ppa_path, keyserver=keyserver)
285
self.add_ppa_signing_key(self.ppa_path)
288
def _get_suggested_ppa_message(user, ppa_name):
292
lp_user = get_info_from_lp(LAUNCHPAD_USER_API % user)
293
lp_ppas = get_info_from_lp(LAUNCHPAD_USER_PPAS_API % user)
294
entity_name = _("team") if lp_user["is_team"] else _("user")
295
if lp_ppas["total_size"] > 0:
296
# Translators: %(entity)s is either "team" or "user"
297
msg.append(_("The %(entity)s named '%(user)s' has no PPA named '%(ppa)s'") % {
298
'entity' : entity_name,
301
msg.append(_("Please choose from the following available PPAs:"))
302
for ppa in lp_ppas["entries"]:
303
msg.append(_(" * '%(name)s': %(displayname)s") % {
304
'name' : ppa["name"],
305
'displayname' : ppa["displayname"]})
307
# Translators: %(entity)s is either "team" or "user"
308
msg.append(_("The %(entity)s named '%(user)s' does not have any PPA") % {
309
'entity' : entity_name, 'user' : user})
310
return '\n'.join(msg)
314
return _("Please check that the PPA name or format is correct.")
317
def get_ppa_info(shortcut):
318
user = shortcut.split("/")[0]
319
ppa = "/".join(shortcut.split("/")[1:])
321
ret = get_ppa_info_from_lp(user, ppa)
322
ret["distribution"] = ret["distribution_link"].split('/')[-1]
323
ret["owner"] = ret["owner_link"].split('/')[-1]
325
except (HTTPError, Exception):
327
msg.append(_("Cannot add PPA: 'ppa:%s/%s'.") % (
330
# If the PPA does not exist, then try to find if the user/team
331
# exists. If it exists, list down the PPAs
332
raise ShortcutException('\n'.join(msg) + "\n" +
333
_get_suggested_ppa_message(user, ppa))
335
except (ValueError, PPAException):
336
raise ShortcutException(
337
_("Cannot access PPA (%s) to get PPA information, "
338
"please check your internet connection.") % \
339
(LAUNCHPAD_PPA_API % (user, ppa)))
342
class PPAShortcutHandler(object):
343
def __init__(self, shortcut):
344
super(PPAShortcutHandler, self).__init__()
345
self.shortcut = mangle_ppa_shortcut(shortcut)
346
info = get_ppa_info(self.shortcut)
348
if "private" in info and info["private"]:
349
raise ShortcutException(
350
_("Adding private PPAs is not supported currently"))
357
def expand(self, codename, distro=None):
358
if distro is not None and distro != self._info["distribution"]:
359
# The requested PPA is for a foreign distribution. Guess that
360
# the user wants that distribution's current series.
361
codename = get_current_series_from_lp(self._info["distribution"])
362
debline = "deb http://ppa.launchpad.net/%s/%s/%s %s main" % (
363
self._info["owner"][1:], self._info["name"],
364
self._info["distribution"], codename)
365
sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
366
filename = os.path.join(sourceslistd, "%s-%s-%s-%s.list" % (
367
encode(self._info["owner"][1:]), encode(self._info["distribution"]),
368
encode(self._info["name"]), codename))
369
return (debline, filename)
371
def should_confirm(self):
374
def add_key(self, keyserver=None):
375
apsk = AddPPASigningKey(self._info["reference"], keyserver=keyserver)
376
return apsk.add_ppa_signing_key()
379
def shortcut_handler(shortcut):
380
if not shortcut.startswith("ppa:"):
382
return PPAShortcutHandler(shortcut)
264
385
if __name__ == "__main__":
266
owner_name, ppa_name = sys.argv[1].split(":")[1].split("/")
267
print(get_ppa_info_from_lp(owner_name, ppa_name))
387
ppa = sys.argv[1].split(":")[1]
388
print(get_ppa_info(ppa))