2010-01-13 04:22:26 +03:00
|
|
|
import hashlib
|
2010-08-21 00:45:53 +04:00
|
|
|
import os
|
2010-01-13 04:22:26 +03:00
|
|
|
import random
|
2010-02-12 21:46:44 +03:00
|
|
|
import re
|
2010-01-13 04:22:26 +03:00
|
|
|
import string
|
2010-03-15 21:48:35 +03:00
|
|
|
import time
|
2013-05-06 23:01:53 +04:00
|
|
|
from base64 import decodestring
|
|
|
|
from contextlib import contextmanager
|
|
|
|
from datetime import datetime
|
2010-01-05 00:41:32 +03:00
|
|
|
|
2013-05-06 23:01:53 +04:00
|
|
|
from django import dispatch, forms
|
2010-02-12 21:46:44 +03:00
|
|
|
from django.conf import settings
|
2010-01-18 13:51:34 +03:00
|
|
|
from django.contrib.auth.models import User as DjangoUser
|
2010-12-02 01:07:06 +03:00
|
|
|
from django.core import validators
|
2011-12-13 02:27:46 +04:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
2012-06-10 08:06:50 +04:00
|
|
|
from django.db import models, transaction
|
2010-03-29 19:09:52 +04:00
|
|
|
from django.template import Context, loader
|
2013-05-06 23:01:53 +04:00
|
|
|
from django.utils import translation
|
2011-08-26 03:09:33 +04:00
|
|
|
from django.utils.encoding import smart_str, smart_unicode
|
2010-09-30 07:02:15 +04:00
|
|
|
from django.utils.functional import lazy
|
2009-10-23 02:37:15 +04:00
|
|
|
|
2010-08-05 11:37:22 +04:00
|
|
|
import caching.base as caching
|
2010-05-20 01:05:20 +04:00
|
|
|
import commonware.log
|
2013-07-30 03:43:03 +04:00
|
|
|
import tower
|
2010-05-20 01:05:20 +04:00
|
|
|
from tower import ugettext as _
|
|
|
|
|
2010-02-22 10:57:38 +03:00
|
|
|
import amo
|
2010-01-29 04:59:26 +03:00
|
|
|
import amo.models
|
2012-06-10 08:06:50 +04:00
|
|
|
from access.models import Group, GroupUser
|
2010-02-24 02:26:11 +03:00
|
|
|
from amo.urlresolvers import reverse
|
2013-05-06 23:01:53 +04:00
|
|
|
from translations.fields import PurifiedField, save_signal
|
2011-11-01 05:40:12 +04:00
|
|
|
from translations.query import order_by_translation
|
2009-10-23 02:37:15 +04:00
|
|
|
|
2010-05-20 01:05:20 +04:00
|
|
|
log = commonware.log.getLogger('z.users')
|
2010-03-12 09:56:44 +03:00
|
|
|
|
2009-10-23 02:37:15 +04:00
|
|
|
|
2010-01-13 04:22:26 +03:00
|
|
|
def get_hexdigest(algorithm, salt, raw_password):
|
2012-03-07 04:41:07 +04:00
|
|
|
if 'base64' in algorithm:
|
|
|
|
# These are getpersonas passwords with base64 encoded salts.
|
|
|
|
salt = decodestring(salt)
|
|
|
|
algorithm = algorithm.replace('+base64', '')
|
|
|
|
|
|
|
|
if algorithm.startswith('sha512+MD5'):
|
|
|
|
# These are persona specific passwords when we imported
|
|
|
|
# users from getpersonas.com. The password is md5 hashed
|
|
|
|
# and then sha512'd.
|
|
|
|
md5 = hashlib.new('md5', raw_password).hexdigest()
|
|
|
|
return hashlib.new('sha512', smart_str(salt + md5)).hexdigest()
|
|
|
|
|
2010-09-08 01:38:33 +04:00
|
|
|
return hashlib.new(algorithm, smart_str(salt + raw_password)).hexdigest()
|
2010-01-13 04:22:26 +03:00
|
|
|
|
|
|
|
|
|
|
|
def rand_string(length):
|
|
|
|
return ''.join(random.choice(string.letters) for i in xrange(length))
|
|
|
|
|
|
|
|
|
|
|
|
def create_password(algorithm, raw_password):
|
2011-12-20 00:55:16 +04:00
|
|
|
salt = get_hexdigest(algorithm, rand_string(12), rand_string(12))[:64]
|
|
|
|
hsh = get_hexdigest(algorithm, salt, raw_password)
|
|
|
|
return '$'.join([algorithm, salt, hsh])
|
2010-01-13 04:22:26 +03:00
|
|
|
|
|
|
|
|
2010-09-30 07:02:15 +04:00
|
|
|
class UserForeignKey(models.ForeignKey):
|
|
|
|
"""
|
|
|
|
A replacement for models.ForeignKey('users.UserProfile').
|
|
|
|
|
|
|
|
This field uses UserEmailField to make form fields key off the user's email
|
|
|
|
instead of the primary key id. We also hook up autocomplete automatically.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, *args, **kw):
|
|
|
|
super(UserForeignKey, self).__init__(UserProfile, *args, **kw)
|
|
|
|
|
|
|
|
def value_from_object(self, obj):
|
|
|
|
return getattr(obj, self.name).email
|
|
|
|
|
|
|
|
def formfield(self, **kw):
|
|
|
|
defaults = {'form_class': UserEmailField}
|
|
|
|
defaults.update(kw)
|
|
|
|
return models.Field.formfield(self, **defaults)
|
|
|
|
|
|
|
|
|
|
|
|
class UserEmailField(forms.EmailField):
|
|
|
|
|
|
|
|
def clean(self, value):
|
2010-12-02 01:07:06 +03:00
|
|
|
if value in validators.EMPTY_VALUES:
|
|
|
|
raise forms.ValidationError(self.error_messages['required'])
|
2010-09-30 07:02:15 +04:00
|
|
|
try:
|
|
|
|
return UserProfile.objects.get(email=value)
|
|
|
|
except UserProfile.DoesNotExist:
|
|
|
|
raise forms.ValidationError(_('No user with that email.'))
|
|
|
|
|
|
|
|
def widget_attrs(self, widget):
|
|
|
|
lazy_reverse = lazy(reverse, str)
|
|
|
|
return {'class': 'email-autocomplete',
|
|
|
|
'data-src': lazy_reverse('users.ajax')}
|
|
|
|
|
|
|
|
|
2011-10-04 23:35:40 +04:00
|
|
|
class UserProfile(amo.models.OnChangeMixin, amo.models.ModelBase):
|
2010-08-05 00:49:44 +04:00
|
|
|
username = models.CharField(max_length=255, default='', unique=True)
|
|
|
|
display_name = models.CharField(max_length=255, default='', null=True,
|
|
|
|
blank=True)
|
|
|
|
|
2010-01-05 00:41:32 +03:00
|
|
|
password = models.CharField(max_length=255, default='')
|
2010-09-03 09:09:05 +04:00
|
|
|
email = models.EmailField(unique=True, null=True)
|
2010-01-05 00:41:32 +03:00
|
|
|
|
2010-06-24 01:20:46 +04:00
|
|
|
averagerating = models.CharField(max_length=255, blank=True, null=True)
|
2010-11-04 06:10:21 +03:00
|
|
|
bio = PurifiedField(short=False)
|
2010-02-09 04:02:15 +03:00
|
|
|
confirmationcode = models.CharField(max_length=255, default='',
|
|
|
|
blank=True)
|
2010-04-02 03:16:59 +04:00
|
|
|
deleted = models.BooleanField(default=False)
|
2010-01-05 00:41:32 +03:00
|
|
|
display_collections = models.BooleanField(default=False)
|
|
|
|
display_collections_fav = models.BooleanField(default=False)
|
2011-06-03 02:35:38 +04:00
|
|
|
emailhidden = models.BooleanField(default=True)
|
2013-10-02 22:56:39 +04:00
|
|
|
homepage = models.URLField(max_length=255, blank=True, default='')
|
2010-04-02 03:16:59 +04:00
|
|
|
location = models.CharField(max_length=255, blank=True, default='')
|
2010-06-24 01:20:46 +04:00
|
|
|
notes = models.TextField(blank=True, null=True)
|
2010-01-05 00:41:32 +03:00
|
|
|
notifycompat = models.BooleanField(default=True)
|
|
|
|
notifyevents = models.BooleanField(default=True)
|
2010-04-02 03:16:59 +04:00
|
|
|
occupation = models.CharField(max_length=255, default='', blank=True)
|
2010-06-10 06:23:22 +04:00
|
|
|
# This is essentially a "has_picture" flag right now
|
2010-02-09 04:02:15 +03:00
|
|
|
picture_type = models.CharField(max_length=75, default='', blank=True)
|
|
|
|
resetcode = models.CharField(max_length=255, default='', blank=True)
|
2010-06-11 07:00:56 +04:00
|
|
|
resetcode_expires = models.DateTimeField(default=datetime.now, null=True,
|
2010-02-09 04:02:15 +03:00
|
|
|
blank=True)
|
2012-08-21 01:14:35 +04:00
|
|
|
read_dev_agreement = models.DateTimeField(null=True, blank=True)
|
2012-02-09 15:27:26 +04:00
|
|
|
|
2010-08-27 09:31:47 +04:00
|
|
|
last_login_ip = models.CharField(default='', max_length=45, editable=False)
|
|
|
|
last_login_attempt = models.DateTimeField(null=True, editable=False)
|
|
|
|
last_login_attempt_ip = models.CharField(default='', max_length=45,
|
|
|
|
editable=False)
|
|
|
|
failed_login_attempts = models.PositiveIntegerField(default=0,
|
|
|
|
editable=False)
|
2012-08-23 00:46:38 +04:00
|
|
|
source = models.PositiveIntegerField(default=amo.LOGIN_SOURCE_UNKNOWN,
|
2012-10-11 00:10:50 +04:00
|
|
|
editable=False, db_index=True)
|
2010-02-09 04:02:15 +03:00
|
|
|
user = models.ForeignKey(DjangoUser, null=True, editable=False, blank=True)
|
2012-11-15 22:03:07 +04:00
|
|
|
is_verified = models.BooleanField(default=True)
|
2013-04-27 01:08:35 +04:00
|
|
|
region = models.CharField(max_length=9, null=True, blank=True,
|
|
|
|
editable=False)
|
|
|
|
lang = models.CharField(max_length=5, null=True, blank=True,
|
|
|
|
editable=False)
|
2009-10-23 02:37:15 +04:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
db_table = 'users'
|
|
|
|
|
2011-08-26 21:57:23 +04:00
|
|
|
def __init__(self, *args, **kw):
|
|
|
|
super(UserProfile, self).__init__(*args, **kw)
|
|
|
|
if self.username:
|
|
|
|
self.username = smart_unicode(self.username)
|
|
|
|
|
2010-01-05 00:41:32 +03:00
|
|
|
def __unicode__(self):
|
2012-03-12 22:16:16 +04:00
|
|
|
return u'%s: %s' % (self.id, self.display_name or self.username)
|
2010-01-05 00:41:32 +03:00
|
|
|
|
2012-06-13 01:17:46 +04:00
|
|
|
def is_anonymous(self):
|
|
|
|
return False
|
|
|
|
|
2013-04-19 01:04:12 +04:00
|
|
|
def get_user_url(self, name='profile', src=None, args=None):
|
2013-03-29 00:48:11 +04:00
|
|
|
"""
|
|
|
|
We use <username> as the slug, unless it contains gross
|
|
|
|
characters - in which case use <id> as the slug.
|
|
|
|
"""
|
2013-06-08 02:33:18 +04:00
|
|
|
# TODO: Remove this ASAP (bug 880767).
|
|
|
|
if settings.MARKETPLACE and name == 'profile':
|
|
|
|
return '#'
|
2013-02-26 11:34:22 +04:00
|
|
|
from amo.utils import urlparams
|
2013-03-29 00:48:11 +04:00
|
|
|
chars = '/<>"\''
|
|
|
|
slug = self.username
|
2013-03-29 02:26:45 +04:00
|
|
|
if not self.username or any(x in chars for x in self.username):
|
2013-03-29 00:48:11 +04:00
|
|
|
slug = self.id
|
2013-04-19 01:04:12 +04:00
|
|
|
args = args or []
|
|
|
|
url = reverse('users.%s' % name, args=[slug] + args)
|
2013-03-20 22:01:34 +04:00
|
|
|
return urlparams(url, src=src)
|
2009-10-23 02:37:15 +04:00
|
|
|
|
2013-04-19 01:04:12 +04:00
|
|
|
def get_url_path(self, src=None):
|
|
|
|
return self.get_user_url('profile', src=src)
|
|
|
|
|
2010-08-13 20:24:36 +04:00
|
|
|
def flush_urls(self):
|
2010-11-11 04:03:11 +03:00
|
|
|
urls = ['*/user/%d/' % self.id,
|
|
|
|
self.picture_url,
|
|
|
|
]
|
2010-08-13 20:24:36 +04:00
|
|
|
|
|
|
|
return urls
|
|
|
|
|
2010-02-12 21:46:44 +03:00
|
|
|
@amo.cached_property
|
|
|
|
def addons_listed(self):
|
2010-06-22 21:51:29 +04:00
|
|
|
"""Public add-ons this user is listed as author of."""
|
2012-03-20 10:09:13 +04:00
|
|
|
return self.addons.reviewed().exclude(type=amo.ADDON_WEBAPP).filter(
|
|
|
|
addonuser__user=self, addonuser__listed=True)
|
2011-11-19 00:05:38 +04:00
|
|
|
|
2013-04-19 03:54:26 +04:00
|
|
|
@property
|
2013-04-13 01:39:43 +04:00
|
|
|
def num_addons_listed(self):
|
|
|
|
"""Number of public add-ons this user is listed as author of."""
|
|
|
|
return self.addons.reviewed().exclude(type=amo.ADDON_WEBAPP).filter(
|
|
|
|
addonuser__user=self, addonuser__listed=True).count()
|
|
|
|
|
2011-11-19 00:05:38 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def apps_listed(self):
|
|
|
|
"""Public apps this user is listed as author of."""
|
2012-02-24 01:55:14 +04:00
|
|
|
return self.addons.reviewed().filter(type=amo.ADDON_WEBAPP,
|
|
|
|
addonuser__user=self, addonuser__listed=True)
|
2010-02-12 21:46:44 +03:00
|
|
|
|
2011-11-01 05:40:12 +04:00
|
|
|
def my_addons(self, n=8):
|
|
|
|
"""Returns n addons (anything not a webapp)"""
|
|
|
|
qs = self.addons.exclude(type=amo.ADDON_WEBAPP)
|
|
|
|
qs = order_by_translation(qs, 'name')
|
|
|
|
return qs[:n]
|
|
|
|
|
|
|
|
def my_apps(self, n=8):
|
|
|
|
"""Returns n apps"""
|
|
|
|
qs = self.addons.filter(type=amo.ADDON_WEBAPP)
|
|
|
|
qs = order_by_translation(qs, 'name')
|
|
|
|
return qs[:n]
|
|
|
|
|
2010-02-12 21:46:44 +03:00
|
|
|
@property
|
2010-08-21 00:45:53 +04:00
|
|
|
def picture_dir(self):
|
2010-02-12 21:46:44 +03:00
|
|
|
split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
|
2010-08-21 01:10:46 +04:00
|
|
|
return os.path.join(settings.USERPICS_PATH, split_id.group(2) or '0',
|
|
|
|
split_id.group(1) or '0')
|
2010-08-21 00:45:53 +04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def picture_path(self):
|
|
|
|
return os.path.join(self.picture_dir, str(self.id) + '.png')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def picture_url(self):
|
2010-03-15 21:48:35 +03:00
|
|
|
if not self.picture_type:
|
|
|
|
return settings.MEDIA_URL + '/img/zamboni/anon_user.png'
|
|
|
|
else:
|
2010-08-21 00:45:53 +04:00
|
|
|
split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
|
|
|
|
return settings.USERPICS_URL % (
|
2010-03-15 21:48:35 +03:00
|
|
|
split_id.group(2) or 0, split_id.group(1) or 0, self.id,
|
|
|
|
int(time.mktime(self.modified.timetuple())))
|
2010-02-12 21:46:44 +03:00
|
|
|
|
|
|
|
@amo.cached_property
|
|
|
|
def is_developer(self):
|
2010-10-21 23:10:15 +04:00
|
|
|
return self.addonuser_set.exists()
|
2010-02-12 21:46:44 +03:00
|
|
|
|
2013-04-05 03:08:20 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def is_addon_developer(self):
|
|
|
|
return self.addonuser_set.exclude(
|
|
|
|
addon__type=amo.ADDON_PERSONA).exists()
|
|
|
|
|
2012-04-25 06:22:59 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def is_app_developer(self):
|
|
|
|
return self.addonuser_set.filter(addon__type=amo.ADDON_WEBAPP).exists()
|
|
|
|
|
2012-02-07 05:01:47 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def is_artist(self):
|
|
|
|
"""Is this user a Personas Artist?"""
|
|
|
|
return self.addonuser_set.filter(
|
|
|
|
addon__type=amo.ADDON_PERSONA).exists()
|
|
|
|
|
2011-06-15 03:13:58 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def needs_tougher_password(user):
|
2012-10-10 22:45:12 +04:00
|
|
|
if user.source in amo.LOGIN_SOURCE_BROWSERIDS:
|
2012-08-23 00:46:38 +04:00
|
|
|
return False
|
2012-02-14 04:23:16 +04:00
|
|
|
from access import acl
|
|
|
|
return (acl.action_allowed_user(user, 'Admin', '%') or
|
2012-03-20 02:52:20 +04:00
|
|
|
acl.action_allowed_user(user, 'Addons', 'Edit') or
|
2012-02-14 04:23:16 +04:00
|
|
|
acl.action_allowed_user(user, 'Addons', 'Review') or
|
|
|
|
acl.action_allowed_user(user, 'Apps', 'Review') or
|
2012-03-20 02:52:20 +04:00
|
|
|
acl.action_allowed_user(user, 'Personas', 'Review') or
|
|
|
|
acl.action_allowed_user(user, 'Users', 'Edit'))
|
2011-06-15 03:13:58 +04:00
|
|
|
|
2009-12-14 23:34:11 +03:00
|
|
|
@property
|
2010-08-30 23:15:25 +04:00
|
|
|
def name(self):
|
2011-08-26 03:09:33 +04:00
|
|
|
return smart_unicode(self.display_name or self.username)
|
2009-12-14 23:34:11 +03:00
|
|
|
|
2010-08-30 23:15:25 +04:00
|
|
|
welcome_name = name
|
|
|
|
|
2010-09-22 22:57:58 +04:00
|
|
|
@property
|
|
|
|
def last_login(self):
|
|
|
|
"""Make UserProfile look more like auth.User."""
|
|
|
|
# Django expects this to be non-null, so fake a login attempt.
|
|
|
|
if not self.last_login_attempt:
|
|
|
|
self.update(last_login_attempt=datetime.now())
|
|
|
|
return self.last_login_attempt
|
|
|
|
|
2010-03-31 11:59:08 +04:00
|
|
|
@amo.cached_property
|
|
|
|
def reviews(self):
|
|
|
|
"""All reviews that are not dev replies."""
|
|
|
|
return self._reviews_all.filter(reply_to=None)
|
|
|
|
|
2010-03-26 03:41:29 +03:00
|
|
|
def anonymize(self):
|
2010-08-24 21:04:40 +04:00
|
|
|
log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email))
|
2010-09-03 08:38:08 +04:00
|
|
|
self.email = None
|
2010-03-26 03:41:29 +03:00
|
|
|
self.password = "sha512$Anonymous$Password"
|
2010-08-05 00:49:44 +04:00
|
|
|
self.username = "Anonymous-%s" % self.id # Can't be null
|
|
|
|
self.display_name = None
|
2010-03-26 03:41:29 +03:00
|
|
|
self.homepage = ""
|
|
|
|
self.deleted = True
|
|
|
|
self.picture_type = ""
|
|
|
|
self.save()
|
|
|
|
|
2012-06-10 08:06:50 +04:00
|
|
|
@transaction.commit_on_success
|
|
|
|
def restrict(self):
|
|
|
|
from amo.utils import send_mail
|
|
|
|
log.info(u'User (%s: <%s>) is being restricted and '
|
|
|
|
'its user-generated content removed.' % (self, self.email))
|
|
|
|
g = Group.objects.get(rules='Restricted:UGC')
|
|
|
|
GroupUser.objects.create(user=self, group=g)
|
|
|
|
self.reviews.all().delete()
|
|
|
|
self.collections.all().delete()
|
|
|
|
|
|
|
|
t = loader.get_template('users/email/restricted.ltxt')
|
|
|
|
send_mail(_('Your account has been restricted'),
|
|
|
|
t.render(Context({})), None, [self.email],
|
2012-08-03 01:27:23 +04:00
|
|
|
use_blacklist=False, real_email=True)
|
2012-06-10 08:06:50 +04:00
|
|
|
|
|
|
|
def unrestrict(self):
|
2012-06-14 23:59:52 +04:00
|
|
|
log.info(u'User (%s: <%s>) is being unrestricted.' % (self,
|
|
|
|
self.email))
|
|
|
|
GroupUser.objects.filter(user=self,
|
|
|
|
group__rules='Restricted:UGC').delete()
|
2012-06-10 08:06:50 +04:00
|
|
|
|
2010-08-20 00:40:36 +04:00
|
|
|
def generate_confirmationcode(self):
|
|
|
|
if not self.confirmationcode:
|
|
|
|
self.confirmationcode = ''.join(random.sample(string.letters +
|
|
|
|
string.digits, 60))
|
|
|
|
return self.confirmationcode
|
|
|
|
|
2010-01-15 04:04:08 +03:00
|
|
|
def save(self, force_insert=False, force_update=False, using=None):
|
2009-12-14 23:34:11 +03:00
|
|
|
# we have to fix stupid things that we defined poorly in remora
|
2010-08-20 00:40:36 +04:00
|
|
|
if not self.resetcode_expires:
|
2009-12-14 23:34:11 +03:00
|
|
|
self.resetcode_expires = datetime.now()
|
|
|
|
|
2010-08-20 03:38:11 +04:00
|
|
|
delete_user = None
|
|
|
|
if self.deleted and self.user:
|
|
|
|
delete_user = self.user
|
|
|
|
self.user = None
|
|
|
|
# Delete user after saving this profile.
|
|
|
|
|
2010-01-15 04:04:08 +03:00
|
|
|
super(UserProfile, self).save(force_insert, force_update, using)
|
2010-01-13 04:22:26 +03:00
|
|
|
|
2010-08-20 03:38:11 +04:00
|
|
|
if self.deleted and delete_user:
|
|
|
|
delete_user.delete()
|
|
|
|
|
2010-01-13 04:22:26 +03:00
|
|
|
def check_password(self, raw_password):
|
2012-08-23 00:46:38 +04:00
|
|
|
# BrowserID does not store a password.
|
2013-01-24 09:21:33 +04:00
|
|
|
if (self.source in amo.LOGIN_SOURCE_BROWSERIDS
|
|
|
|
and settings.MARKETPLACE):
|
2012-08-23 00:46:38 +04:00
|
|
|
return True
|
2013-01-24 09:21:33 +04:00
|
|
|
|
2011-12-20 00:55:16 +04:00
|
|
|
if '$' not in self.password:
|
|
|
|
valid = (get_hexdigest('md5', '', raw_password) == self.password)
|
|
|
|
if valid:
|
|
|
|
# Upgrade an old password.
|
2010-01-13 04:22:26 +03:00
|
|
|
self.set_password(raw_password)
|
|
|
|
self.save()
|
2011-12-20 00:55:16 +04:00
|
|
|
return valid
|
|
|
|
|
|
|
|
algo, salt, hsh = self.password.split('$')
|
2013-04-05 01:39:12 +04:00
|
|
|
#Complication due to getpersonas account migration; we don't
|
|
|
|
#know if passwords were utf-8 or latin-1 when hashed. If you
|
|
|
|
#can prove that they are one or the other, you can delete one
|
|
|
|
#of these branches.
|
|
|
|
if '+base64' in algo and isinstance(raw_password, unicode):
|
|
|
|
if hsh == get_hexdigest(algo, salt, raw_password.encode('utf-8')):
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
return hsh == get_hexdigest(algo, salt,
|
|
|
|
raw_password.encode('latin1'))
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return hsh == get_hexdigest(algo, salt, raw_password)
|
2010-01-13 04:22:26 +03:00
|
|
|
|
2011-12-20 00:55:16 +04:00
|
|
|
def set_password(self, raw_password, algorithm='sha512'):
|
2010-01-13 04:22:26 +03:00
|
|
|
self.password = create_password(algorithm, raw_password)
|
2011-12-02 01:53:51 +04:00
|
|
|
# Can't do CEF logging here because we don't have a request object.
|
2010-01-13 04:26:20 +03:00
|
|
|
|
2010-03-29 19:09:52 +04:00
|
|
|
def email_confirmation_code(self):
|
2010-11-16 22:31:38 +03:00
|
|
|
from amo.utils import send_mail
|
2010-03-29 19:09:52 +04:00
|
|
|
log.debug("Sending account confirmation code for user (%s)", self)
|
|
|
|
|
|
|
|
url = "%s%s" % (settings.SITE_URL,
|
|
|
|
reverse('users.confirm',
|
|
|
|
args=[self.id, self.confirmationcode]))
|
|
|
|
domain = settings.DOMAIN
|
2010-04-01 07:49:13 +04:00
|
|
|
t = loader.get_template('users/email/confirm.ltxt')
|
2010-03-29 19:09:52 +04:00
|
|
|
c = {'domain': domain, 'url': url, }
|
|
|
|
send_mail(_("Please confirm your email address"),
|
2010-11-16 22:31:38 +03:00
|
|
|
t.render(Context(c)), None, [self.email],
|
2012-08-03 01:27:23 +04:00
|
|
|
use_blacklist=False, real_email=True)
|
2010-03-29 19:09:52 +04:00
|
|
|
|
2011-12-02 01:53:51 +04:00
|
|
|
def log_login_attempt(self, successful):
|
2010-08-27 09:31:47 +04:00
|
|
|
"""Log a user's login attempt"""
|
|
|
|
self.last_login_attempt = datetime.now()
|
2011-03-26 03:13:44 +03:00
|
|
|
self.last_login_attempt_ip = commonware.log.get_remote_addr()
|
2010-08-27 09:31:47 +04:00
|
|
|
|
|
|
|
if successful:
|
|
|
|
log.debug(u"User (%s) logged in successfully" % self)
|
|
|
|
self.failed_login_attempts = 0
|
2011-03-26 03:13:44 +03:00
|
|
|
self.last_login_ip = commonware.log.get_remote_addr()
|
2010-08-27 09:31:47 +04:00
|
|
|
else:
|
|
|
|
log.debug(u"User (%s) failed to log in" % self)
|
|
|
|
if self.failed_login_attempts < 16777216:
|
|
|
|
self.failed_login_attempts += 1
|
|
|
|
|
|
|
|
self.save()
|
|
|
|
|
2012-11-15 22:03:07 +04:00
|
|
|
def create_django_user(self, **kw):
|
2010-01-13 04:26:20 +03:00
|
|
|
"""Make a django.contrib.auth.User for this UserProfile."""
|
2013-08-24 03:19:00 +04:00
|
|
|
# Due to situations like bug 905984 and similar, a django user
|
|
|
|
# for this email may already exist. Let's try to find it first
|
|
|
|
# before creating a new one.
|
|
|
|
try:
|
|
|
|
self.user = DjangoUser.objects.get(email=self.email)
|
|
|
|
for k, v in kw.iteritems():
|
|
|
|
setattr(self.user, k, v)
|
|
|
|
self.save()
|
|
|
|
return self.user
|
|
|
|
except DjangoUser.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2010-01-13 04:26:20 +03:00
|
|
|
# Reusing the id will make our life easier, because we can use the
|
|
|
|
# OneToOneField as pk for Profile linked back to the auth.user
|
|
|
|
# in the future.
|
2010-01-18 22:45:23 +03:00
|
|
|
self.user = DjangoUser(id=self.pk)
|
2010-08-05 00:49:44 +04:00
|
|
|
self.user.first_name = ''
|
|
|
|
self.user.last_name = ''
|
2013-06-19 00:31:23 +04:00
|
|
|
self.user.username = 'uid-%d' % self.pk
|
2010-01-13 04:26:20 +03:00
|
|
|
self.user.email = self.email
|
|
|
|
self.user.password = self.password
|
|
|
|
self.user.date_joined = self.created
|
|
|
|
|
2012-11-15 22:03:07 +04:00
|
|
|
for k, v in kw.iteritems():
|
|
|
|
setattr(self.user, k, v)
|
|
|
|
|
2010-05-18 15:29:16 +04:00
|
|
|
if self.groups.filter(rules='*:*').count():
|
2010-01-13 04:26:20 +03:00
|
|
|
self.user.is_superuser = self.user.is_staff = True
|
|
|
|
|
|
|
|
self.user.save()
|
|
|
|
self.save()
|
|
|
|
return self.user
|
2010-07-13 07:46:23 +04:00
|
|
|
|
2010-08-10 05:37:34 +04:00
|
|
|
def mobile_collection(self):
|
|
|
|
return self.special_collection(amo.COLLECTION_MOBILE,
|
|
|
|
defaults={'slug': 'mobile', 'listed': False,
|
|
|
|
'name': _('My Mobile Add-ons')})
|
|
|
|
|
|
|
|
def favorites_collection(self):
|
|
|
|
return self.special_collection(amo.COLLECTION_FAVORITES,
|
|
|
|
defaults={'slug': 'favorites', 'listed': False,
|
|
|
|
'name': _('My Favorite Add-ons')})
|
|
|
|
|
|
|
|
def special_collection(self, type_, defaults):
|
|
|
|
from bandwagon.models import Collection
|
2010-10-22 00:18:22 +04:00
|
|
|
c, new = Collection.objects.get_or_create(
|
2010-08-10 05:37:34 +04:00
|
|
|
author=self, type=type_, defaults=defaults)
|
2010-10-22 00:18:22 +04:00
|
|
|
if new:
|
|
|
|
# Do an extra query to make sure this gets transformed.
|
2010-11-10 08:47:01 +03:00
|
|
|
c = Collection.objects.using('default').get(id=c.id)
|
2010-08-10 05:37:34 +04:00
|
|
|
return c
|
|
|
|
|
2011-09-23 21:52:29 +04:00
|
|
|
def purchase_ids(self):
|
2012-05-09 02:40:21 +04:00
|
|
|
"""
|
|
|
|
I'm special casing this because we use purchase_ids a lot in the site
|
|
|
|
and we are not caching empty querysets in cache-machine.
|
|
|
|
That means that when the site is first launched we are having a
|
|
|
|
lot of empty queries hit.
|
|
|
|
|
|
|
|
We can probably do this in smarter fashion by making cache-machine
|
|
|
|
cache empty queries on an as need basis.
|
|
|
|
"""
|
|
|
|
# Circular import
|
|
|
|
from amo.utils import memoize
|
|
|
|
from market.models import AddonPurchase
|
2012-06-14 23:59:52 +04:00
|
|
|
|
2012-05-09 02:40:21 +04:00
|
|
|
@memoize(prefix='users:purchase-ids')
|
|
|
|
def ids(pk):
|
|
|
|
return (AddonPurchase.objects.filter(user=pk)
|
|
|
|
.values_list('addon_id', flat=True)
|
|
|
|
.filter(type=amo.CONTRIB_PURCHASE)
|
|
|
|
.order_by('pk'))
|
|
|
|
return ids(self.pk)
|
2011-09-23 21:08:33 +04:00
|
|
|
|
2011-12-13 02:27:46 +04:00
|
|
|
def get_preapproval(self):
|
|
|
|
"""
|
|
|
|
Returns the pre approval object for this user, or None if it does
|
|
|
|
not exist
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return self.preapprovaluser
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2011-12-21 02:33:13 +04:00
|
|
|
def has_preapproval_key(self):
|
|
|
|
"""
|
2012-05-08 01:14:52 +04:00
|
|
|
Returns the pre approval paypal key for this user, or False if the
|
2011-12-21 02:33:13 +04:00
|
|
|
pre_approval doesn't exist or the key is blank.
|
|
|
|
"""
|
|
|
|
return bool(getattr(self.get_preapproval(), 'paypal_key', ''))
|
|
|
|
|
2013-05-06 23:01:53 +04:00
|
|
|
@contextmanager
|
|
|
|
def activate_lang(self):
|
|
|
|
"""
|
|
|
|
Activate the language for the user. If none is set will go to the site
|
|
|
|
default which is en-US.
|
|
|
|
"""
|
|
|
|
lang = self.lang if self.lang else settings.LANGUAGE_CODE
|
|
|
|
old = translation.get_language()
|
2013-07-30 03:43:03 +04:00
|
|
|
tower.activate(lang)
|
2013-05-06 23:01:53 +04:00
|
|
|
yield
|
2013-07-30 03:43:03 +04:00
|
|
|
tower.activate(old)
|
2013-05-06 23:01:53 +04:00
|
|
|
|
|
|
|
|
2013-03-06 00:29:33 +04:00
|
|
|
models.signals.pre_save.connect(save_signal, sender=UserProfile,
|
|
|
|
dispatch_uid='userprofile_translations')
|
|
|
|
|
2010-10-30 04:27:57 +04:00
|
|
|
|
2011-08-02 02:01:30 +04:00
|
|
|
@dispatch.receiver(models.signals.post_save, sender=UserProfile,
|
|
|
|
dispatch_uid='user.post_save')
|
|
|
|
def user_post_save(sender, instance, **kw):
|
|
|
|
if not kw.get('raw'):
|
|
|
|
from . import tasks
|
|
|
|
tasks.index_users.delay([instance.id])
|
|
|
|
|
|
|
|
|
|
|
|
@dispatch.receiver(models.signals.post_delete, sender=UserProfile,
|
|
|
|
dispatch_uid='user.post_delete')
|
|
|
|
def user_post_delete(sender, instance, **kw):
|
|
|
|
if not kw.get('raw'):
|
|
|
|
from . import tasks
|
|
|
|
tasks.unindex_users.delay([instance.id])
|
|
|
|
|
|
|
|
|
2011-07-19 02:49:15 +04:00
|
|
|
class UserNotification(amo.models.ModelBase):
|
|
|
|
user = models.ForeignKey(UserProfile, related_name='notifications')
|
|
|
|
notification_id = models.IntegerField()
|
|
|
|
enabled = models.BooleanField(default=False)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
db_table = 'users_notifications'
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def update_or_create(update={}, **kwargs):
|
|
|
|
rows = UserNotification.objects.filter(**kwargs).update(**update)
|
|
|
|
if not rows:
|
|
|
|
update.update(dict(**kwargs))
|
2011-08-02 02:01:30 +04:00
|
|
|
UserNotification.objects.create(**update)
|
2011-07-19 02:49:15 +04:00
|
|
|
|
|
|
|
|
2010-10-30 04:27:57 +04:00
|
|
|
class RequestUserManager(amo.models.ManagerBase):
|
|
|
|
|
|
|
|
def get_query_set(self):
|
|
|
|
qs = super(RequestUserManager, self).get_query_set()
|
|
|
|
return qs.transform(RequestUser.transformer)
|
|
|
|
|
|
|
|
|
|
|
|
class RequestUser(UserProfile):
|
|
|
|
"""
|
|
|
|
A RequestUser has extra attributes we don't care about for normal users.
|
|
|
|
"""
|
|
|
|
|
|
|
|
objects = RequestUserManager()
|
|
|
|
|
|
|
|
def __init__(self, *args, **kw):
|
|
|
|
super(RequestUser, self).__init__(*args, **kw)
|
|
|
|
self.mobile_addons = []
|
|
|
|
self.favorite_addons = []
|
|
|
|
self.watching = []
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
proxy = True
|
|
|
|
|
2010-08-11 03:33:05 +04:00
|
|
|
@staticmethod
|
2010-10-30 04:27:57 +04:00
|
|
|
def transformer(users):
|
2010-08-11 03:33:05 +04:00
|
|
|
# We don't want to cache these things on every UserProfile; they're
|
|
|
|
# only used by a user attached to a request.
|
2010-10-30 04:27:57 +04:00
|
|
|
if not users:
|
|
|
|
return
|
2012-05-09 02:17:42 +04:00
|
|
|
|
|
|
|
# Touch this @cached_property so the answer is cached with the object.
|
|
|
|
user = users[0]
|
|
|
|
user.is_developer
|
|
|
|
|
|
|
|
# Until the Marketplace gets collections, these lookups are pointless.
|
|
|
|
if settings.MARKETPLACE:
|
|
|
|
return
|
|
|
|
|
2010-08-24 07:32:15 +04:00
|
|
|
from bandwagon.models import CollectionAddon, CollectionWatcher
|
2010-08-18 23:03:48 +04:00
|
|
|
SPECIAL = amo.COLLECTION_SPECIAL_SLUGS.keys()
|
2010-08-11 03:33:05 +04:00
|
|
|
qs = CollectionAddon.objects.filter(
|
2010-08-18 23:03:48 +04:00
|
|
|
collection__author=user, collection__type__in=SPECIAL)
|
|
|
|
addons = dict((type_, []) for type_ in SPECIAL)
|
|
|
|
for addon, ctype in qs.values_list('addon', 'collection__type'):
|
|
|
|
addons[ctype].append(addon)
|
|
|
|
user.mobile_addons = addons[amo.COLLECTION_MOBILE]
|
|
|
|
user.favorite_addons = addons[amo.COLLECTION_FAVORITES]
|
2010-08-24 07:32:15 +04:00
|
|
|
user.watching = list((CollectionWatcher.objects.filter(user=user)
|
|
|
|
.values_list('collection', flat=True)))
|
2010-08-11 03:33:05 +04:00
|
|
|
|
2010-10-30 04:27:57 +04:00
|
|
|
def _cache_keys(self):
|
|
|
|
# Add UserProfile.cache_key so RequestUser gets invalidated when the
|
|
|
|
# UserProfile is changed.
|
|
|
|
keys = super(RequestUser, self)._cache_keys()
|
2013-10-08 01:59:09 +04:00
|
|
|
return keys + (UserProfile._cache_key(self.id, 'default'),)
|
2010-10-30 04:27:57 +04:00
|
|
|
|
2010-08-11 03:33:05 +04:00
|
|
|
|
2010-08-05 00:49:44 +04:00
|
|
|
class BlacklistedUsername(amo.models.ModelBase):
|
|
|
|
"""Blacklisted user usernames."""
|
|
|
|
username = models.CharField(max_length=255, unique=True, default='')
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
db_table = 'users_blacklistedusername'
|
2010-07-13 07:46:23 +04:00
|
|
|
|
|
|
|
def __unicode__(self):
|
2010-08-05 00:49:44 +04:00
|
|
|
return self.username
|
2010-07-13 07:46:23 +04:00
|
|
|
|
|
|
|
@classmethod
|
2010-08-05 00:49:44 +04:00
|
|
|
def blocked(cls, username):
|
|
|
|
"""Check to see if a username is in the (cached) blacklist."""
|
2010-08-05 11:37:22 +04:00
|
|
|
qs = cls.objects.all()
|
2010-08-05 00:49:44 +04:00
|
|
|
f = lambda: [u.lower() for u in qs.values_list('username', flat=True)]
|
2010-08-05 11:37:22 +04:00
|
|
|
blacklist = caching.cached_with(qs, f, 'blocked')
|
2010-09-07 12:50:52 +04:00
|
|
|
return username.lower() in blacklist
|
|
|
|
|
|
|
|
|
|
|
|
class BlacklistedEmailDomain(amo.models.ModelBase):
|
|
|
|
"""Blacklisted user e-mail domains."""
|
|
|
|
domain = models.CharField(max_length=255, unique=True, default='',
|
|
|
|
blank=False)
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return self.domain
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def blocked(cls, domain):
|
|
|
|
qs = cls.objects.all()
|
|
|
|
f = lambda: list(qs.values_list('domain', flat=True))
|
|
|
|
blacklist = caching.cached_with(qs, f, 'blocked')
|
|
|
|
# because there isn't a good way to know if the domain is
|
|
|
|
# "example.com" or "example.co.jp", we'll re-construct it...
|
|
|
|
# so if it's "bad.example.co.jp", the following check the
|
|
|
|
# values in ['bad.example.co.jp', 'example.co.jp', 'co.jp']
|
|
|
|
x = domain.lower().split('.')
|
|
|
|
for d in ['.'.join(x[y:]) for y in range(len(x) - 1)]:
|
|
|
|
if d in blacklist:
|
|
|
|
return True
|
2010-08-05 04:29:03 +04:00
|
|
|
|
|
|
|
|
2011-05-27 22:23:28 +04:00
|
|
|
class BlacklistedPassword(amo.models.ModelBase):
|
|
|
|
"""Blacklisted passwords"""
|
|
|
|
password = models.CharField(max_length=255, unique=True, blank=False)
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return self.password
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def blocked(cls, password):
|
|
|
|
return cls.objects.filter(password=password)
|
2011-10-04 23:35:40 +04:00
|
|
|
|
|
|
|
|
|
|
|
class UserHistory(amo.models.ModelBase):
|
2011-10-07 20:43:13 +04:00
|
|
|
email = models.EmailField()
|
2011-10-04 23:35:40 +04:00
|
|
|
user = models.ForeignKey(UserProfile, related_name='history')
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
db_table = 'users_history'
|
|
|
|
ordering = ('-created',)
|
|
|
|
|
|
|
|
|
|
|
|
@UserProfile.on_change
|
|
|
|
def watch_email(old_attr={}, new_attr={}, instance=None,
|
|
|
|
sender=None, **kw):
|
|
|
|
new_email, old_email = new_attr.get('email'), old_attr.get('email')
|
|
|
|
if old_email and new_email != old_email:
|
|
|
|
log.debug('Creating user history for user: %s' % instance.pk)
|
|
|
|
UserHistory.objects.create(email=old_email, user_id=instance.pk)
|