summaryrefslogtreecommitdiff
path: root/repoze/bfg/url.py
blob: dcaea67ff92a562f38c58a28b1b94a877b500587 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
""" Utility functions for dealing with URLs in repoze.bfg """

import urllib

from zope.component import queryMultiAdapter
from zope.component import getUtility
from repoze.bfg.interfaces import IContextURL
from repoze.bfg.interfaces import IRoutesMapper

from repoze.bfg.traversal import TraversalContextURL
from repoze.bfg.traversal import quote_path_segment

def route_url(route_name, request, *elements, **kw):
    """Generates a fully qualified URL for a named BFG route.
    
    Use the route's ``name`` as the first positional argument.  Use a
    request object as the second positional argument.  Additional
    positional arguments are appended to the URL as path segments
    after it is generated.
    
    Use keyword arguments to supply values which match any dynamic
    path elements in the route definition.  Raises a KeyError
    exception if the URL cannot be generated for any reason (not
    enough arguments, for example).

    For example, if you've defined a route named "foobar" with the path
    ``:foo/:bar/*traverse``::

        route_url('foobar', request, foo='1')          => <KeyError exception>
        route_url('foobar', request, foo='1', bar='2') => <KeyError exception>
        route_url('foobar', request, foo='1', bar='2',
                   'traverse=('a','b')                 =>  http://e.com/1/2/a/b
        route_url('foobar', request, foo='1', bar='2',
                   'traverse=('/a/b')                 =>  http://e.com/1/2/a/b

    Values replacing ``:segment`` arguments can be passed as strings
    or Unicode objects.  They will be encoded to UTF-8 and URL-quoted
    before being placed into the generated URL.

    Values replacing ``*remainder`` arguments can be passed as strings
    *or* tuples of Unicode/string values.  If a tuple is passed as a
    ``*remainder`` replacement value, its values are URL-quoted and
    encoded to UTF-8.  The resulting strings are joined with slashes
    and rendered into the URL.  If a string is passed as a
    ``*remainder`` replacement value, it is tacked on to the URL
    untouched.

    If a keyword argument ``_query`` is present, it will used to
    compose a query string that will be tacked on to the end of the
    URL.  The value of ``query`` must be a sequence of two-tuples *or*
    a data structure with an ``.items()`` method that returns a
    sequence of two-tuples (presumably a dictionary).  This data
    structure will be turned into a query string per the documentation
    of ``repoze.url.urlencode`` function.  After the query data is
    turned into a query string, a leading ``?`` is prepended, and the
    the resulting string is appended to the generated URL.

    .. note:: Python data structures that are passed as ``_query``
              which are sequences or dictionaries are turned into a
              string under the same rules as when run through
              urllib.urlencode with the ``doseq`` argument equal to
              ``True``.  This means that sequences can be passed as
              values, and a k=v pair will be placed into the query
              string for each value.

    If a keyword argument ``_anchor`` is present, its string
    representation will be used as a named anchor in the generated URL
    (e.g. if ``anchor`` is passed as ``foo`` and the model URL is
    ``http://example.com/model/url``, the resulting generated URL will
    be ``http://example.com/model/url#foo``).

    .. note:: If ``_anchor`` is passed as a string, it should be UTF-8
              encoded. If ``anchor`` is passed as a Unicode object, it
              will be converted to UTF-8 before being appended to the
              URL.  The anchor value is not quoted in any way before
              being appended to the generated URL.

    If both ``anchor`` and ``query`` are specified, the anchor element
    will always follow the query element,
    e.g. ``http://example.com?foo=1#bar``.

    This function raises a ``KeyError`` if the URL cannot be generated
    due to missing replacement names.  Extra replacement names are
    ignored.
    """
    mapper = getUtility(IRoutesMapper)
    path = mapper.generate(route_name, kw) # raises KeyError if generate fails

    anchor = ''
    qs = ''

    if '_query' in kw:
        qs = '?' + urlencode(kw['_query'], doseq=True)

    if '_anchor' in kw:
        anchor = kw['_anchor']
        if isinstance(anchor, unicode):
            anchor = anchor.encode('utf-8')
        anchor = '#' + anchor

    if elements:
        suffix = '/'.join([quote_path_segment(s) for s in elements])
        if not path.endswith('/'):
            suffix = '/' + suffix
    else:
        suffix = ''

    return request.application_url + path + suffix + qs + anchor

def model_url(model, request, *elements, **kw):
    """
    Generate a string representing the absolute URL of the model (or
    context) object based on the ``wsgi.url_scheme``, ``HTTP_HOST`` or
    ``SERVER_NAME`` in the request, plus any ``SCRIPT_NAME``.  If a
    'virtual root path' is present in the request environment (the
    value of the WSGI environ key ``HTTP_X_VHM_ROOT``), and the
    ``model`` was obtained via traversal, the URL path will not
    include the virtual root prefix (it will be stripped out of the
    generated URL).  If a ``query`` keyword argument is provided, a
    query string based on its value will be composed and appended to
    the generated URL string (see details below).  The overall result
    of this function is always a UTF-8 encoded string (never unicode).

    .. note:: If the ``model`` used is the result of a traversal, it
       must be :term:`location`-aware.  The 'model' can also be the
       context of a URL dispatch; contexts found this way do not need
       to be location-aware.

    Any positional arguments passed in as ``elements`` must be strings
    or unicode objects.  These will be joined by slashes and appended
    to the generated model URL.  Each of the elements passed in is
    URL-quoted before being appended; if any element is unicode, it
    will converted to a UTF-8 bytestring before being URL-quoted.

    .. warning:: if no ``elements`` arguments are specified, the model
                 URL will end with a trailing slash.  If any
                 ``elements`` are used, the generated URL will *not*
                 end in trailing a slash.

    If a keyword argument ``query`` is present, it will used to
    compose a query string that will be tacked on to the end of the
    URL.  The value of ``query`` must be a sequence of two-tuples *or*
    a data structure with an ``.items()`` method that returns a
    sequence of two-tuples (presumably a dictionary).  This data
    structure will be turned into a query string per the documentation
    of ``repoze.url.urlencode`` function.  After the query data is
    turned into a query string, a leading ``?`` is prepended, and the
    the resulting string is appended to the generated URL.

    .. note:: Python data structures that are passed as ``query``
              which are sequences or dictionaries are turned into a
              string under the same rules as when run through
              urllib.urlencode with the ``doseq`` argument equal to
              ``True``.  This means that sequences can be passed as
              values, and a k=v pair will be placed into the query
              string for each value.

    If a keyword argument ``anchor`` is present, its string
    representation will be used as a named anchor in the generated URL
    (e.g. if ``anchor`` is passed as ``foo`` and the model URL is
    ``http://example.com/model/url``, the resulting generated URL will
    be ``http://example.com/model/url#foo``).

    .. note:: If ``anchor`` is passed as a string, it should be UTF-8
              encoded. If ``anchor`` is passed as a Unicode object, it
              will be converted to UTF-8 before being appended to the
              URL.  The anchor value is not quoted in any way before
              being appended to the generated URL.

    If both ``anchor`` and ``query`` are specified, the anchor element
    will always follow the query element,
    e.g. ``http://example.com?foo=1#bar``.
    """
    
    context_url = queryMultiAdapter((model, request), IContextURL)
    if context_url is None:
        # b/w compat for unit tests
        context_url = TraversalContextURL(model, request)
    model_url = context_url()

    qs = ''
    anchor = ''

    if 'query' in kw:
        qs = '?' + urlencode(kw['query'], doseq=True)

    if 'anchor' in kw:
        anchor = kw['anchor']
        if isinstance(anchor, unicode):
            anchor = anchor.encode('utf-8')
        anchor = '#' + anchor

    if elements:
        suffix = '/'.join([quote_path_segment(s) for s in elements])
    else:
        suffix = ''

    return model_url + suffix + qs + anchor

def urlencode(query, doseq=False):
    """
    A wrapper around Python's stdlib `urllib.urlencode function
    <http://docs.python.org/library/urllib.html>`_ which accepts
    unicode keys and values within the ``query`` dict/sequence; all
    Unicode keys and values are first converted to UTF-8 before being
    used to compose the query string.  The behavior of the function is
    otherwise the same as the stdlib version.

    The value of ``query`` must be a sequence of two-tuples
    representing key/value pairs *or* an object (often a dictionary)
    with an ``.items()`` method that returns a sequence of two-tuples
    representing key/value pairs.  ``doseq`` controls what happens
    when a sequence is presented as one of the values.  See the Python
    stdlib documentation for ``urllib.urlencode`` for more
    information.
    """
    if hasattr(query, 'items'):
        # presumed to be a dictionary
        query = query.items()

    newquery = []
    for k, v in query:

        if k.__class__ is unicode:
            k = k.encode('utf-8')

        try:
            v.__iter__
        except AttributeError:
            if v.__class__ is unicode:
                v = v.encode('utf-8')
        else:
            L = []
            for x in v:
                if x.__class__ is unicode:
                    x = x.encode('utf-8')
                L.append(x)
            v = L

        newquery.append((k, v))

    return urllib.urlencode(newquery, doseq=doseq)