Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / util.py @ 7c3549f0

History | View | Annotate | Download (11.1 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import datetime
36
import time
37
import urllib
38

    
39
from urlparse import urlparse
40
from datetime import tzinfo, timedelta
41

    
42
from django.http import HttpResponse, HttpResponseBadRequest, urlencode, \
43
                        HttpResponseRedirect
44
from django.template import RequestContext
45
from django.contrib.auth import authenticate
46
from django.core.urlresolvers import reverse
47
from django.shortcuts import redirect
48
from django.core.exceptions import ValidationError, ObjectDoesNotExist
49
from django.utils.translation import ugettext as _
50
from django.core.urlresolvers import reverse
51

    
52
from astakos.im.models import AstakosUser, Invitation
53
from astakos.im.functions import login
54
from astakos.im import settings
55

    
56
import astakos.im.messages as astakos_messages
57

    
58
logger = logging.getLogger(__name__)
59

    
60

    
61
class UTC(tzinfo):
62
    def utcoffset(self, dt):
63
        return timedelta(0)
64

    
65
    def tzname(self, dt):
66
        return 'UTC'
67

    
68
    def dst(self, dt):
69
        return timedelta(0)
70

    
71

    
72
def isoformat(d):
73
    """Return an ISO8601 date string that includes a timezone."""
74

    
75
    return d.replace(tzinfo=UTC()).isoformat()
76

    
77

    
78
def epoch(datetime):
79
    return int(time.mktime(datetime.timetuple()) * 1000)
80

    
81

    
82
def get_context(request, extra_context=None, **kwargs):
83
    extra_context = extra_context or {}
84
    extra_context.update(kwargs)
85
    return RequestContext(request, extra_context)
86

    
87

    
88
def get_invitation(request):
89
    """
90
    Returns the invitation identified by the ``code``.
91

92
    Raises ValueError if the invitation is consumed or there is another account
93
    associated with this email.
94
    """
95
    code = request.GET.get('code')
96
    if request.method == 'POST':
97
        code = request.POST.get('code')
98
    if not code:
99
        return
100
    invitation = Invitation.objects.get(code=code)
101
    if invitation.is_consumed:
102
        raise ValueError(_(astakos_messages.INVITATION_CONSUMED_ERR))
103
    if reserved_email(invitation.username):
104
        email = invitation.username
105
        raise ValueError(_(astakos_messages.EMAIL_RESERVED) % locals())
106
    return invitation
107

    
108

    
109
def restrict_next(url, domain=None, allowed_schemes=()):
110
    """
111
    Utility method to validate that provided url is safe to be used as the
112
    redirect location of an http redirect response. The method parses the
113
    provided url and identifies if it conforms CORS against provided domain
114
    AND url scheme matches any of the schemes in `allowed_schemes` parameter.
115
    If verirication succeeds sanitized safe url is returned so you must use
116
    the method's response in the response location header and not the
117
    originally provided url. If verification fails the method returns None.
118

119
    >>> print restrict_next('/im/feedback', '.okeanos.grnet.gr')
120
    /im/feedback
121
    >>> print restrict_next('pithos.okeanos.grnet.gr/im/feedback',
122
    ...                     '.okeanos.grnet.gr')
123
    //pithos.okeanos.grnet.gr/im/feedback
124
    >>> print restrict_next('https://pithos.okeanos.grnet.gr/im/feedback',
125
    ...                     '.okeanos.grnet.gr')
126
    https://pithos.okeanos.grnet.gr/im/feedback
127
    >>> print restrict_next('pithos://127.0.0.1', '.okeanos.grnet.gr')
128
    None
129
    >>> print restrict_next('pithos://127.0.0.1', '.okeanos.grnet.gr',
130
    ...                     allowed_schemes=('pithos'))
131
    None
132
    >>> print restrict_next('pithos://127.0.0.1', '127.0.0.1',
133
    ...                     allowed_schemes=('pithos'))
134
    pithos://127.0.0.1
135
    >>> print restrict_next('node1.example.com', '.okeanos.grnet.gr')
136
    None
137
    >>> print restrict_next('//node1.example.com', '.okeanos.grnet.gr')
138
    None
139
    >>> print restrict_next('https://node1.example.com', '.okeanos.grnet.gr')
140
    None
141
    >>> print restrict_next('https://node1.example.com')
142
    https://node1.example.com
143
    >>> print restrict_next('//node1.example.com')
144
    //node1.example.com
145
    >>> print restrict_next('node1.example.com')
146
    //node1.example.com
147
    >>> print restrict_next('node1.example.com', allowed_schemes=('pithos',))
148
    None
149
    >>> print restrict_next('pithos://localhost', 'localhost',
150
    ...                     allowed_schemes=('pithos',))
151
    pithos://localhost
152
    """
153
    if not url:
154
        return None
155

    
156
    parts = urlparse(url, scheme='http')
157
    if not parts.netloc and not parts.path.startswith('/'):
158
        # fix url if does not conforms RFC 1808
159
        url = '//%s' % url
160
        parts = urlparse(url, scheme='http')
161

    
162
    if not domain and not allowed_schemes:
163
        return url
164

    
165
    if domain:
166
        if not parts.netloc:
167
            return url
168
        if parts.netloc.endswith(domain):
169
            return url
170
        else:
171
            return None
172

    
173
    if allowed_schemes:
174
        if parts.scheme in allowed_schemes:
175
            return url
176

    
177
    return None
178

    
179

    
180
def prepare_response(request, user, next='', renew=False):
181
    """Return the unique username and the token
182
       as 'X-Auth-User' and 'X-Auth-Token' headers,
183
       or redirect to the URL provided in 'next'
184
       with the 'user' and 'token' as parameters.
185

186
       Reissue the token even if it has not yet
187
       expired, if the 'renew' parameter is present
188
       or user has not a valid token.
189
    """
190
    renew = renew or (not user.auth_token)
191
    renew = renew or user.token_expired()
192
    if renew:
193
        user.renew_token(
194
            flush_sessions=True,
195
            current_key=request.session.session_key
196
        )
197
        try:
198
            user.save()
199
        except ValidationError, e:
200
            return HttpResponseBadRequest(e)
201

    
202
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
203

    
204
    if settings.FORCE_PROFILE_UPDATE and \
205
            not user.is_verified and not user.is_superuser:
206
        params = ''
207
        if next:
208
            params = '?' + urlencode({'next': next})
209
        next = reverse('edit_profile') + params
210

    
211
    response = HttpResponse()
212

    
213
    # authenticate before login
214
    user = authenticate(email=user.email, auth_token=user.auth_token)
215
    login(request, user)
216
    request.session.set_expiry(user.auth_token_expires)
217

    
218
    if not next:
219
        next = settings.LOGIN_SUCCESS_URL
220

    
221
    response['Location'] = next
222
    response.status_code = 302
223
    return response
224

    
225
class lazy_string(object):
226
    def __init__(self, function, *args, **kwargs):
227
        self.function = function
228
        self.args = args
229
        self.kwargs = kwargs
230

    
231
    def __str__(self):
232
        if not hasattr(self, 'str'):
233
            self.str = self.function(*self.args, **self.kwargs)
234
        return self.str
235

    
236

    
237
def reverse_lazy(*args, **kwargs):
238
    return lazy_string(reverse, *args, **kwargs)
239

    
240

    
241
def reserved_email(email):
242
    return AstakosUser.objects.user_exists(email)
243

    
244

    
245
def reserved_verified_email(email):
246
    return AstakosUser.objects.verified_user_exists(email)
247

    
248

    
249
def get_query(request):
250
    try:
251
        return request.__getattribute__(request.method)
252
    except AttributeError:
253
        return {}
254

    
255
def get_properties(obj):
256
    def get_class_attr(_class, attr):
257
        try:
258
            return getattr(_class, attr)
259
        except AttributeError:
260
            return
261

    
262
    return (i for i in vars(obj.__class__) \
263
        if isinstance(get_class_attr(obj.__class__, i), property))
264

    
265
def model_to_dict(obj, exclude=['AutoField', 'ForeignKey', 'OneToOneField'],
266
                  include_empty=True):
267
    '''
268
        serialize model object to dict with related objects
269

270
        author: Vadym Zakovinko <vp@zakovinko.com>
271
        date: January 31, 2011
272
        http://djangosnippets.org/snippets/2342/
273
    '''
274
    tree = {}
275
    for field_name in obj._meta.get_all_field_names():
276
        try:
277
            field = getattr(obj, field_name)
278
        except (ObjectDoesNotExist, AttributeError):
279
            continue
280

    
281
        if field.__class__.__name__ in ['RelatedManager', 'ManyRelatedManager']:
282
            if field.model.__name__ in exclude:
283
                continue
284

    
285
            if field.__class__.__name__ == 'ManyRelatedManager':
286
                exclude.append(obj.__class__.__name__)
287
            subtree = []
288
            for related_obj in getattr(obj, field_name).all():
289
                value = model_to_dict(related_obj, exclude=exclude)
290
                if value or include_empty:
291
                    subtree.append(value)
292
            if subtree or include_empty:
293
                tree[field_name] = subtree
294
            continue
295

    
296
        field = obj._meta.get_field_by_name(field_name)[0]
297
        if field.__class__.__name__ in exclude:
298
            continue
299

    
300
        if field.__class__.__name__ == 'RelatedObject':
301
            exclude.append(field.model.__name__)
302
            tree[field_name] = model_to_dict(getattr(obj, field_name),
303
                                             exclude=exclude)
304
            continue
305

    
306
        value = getattr(obj, field_name)
307
        if field.__class__.__name__ == 'ForeignKey':
308
            value = unicode(value) if value is not None else value
309
        if value or include_empty:
310
            tree[field_name] = value
311
    properties = list(get_properties(obj))
312
    for p in properties:
313
       tree[p] = getattr(obj, p)
314
    tree['str_repr'] = obj.__str__()
315

    
316
    return tree
317

    
318
def login_url(request):
319
    attrs = {}
320
    for attr in ['login', 'key', 'code']:
321
        val = request.REQUEST.get(attr, None)
322
        if val:
323
            attrs[attr] = val
324
    return "%s?%s" % (reverse('login'), urllib.urlencode(attrs))
325

    
326

    
327
def redirect_back(request, default='index'):
328
    """
329
    Redirect back to referer if safe and possible.
330
    """
331
    referer = request.META.get('HTTP_REFERER')
332

    
333
    safedomain = settings.BASE_URL.replace("https://", "").replace(
334
        "http://", "")
335
    safe = restrict_next(referer, safedomain)
336
    # avoid redirect loop
337
    loops = referer == request.get_full_path()
338
    if referer and safe and not loops:
339
        return redirect(referer)
340
    return redirect(reverse(default))