import hashlib import re import warnings from pathlib import Path import jdatetime import magic from PIL import Image, UnidentifiedImageError from django.core.exceptions import ValidationError from django.utils import timezone from apps.attachments.models import AttachmentPolicy from .models import MAX_AMOUNT DIGIT_MAP = str.maketrans('۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩', '01234567890123456789') def digits(value): return str(value or '').translate(DIGIT_MAP).strip() def amount(value): value = digits(value) if not re.fullmatch(r'\d+|\d{1,3}(?:[,٬ ]\d{3})+', value): raise ValidationError('مبلغ را به ریال، فقط با عدد صحیح وارد کنید.') value = int(re.sub(r'[,٬ ]', '', value)) if not 0 < value <= MAX_AMOUNT: raise ValidationError('مبلغ باید بیشتر از صفر و در محدوده مجاز باشد.') return value def bank_number(value): value = re.sub(r'[\s-]', '', digits(value)) if value and not value.isascii(): raise ValidationError('شماره را با ارقام معتبر وارد کنید.') if value and not value.isdecimal(): raise ValidationError('شماره حساب یا کارت باید فقط عدد باشد.') return value def iban(value): value = re.sub(r'\s', '', digits(value)).upper() if not value: return '' if not re.fullmatch(r'IR\d{24}', value) or int(value[4:] + '1827' + value[2:4]) % 97 != 1: raise ValidationError('شماره شبای معتبر ایرانی وارد کنید؛ IR و ۲۴ رقم.') return value def paid_date(day, month, year): if not all((day, month, year)): raise ValidationError('تاریخ را کامل کنید؛ روز، ماه و سال لازم‌اند.') try: value = jdatetime.date(int(digits(year)), int(digits(month)), int(digits(day))).togregorian() except (ValueError, TypeError, OverflowError): raise ValidationError('تاریخ شمسی معتبر انتخاب کنید.') today = timezone.localdate() if value > today: raise ValidationError('تاریخ واریز نمی‌تواند بعد از امروز باشد.') if jdatetime.date.fromgregorian(date=value).year < jdatetime.date.fromgregorian(date=today).year - 10: raise ValidationError('تاریخ خارج از بازه ده سال گذشته است.') return value def image(upload): policy = AttachmentPolicy.objects.filter(attachment_type='IMAGE', is_enabled=True).first() if not policy: raise ValidationError('ارسال تصویر فعلاً فعال نیست؛ با کارشناس هماهنگ کنید.') if not upload.size or upload.size > policy.max_file_size: raise ValidationError('تصویر خالی یا بزرگ‌تر از حجم مجاز است.') ext = Path(upload.name).suffix.lower() if ext not in (policy.allowed_extensions or ['.jpg', '.jpeg', '.png', '.gif', '.webp']): raise ValidationError('فقط تصویر مجاز فیش را انتخاب کنید.') head = upload.read(8192) upload.seek(0) mime = magic.from_buffer(head, mime=True) expected = {'.jpg':'image/jpeg', '.jpeg':'image/jpeg', '.png':'image/png', '.gif':'image/gif', '.webp':'image/webp'} if expected.get(ext) != mime or (policy.allowed_mime_types and mime not in policy.allowed_mime_types): raise ValidationError('محتوای واقعی فایل با نوع تصویر سازگار نیست.') try: with warnings.catch_warnings(): warnings.simplefilter('error', Image.DecompressionBombWarning) with Image.open(upload) as picture: if picture.width * picture.height > 25_000_000: raise ValidationError('ابعاد تصویر بیش از حد مجاز است.') picture.verify() except (UnidentifiedImageError, OSError, SyntaxError, Image.DecompressionBombError, Image.DecompressionBombWarning): raise ValidationError('تصویر خراب است یا باز نمی‌شود؛ تصویر دیگری انتخاب کنید.') finally: upload.seek(0) digest = hashlib.sha256() for chunk in upload.chunks(): digest.update(chunk) upload.seek(0) return {'mime_type': mime, 'sha256': digest.hexdigest(), 'original_name': Path(upload.name).name[:255], 'file_size': upload.size}