bgneal@695: """Forms for the user_photos application."""
bgneal@749: import hashlib
bgneal@964: import os.path
bgneal@971: import urlparse
bgneal@700: 
bgneal@695: from django import forms
bgneal@697: from django.conf import settings
bgneal@695: 
bgneal@971: from core.download import download_file
bgneal@976: from core.functions import remove_file, TemporaryFile
bgneal@1195: from core.images.upload import process_upload
bgneal@701: from core.services import get_redis_connection
bgneal@696: from user_photos.models import Photo
bgneal@696: 
bgneal@695: 
bgneal@701: def rate_limit(key, limit, seconds):
bgneal@701:     """Use Redis to do a rate limit check. Returns True if the limit is violated
bgneal@701:     and False otherwise.
bgneal@701: 
bgneal@701:     key - the key to check in Redis
bgneal@701:     limit - the rate limit maximum value
bgneal@701:     seconds - the rate limit period in seconds
bgneal@701: 
bgneal@701:     """
bgneal@701:     conn = get_redis_connection()
bgneal@701:     val = conn.incr(key)
bgneal@701:     if val == 1:
bgneal@701:         conn.expire(key, seconds)
bgneal@701:     return val > limit
bgneal@701: 
bgneal@701: 
bgneal@971: def rate_limit_user(user):
bgneal@971:     """Shared function to rate limit user uploads."""
bgneal@971:     key = 'user_photos:counts:' + user.username
bgneal@971:     limit = settings.USER_PHOTOS_RATE_LIMIT
bgneal@971:     if rate_limit(key, *limit):
bgneal@971:         raise forms.ValidationError("You've exceeded your upload quota. "
bgneal@971:                 "Please try again later.")
bgneal@971: 
bgneal@971: 
bgneal@695: class UploadForm(forms.Form):
bgneal@695:     image_file = forms.ImageField()
bgneal@696: 
bgneal@696:     def __init__(self, *args, **kwargs):
bgneal@696:         self.user = kwargs.pop('user')
bgneal@696:         super(UploadForm, self).__init__(*args, **kwargs)
bgneal@696: 
bgneal@701:     def clean(self):
bgneal@701:         cleaned_data = super(UploadForm, self).clean()
bgneal@971:         rate_limit_user(self.user)
bgneal@701:         return cleaned_data
bgneal@701: 
bgneal@696:     def save(self):
bgneal@696:         """Processes the image and creates a new Photo object, which is saved to
bgneal@696:         the database. The new Photo instance is returned.
bgneal@696: 
bgneal@749:         Note that we do de-duplication. A signature is computed for the photo.
bgneal@749:         If the user has already uploaded a file with the same signature, that
bgneal@749:         photo object is returned instead.
bgneal@749: 
bgneal@696:         This function should only be called if is_valid() returns True.
bgneal@696: 
bgneal@696:         """
bgneal@749:         # Check for duplicate uploads from this user
bgneal@749:         signature = self._signature()
bgneal@749:         try:
bgneal@749:             return Photo.objects.get(user=self.user, signature=signature)
bgneal@749:         except Photo.DoesNotExist:
bgneal@749:             pass
bgneal@749: 
bgneal@964:         # Trying to use PIL (or Pillow) on a Django UploadedFile is often
bgneal@964:         # problematic because the file is often an in-memory file if it is under
bgneal@964:         # a certain size. This complicates matters and many of the operations we try
bgneal@964:         # to perform on it fail if this is the case. To get around these issues,
bgneal@964:         # we make a copy of the file on the file system and operate on the copy.
bgneal@700: 
bgneal@964:         fp = self.cleaned_data['image_file']
bgneal@964:         ext = os.path.splitext(fp.name)[1]
bgneal@964: 
bgneal@964:         # Write the UploadedFile to a temporary file on disk
bgneal@964:         with TemporaryFile(suffix=ext) as t:
bgneal@964:             for chunk in fp.chunks():
bgneal@964:                 t.file.write(chunk)
bgneal@964:             t.file.close()
bgneal@964: 
bgneal@1195:             url, thumb_url = process_upload(self.user, t.filename)
bgneal@700: 
bgneal@749:         photo = Photo(user=self.user, url=url, thumb_url=thumb_url,
bgneal@749:                 signature=signature)
bgneal@696:         photo.save()
bgneal@696:         return photo
bgneal@749: 
bgneal@749:     def _signature(self):
bgneal@749:         """Calculates and returns a signature for the image file as a hex digest
bgneal@749:         string.
bgneal@749: 
bgneal@749:         This function should only be called if is_valid() is True.
bgneal@749: 
bgneal@749:         """
bgneal@749:         fp = self.cleaned_data['image_file']
bgneal@749:         md5 = hashlib.md5()
bgneal@749:         for chunk in fp.chunks():
bgneal@749:             md5.update(chunk)
bgneal@749:         return md5.hexdigest()
bgneal@971: 
bgneal@971: 
bgneal@971: class HotLinkImageForm(forms.Form):
bgneal@971:     """Form for hot-linking images. If the supplied URL's host is on
bgneal@971:     a white-list, return it as-is. Otherwise attempt to download the image at
bgneal@971:     the given URL and upload it into our S3 storage. The URL to the new location
bgneal@971:     is returned.
bgneal@971:     """
bgneal@971:     url = forms.URLField()
bgneal@971: 
bgneal@971:     def __init__(self, *args, **kwargs):
bgneal@971:         self.user = kwargs.pop('user')
bgneal@971:         super(HotLinkImageForm, self).__init__(*args, **kwargs)
bgneal@971: 
bgneal@971:     def clean_url(self):
bgneal@971:         the_url = self.cleaned_data['url']
bgneal@971:         self.url_parts = urlparse.urlsplit(the_url)
bgneal@971:         if self.url_parts.scheme not in ['http', 'https']:
bgneal@971:             raise forms.ValidationError("Invalid URL scheme")
bgneal@971:         return the_url
bgneal@971: 
bgneal@971:     def clean(self):
bgneal@971:         cleaned_data = super(HotLinkImageForm, self).clean()
bgneal@971:         rate_limit_user(self.user)
bgneal@971:         return cleaned_data
bgneal@971: 
bgneal@971:     def save(self):
bgneal@971:         url = self.url_parts.geturl()
bgneal@971:         if (self.url_parts.scheme == 'https' and
bgneal@971:                 self.url_parts.hostname in settings.USER_IMAGES_SOURCES):
bgneal@971:             return url
bgneal@971: 
bgneal@971:         # Try to download the file
bgneal@976:         with remove_file(download_file(url)) as path:
bgneal@1195:             url, _ = process_upload(self.user, path)
bgneal@971: 
bgneal@971:         return url