Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / util.py @ 6e3bb9c8

History | View | Annotate | Download (13.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 time
36
import urllib
37

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

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

    
50
from astakos.im.models import AstakosUser, Invitation, EmailChange
51
from astakos.im.functions import login
52
from astakos.im import settings
53

    
54
import astakos.im.messages as astakos_messages
55

    
56
logger = logging.getLogger(__name__)
57

    
58

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

    
63
    def tzname(self, dt):
64
        return 'UTC'
65

    
66
    def dst(self, dt):
67
        return timedelta(0)
68

    
69

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

    
73
    return d.replace(tzinfo=UTC()).isoformat()
74

    
75

    
76
def epoch(dt):
77
    return int(time.mktime(dt.timetuple()) * 1000)
78

    
79

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

    
85

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

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

    
106

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

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

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

    
160
    if not domain and not allowed_schemes:
161
        return url
162

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

    
172
    # scheme validation
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

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

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

    
237

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

    
241

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

    
245

    
246
def email_address_log(email, skip_current=False, user_field='uuid'):
247
    user_value = lambda u: user_field(u) if callable(user_field) else \
248
                 getattr(u, user_field)
249
    log = []
250
    if reserved_email(email) and not skip_current:
251
        user = AstakosUser.objects.get_by_identifier(email)
252
        email_date = user.moderated_at
253
        if user.emailchanges.consumed().count():
254
            try:
255
                change = user.emailchanges.consumed().order_by('-consumed_at')
256
                email_date = change[0].consumed_at
257
            except IndexError:
258
                email_date = None
259
        until_date = None
260
        if user.is_accepted():
261
            if not skip_current:
262
                log.append((True, user_value(user), email_date, until_date))
263
        else:
264
            status = user.status_display
265
            log.append((True, user_value(user), status, None))
266

    
267
    emailq = Q(new_email_address__iexact=email) | \
268
             Q(replaced_email_address__iexact=email)
269
    changes = EmailChange.objects.consumed().filter(emailq).order_by(
270
                                                                'consumed_at')
271
    for c in changes:
272
        date_from = None
273
        date_until = None
274

    
275
        if c.new_email_address.lower() == email.lower():
276
            date_from = c.consumed_at
277
            date_until = None
278
            try:
279
                next_change = c.user.emailchanges.filter(
280
                    consumed_at__gt=c.consumed_at)[0]
281
                date_until = next_change.consumed_at
282
            except IndexError:
283
                pass
284
        else:
285
            date_from = c.user.moderated_at
286
            date_until = c.consumed_at
287

    
288
        log.append((False, user_value(c.user), date_from, date_until))
289

    
290
    for c in EmailChange.objects.filter(new_email_address__iexact=email):
291
        log.append((False, user_value(c.user), "pending", None))
292

    
293
    log = sorted(log, key=lambda k: datetime.now() if not k[2] or \
294
                 isinstance(k[2], basestring) else k[2])
295
    return log
296

    
297

    
298
def reserved_verified_email(email):
299
    return AstakosUser.objects.verified_user_exists(email)
300

    
301

    
302
def get_query(request):
303
    try:
304
        return request.__getattribute__(request.method)
305
    except AttributeError:
306
        return {}
307

    
308

    
309
def get_properties(obj):
310
    def get_class_attr(_class, attr):
311
        try:
312
            return getattr(_class, attr)
313
        except AttributeError:
314
            return
315

    
316
    return (i for i in vars(obj.__class__)
317
            if isinstance(get_class_attr(obj.__class__, i), property))
318

    
319

    
320
def model_to_dict(obj, exclude=None, include_empty=True):
321
    '''
322
        serialize model object to dict with related objects
323

324
        author: Vadym Zakovinko <vp@zakovinko.com>
325
        date: January 31, 2011
326
        http://djangosnippets.org/snippets/2342/
327
    '''
328

    
329
    if exclude is None:
330
        exclude = ['AutoField', 'ForeignKey', 'OneToOneField']
331
    tree = {}
332
    for field_name in obj._meta.get_all_field_names():
333
        try:
334
            field = getattr(obj, field_name)
335
        except (ObjectDoesNotExist, AttributeError):
336
            continue
337

    
338
        if field.__class__.__name__ in ['RelatedManager',
339
                                        'ManyRelatedManager']:
340
            if field.model.__name__ in exclude:
341
                continue
342

    
343
            if field.__class__.__name__ == 'ManyRelatedManager':
344
                exclude.append(obj.__class__.__name__)
345
            subtree = []
346
            for related_obj in getattr(obj, field_name).all():
347
                value = model_to_dict(related_obj, exclude=exclude)
348
                if value or include_empty:
349
                    subtree.append(value)
350
            if subtree or include_empty:
351
                tree[field_name] = subtree
352
            continue
353

    
354
        field = obj._meta.get_field_by_name(field_name)[0]
355
        if field.__class__.__name__ in exclude:
356
            continue
357

    
358
        if field.__class__.__name__ == 'RelatedObject':
359
            exclude.append(field.model.__name__)
360
            tree[field_name] = model_to_dict(getattr(obj, field_name),
361
                                             exclude=exclude)
362
            continue
363

    
364
        value = getattr(obj, field_name)
365
        if field.__class__.__name__ == 'ForeignKey':
366
            value = unicode(value) if value is not None else value
367
        if value or include_empty:
368
            tree[field_name] = value
369
    properties = list(get_properties(obj))
370
    for p in properties:
371
        tree[p] = getattr(obj, p)
372
    tree['str_repr'] = obj.__str__()
373

    
374
    return tree
375

    
376

    
377
def login_url(request):
378
    attrs = {}
379
    for attr in ['login', 'key', 'code']:
380
        val = request.REQUEST.get(attr, None)
381
        if val:
382
            attrs[attr] = val
383
    return "%s?%s" % (reverse('login'), urllib.urlencode(attrs))
384

    
385

    
386
def redirect_back(request, default='index'):
387
    """
388
    Redirect back to referer if safe and possible.
389
    """
390
    referer = request.META.get('HTTP_REFERER')
391

    
392
    safedomain = settings.BASE_URL.replace("https://", "").replace(
393
        "http://", "")
394
    safe = restrict_next(referer, safedomain)
395
    # avoid redirect loop
396
    loops = referer == request.get_full_path()
397
    if referer and safe and not loops:
398
        return redirect(referer)
399
    return redirect(reverse(default))