1# -*- test-case-name: openid.test.test_xrires -*-
2"""XRI resolution.
3"""
4
5from urllib.parse import urlencode
6from openid import fetchers
7from openid.yadis import etxrd
8from openid.yadis.xri import toURINormal
9from openid.yadis.services import iterServices
10
11DEFAULT_PROXY = 'http://proxy.xri.net/'
12
13
14class ProxyResolver(object):
15    """Python interface to a remote XRI proxy resolver.
16    """
17
18    def __init__(self, proxy_url=DEFAULT_PROXY):
19        self.proxy_url = proxy_url
20
21    def queryURL(self, xri, service_type=None):
22        """Build a URL to query the proxy resolver.
23
24        @param xri: An XRI to resolve.
25        @type xri: unicode
26
27        @param service_type: The service type to resolve, if you desire
28            service endpoint selection.  A service type is a URI.
29        @type service_type: str
30
31        @returns: a URL
32        @returntype: str
33        """
34        # Trim off the xri:// prefix.  The proxy resolver didn't accept it
35        # when this code was written, but that may (or may not) change for
36        # XRI Resolution 2.0 Working Draft 11.
37        qxri = toURINormal(xri)[6:]
38        hxri = self.proxy_url + qxri
39        args = {
40            # XXX: If the proxy resolver will ensure that it doesn't return
41            # bogus CanonicalIDs (as per Steve's message of 15 Aug 2006
42            # 11:13:42), then we could ask for application/xrd+xml instead,
43            # which would give us a bit less to process.
44            '_xrd_r': 'application/xrds+xml',
45        }
46        if service_type:
47            args['_xrd_t'] = service_type
48        else:
49            # Don't perform service endpoint selection.
50            args['_xrd_r'] += ';sep=false'
51        query = _appendArgs(hxri, args)
52        return query
53
54    def query(self, xri, service_types):
55        """Resolve some services for an XRI.
56
57        Note: I don't implement any service endpoint selection beyond what
58        the resolver I'm querying does, so the Services I return may well
59        include Services that were not of the types you asked for.
60
61        May raise fetchers.HTTPFetchingError or L{etxrd.XRDSError} if
62        the fetching or parsing don't go so well.
63
64        @param xri: An XRI to resolve.
65        @type xri: unicode
66
67        @param service_types: A list of services types to query for.  Service
68            types are URIs.
69        @type service_types: list of str
70
71        @returns: tuple of (CanonicalID, Service elements)
72        @returntype: (unicode, list of C{ElementTree.Element}s)
73        """
74        # FIXME: No test coverage!
75        services = []
76        # Make a seperate request to the proxy resolver for each service
77        # type, as, if it is following Refs, it could return a different
78        # XRDS for each.
79
80        canonicalID = None
81
82        for service_type in service_types:
83            url = self.queryURL(xri, service_type)
84            response = fetchers.fetch(url)
85            if response.status not in (200, 206):
86                # XXX: sucks to fail silently.
87                # print "response not OK:", response
88                continue
89            et = etxrd.parseXRDS(response.body)
90            canonicalID = etxrd.getCanonicalID(xri, et)
91            some_services = list(iterServices(et))
92            services.extend(some_services)
93        # TODO:
94        #  * If we do get hits for multiple service_types, we're almost
95        #    certainly going to have duplicated service entries and
96        #    broken priority ordering.
97        return canonicalID, services
98
99
100def _appendArgs(url, args):
101    """Append some arguments to an HTTP query.
102    """
103    # to be merged with oidutil.appendArgs when we combine the projects.
104    if hasattr(args, 'items'):
105        args = list(args.items())
106        args.sort()
107
108    if len(args) == 0:
109        return url
110
111    # According to XRI Resolution section "QXRI query parameters":
112    #
113    # """If the original QXRI had a null query component (only a leading
114    #    question mark), or a query component consisting of only question
115    #    marks, one additional leading question mark MUST be added when
116    #    adding any XRI resolution parameters."""
117
118    if '?' in url.rstrip('?'):
119        sep = '&'
120    else:
121        sep = '?'
122
123    return '%s%s%s' % (url, sep, urlencode(args))
124