user.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. from hashlib import md5
  2. from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
  3. from django.contrib.auth.models import UserManager as BaseUserManager
  4. from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
  5. from django.contrib.auth.password_validation import validate_password
  6. from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
  7. from django.core.mail import send_mail
  8. from django.db import IntegrityError, models, transaction
  9. from django.urls import reverse
  10. from django.utils import timezone
  11. from django.utils.translation import gettext_lazy as _
  12. from misago.acl.models import Role
  13. from misago.conf import settings
  14. from misago.core.pgutils import PgPartialIndex
  15. from misago.core.utils import slugify
  16. from misago.users import avatars
  17. from misago.users.audittrail import create_user_audit_trail
  18. from misago.users.signatures import is_user_signature_valid
  19. from misago.users.utils import hash_email
  20. from .online import Online
  21. from .rank import Rank
  22. class UserManager(BaseUserManager):
  23. def _create_user(self, username, email, password, **extra_fields):
  24. """
  25. Create and save a user with the given username, email, and password.
  26. """
  27. if not username:
  28. raise ValueError("User must have an username.")
  29. if not email:
  30. raise ValueError("User must have an email address.")
  31. user = self.model(**extra_fields)
  32. user.set_username(username)
  33. user.set_email(email)
  34. user.set_password(password)
  35. if not 'rank' in extra_fields:
  36. user.rank = Rank.objects.get_default()
  37. now = timezone.now()
  38. user.last_login=now
  39. user.joined_on=now
  40. user.save(using=self._db)
  41. self._assert_user_has_authenticated_role(user)
  42. Online.objects.create(user=user, last_click=now)
  43. return user
  44. def _assert_user_has_authenticated_role(self, user):
  45. authenticated_role = Role.objects.get(special_role='authenticated')
  46. if authenticated_role not in user.roles.all():
  47. user.roles.add(authenticated_role)
  48. user.update_acl_key()
  49. user.save(update_fields=['acl_key'])
  50. def create_user(self, username, email=None, password=None, **extra_fields):
  51. extra_fields.setdefault('is_staff', False)
  52. extra_fields.setdefault('is_superuser', False)
  53. return self._create_user(username, email, password, **extra_fields)
  54. def create_superuser(self, username, email, password=None, **extra_fields):
  55. extra_fields.setdefault('is_staff', True)
  56. extra_fields.setdefault('is_superuser', True)
  57. if extra_fields.get('is_staff') is not True:
  58. raise ValueError('Superuser must have is_staff=True.')
  59. if extra_fields.get('is_superuser') is not True:
  60. raise ValueError('Superuser must have is_superuser=True.')
  61. user = self._create_user(username, email, password, **extra_fields)
  62. try:
  63. user.rank = Rank.objects.get(name=_("Forum team"))
  64. user.update_acl_key()
  65. except Rank.DoesNotExist:
  66. pass
  67. return user
  68. def get_by_username(self, username):
  69. return self.get(slug=slugify(username))
  70. def get_by_email(self, email):
  71. return self.get(email_hash=hash_email(email))
  72. def get_by_username_or_email(self, login):
  73. if '@' in login:
  74. return self.get(email_hash=hash_email(login))
  75. return self.get(slug=slugify(login))
  76. class User(AbstractBaseUser, PermissionsMixin):
  77. ACTIVATION_NONE = 0
  78. ACTIVATION_USER = 1
  79. ACTIVATION_ADMIN = 2
  80. SUBSCRIPTION_NONE = 0
  81. SUBSCRIPTION_NOTIFY = 1
  82. SUBSCRIPTION_ALL = 2
  83. SUBSCRIPTION_CHOICES = [
  84. (SUBSCRIPTION_NONE, _("No")),
  85. (SUBSCRIPTION_NOTIFY, _("Notify")),
  86. (SUBSCRIPTION_ALL, _("Notify with e-mail")),
  87. ]
  88. LIMIT_INVITES_TO_NONE = 0
  89. LIMIT_INVITES_TO_FOLLOWED = 1
  90. LIMIT_INVITES_TO_NOBODY = 2
  91. LIMIT_INVITES_TO_CHOICES = [
  92. (LIMIT_INVITES_TO_NONE, _("Everybody")),
  93. (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
  94. (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
  95. ]
  96. # Note that "username" field is purely for shows.
  97. # When searching users by their names, always use lowercased string
  98. # and slug field instead that is normalized around DB engines
  99. # differences in case handling.
  100. username = models.CharField(max_length=30)
  101. slug = models.CharField(max_length=30, unique=True)
  102. # Misago stores user email in two fields:
  103. # "email" holds normalized email address
  104. # "email_hash" is lowercase hash of email address used to identify account
  105. # as well as enforcing on database level that no more than one user can be
  106. # using one email address
  107. email = models.EmailField(max_length=255, db_index=True)
  108. email_hash = models.CharField(max_length=32, unique=True)
  109. joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
  110. joined_from_ip = models.GenericIPAddressField(null=True, blank=True)
  111. is_hiding_presence = models.BooleanField(default=False)
  112. rank = models.ForeignKey(
  113. 'Rank',
  114. null=True,
  115. blank=True,
  116. on_delete=models.deletion.PROTECT,
  117. )
  118. title = models.CharField(max_length=255, null=True, blank=True)
  119. requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
  120. is_staff = models.BooleanField(
  121. _('staff status'),
  122. default=False,
  123. help_text=_('Designates whether the user can log into admin sites.'),
  124. )
  125. roles = models.ManyToManyField('misago_acl.Role')
  126. acl_key = models.CharField(max_length=12, null=True, blank=True)
  127. is_active = models.BooleanField(
  128. _('active'),
  129. db_index=True,
  130. default=True,
  131. help_text=_(
  132. "Designates whether this user should be treated as active. "
  133. "Unselect this instead of deleting accounts."
  134. ),
  135. )
  136. is_active_staff_message = models.TextField(null=True, blank=True)
  137. is_deleting_account = models.BooleanField(default=False)
  138. avatar_tmp = models.ImageField(
  139. max_length=255,
  140. upload_to=avatars.store.upload_to,
  141. null=True,
  142. blank=True,
  143. )
  144. avatar_src = models.ImageField(
  145. max_length=255,
  146. upload_to=avatars.store.upload_to,
  147. null=True,
  148. blank=True,
  149. )
  150. avatar_crop = models.CharField(max_length=255, null=True, blank=True)
  151. avatars = JSONField(null=True, blank=True)
  152. is_avatar_locked = models.BooleanField(default=False)
  153. avatar_lock_user_message = models.TextField(null=True, blank=True)
  154. avatar_lock_staff_message = models.TextField(null=True, blank=True)
  155. signature = models.TextField(null=True, blank=True)
  156. signature_parsed = models.TextField(null=True, blank=True)
  157. signature_checksum = models.CharField(max_length=64, null=True, blank=True)
  158. is_signature_locked = models.BooleanField(default=False)
  159. signature_lock_user_message = models.TextField(null=True, blank=True)
  160. signature_lock_staff_message = models.TextField(null=True, blank=True)
  161. followers = models.PositiveIntegerField(default=0)
  162. following = models.PositiveIntegerField(default=0)
  163. follows = models.ManyToManyField(
  164. 'self',
  165. related_name='followed_by',
  166. symmetrical=False,
  167. )
  168. blocks = models.ManyToManyField(
  169. 'self',
  170. related_name='blocked_by',
  171. symmetrical=False,
  172. )
  173. limits_private_thread_invites_to = models.PositiveIntegerField(
  174. default=LIMIT_INVITES_TO_NONE,
  175. choices=LIMIT_INVITES_TO_CHOICES,
  176. )
  177. unread_private_threads = models.PositiveIntegerField(default=0)
  178. sync_unread_private_threads = models.BooleanField(default=False)
  179. subscribe_to_started_threads = models.PositiveIntegerField(
  180. default=SUBSCRIPTION_NONE,
  181. choices=SUBSCRIPTION_CHOICES,
  182. )
  183. subscribe_to_replied_threads = models.PositiveIntegerField(
  184. default=SUBSCRIPTION_NONE,
  185. choices=SUBSCRIPTION_CHOICES,
  186. )
  187. threads = models.PositiveIntegerField(default=0)
  188. posts = models.PositiveIntegerField(default=0, db_index=True)
  189. last_posted_on = models.DateTimeField(null=True, blank=True)
  190. profile_fields = HStoreField(default=dict)
  191. agreements = ArrayField(models.PositiveIntegerField(), default=list)
  192. USERNAME_FIELD = 'slug'
  193. REQUIRED_FIELDS = ['email']
  194. objects = UserManager()
  195. class Meta:
  196. indexes = [
  197. PgPartialIndex(
  198. fields=['is_staff'],
  199. where={'is_staff': True},
  200. ),
  201. PgPartialIndex(
  202. fields=['requires_activation'],
  203. where={'requires_activation__gt': 0},
  204. ),
  205. PgPartialIndex(
  206. fields=['is_deleting_account'],
  207. where={'is_deleting_account': True},
  208. ),
  209. ]
  210. def clean(self):
  211. self.username = self.normalize_username(self.username)
  212. self.email = UserManager.normalize_email(self.email)
  213. def lock(self):
  214. """locks user in DB, shortcut for locking user model in views"""
  215. return User.objects.select_for_update().get(pk=self.pk)
  216. def delete(self, *args, **kwargs):
  217. if kwargs.pop('delete_content', False):
  218. self.delete_content()
  219. self.anonymize_data()
  220. avatars.delete_avatar(self)
  221. return super().delete(*args, **kwargs)
  222. def delete_content(self):
  223. from misago.users.signals import delete_user_content
  224. delete_user_content.send(sender=self)
  225. def mark_for_delete(self):
  226. self.is_active = False
  227. self.is_deleting_account = True
  228. self.save(update_fields=['is_active', 'is_deleting_account'])
  229. def anonymize_data(self):
  230. """Replaces username with anonymized one, then send anonymization signal.
  231. Items associated with this user then anonymize their user-specific data
  232. like username or IP addresses.
  233. """
  234. self.username = settings.MISAGO_ANONYMOUS_USERNAME
  235. self.slug = slugify(self.username)
  236. from misago.users.signals import anonymize_user_data
  237. anonymize_user_data.send(sender=self)
  238. @property
  239. def requires_activation_by_admin(self):
  240. return self.requires_activation == self.ACTIVATION_ADMIN
  241. @property
  242. def requires_activation_by_user(self):
  243. return self.requires_activation == self.ACTIVATION_USER
  244. @property
  245. def can_be_messaged_by_everyone(self):
  246. preference = self.limits_private_thread_invites_to
  247. return preference == self.LIMIT_INVITES_TO_NONE
  248. @property
  249. def can_be_messaged_by_followed(self):
  250. preference = self.limits_private_thread_invites_to
  251. return preference == self.LIMIT_INVITES_TO_FOLLOWED
  252. @property
  253. def can_be_messaged_by_nobody(self):
  254. preference = self.limits_private_thread_invites_to
  255. return preference == self.LIMIT_INVITES_TO_NOBODY
  256. @property
  257. def has_valid_signature(self):
  258. return is_user_signature_valid(self)
  259. def get_absolute_url(self):
  260. return reverse(
  261. 'misago:user', kwargs={
  262. 'slug': self.slug,
  263. 'pk': self.pk,
  264. }
  265. )
  266. def get_username(self):
  267. """dirty hack: return real username instead of normalized slug"""
  268. return self.username
  269. def get_full_name(self):
  270. return self.username
  271. def get_short_name(self):
  272. return self.username
  273. def get_real_name(self):
  274. return self.profile_fields.get('real_name')
  275. def set_username(self, new_username, changed_by=None):
  276. new_username = self.normalize_username(new_username)
  277. if new_username != self.username:
  278. old_username = self.username
  279. self.username = new_username
  280. self.slug = slugify(new_username)
  281. if self.pk:
  282. changed_by = changed_by or self
  283. namechange = self.record_name_change(
  284. changed_by, new_username, old_username
  285. )
  286. from misago.users.signals import username_changed
  287. username_changed.send(sender=self)
  288. return namechange
  289. def record_name_change(self, changed_by, new_username, old_username):
  290. return self.namechanges.create(
  291. new_username=new_username,
  292. old_username=old_username,
  293. changed_by=changed_by,
  294. changed_by_username=changed_by.username,
  295. )
  296. def set_email(self, new_email):
  297. self.email = UserManager.normalize_email(new_email)
  298. self.email_hash = hash_email(new_email)
  299. def get_any_title(self):
  300. return self.title or self.rank.title or self.rank.name
  301. def get_roles(self):
  302. roles_pks = []
  303. roles_dict = {}
  304. for role in self.roles.all():
  305. roles_pks.append(role.pk)
  306. role.origin = self
  307. roles_dict[role.pk] = role
  308. if self.rank:
  309. for role in self.rank.roles.all():
  310. if role.pk not in roles_pks:
  311. role.origin = self.rank
  312. roles_pks.append(role.pk)
  313. roles_dict[role.pk] = role
  314. return [roles_dict[r] for r in sorted(roles_pks)]
  315. def update_acl_key(self):
  316. roles_pks = []
  317. for role in self.get_roles():
  318. if role.origin == 'self':
  319. roles_pks.append('u%s' % role.pk)
  320. else:
  321. roles_pks.append('%s:%s' % (self.rank.pk, role.pk))
  322. self.acl_key = md5(','.join(roles_pks).encode()).hexdigest()[:12]
  323. def email_user(self, subject, message, from_email=None, **kwargs):
  324. """sends an email to this user (for compat with Django)"""
  325. send_mail(subject, message, from_email, [self.email], **kwargs)
  326. def is_following(self, user_or_id):
  327. try:
  328. user_id = user_or_id.id
  329. except AttributeError:
  330. user_id = user_or_id
  331. try:
  332. self.follows.get(id=user_id)
  333. return True
  334. except User.DoesNotExist:
  335. return False
  336. def is_blocking(self, user_or_id):
  337. try:
  338. user_id = user_or_id.id
  339. except AttributeError:
  340. user_id = user_or_id
  341. try:
  342. self.blocks.get(id=user_id)
  343. return True
  344. except User.DoesNotExist:
  345. return False
  346. class UsernameChange(models.Model):
  347. user = models.ForeignKey(
  348. settings.AUTH_USER_MODEL,
  349. related_name='namechanges',
  350. on_delete=models.CASCADE,
  351. )
  352. changed_by = models.ForeignKey(
  353. settings.AUTH_USER_MODEL,
  354. null=True,
  355. blank=True,
  356. related_name='user_renames',
  357. on_delete=models.SET_NULL,
  358. )
  359. changed_by_username = models.CharField(max_length=30)
  360. changed_on = models.DateTimeField(default=timezone.now)
  361. new_username = models.CharField(max_length=255)
  362. old_username = models.CharField(max_length=255)
  363. class Meta:
  364. get_latest_by = "changed_on"
  365. def set_change_author(self, user):
  366. self.changed_by = user
  367. self.changed_by_username = user.username
  368. class AnonymousUser(DjangoAnonymousUser):
  369. acl_key = 'anonymous'
  370. @property
  371. def acl_cache(self):
  372. raise Exception("AnonymousUser.acl_cache has been removed")
  373. @acl_cache.setter
  374. def acl_cache(self, value):
  375. raise TypeError("AnonymousUser instances can't be made ACL aware")
  376. def get_roles(self):
  377. try:
  378. return [Role.objects.get(special_role="anonymous")]
  379. except Role.DoesNotExist:
  380. raise RuntimeError("Anonymous user role not found.")
  381. def update_acl_key(self):
  382. raise TypeError("Can't update ACL key on anonymous users")