diff --git a/src/backend/api/v1/profile/constants.py b/src/backend/api/v1/profile/constants.py index 15558d2..61ead87 100644 --- a/src/backend/api/v1/profile/constants.py +++ b/src/backend/api/v1/profile/constants.py @@ -11,7 +11,5 @@ "span", # разрешенные теги котоыре не нужно очищать ] -ALLOWED_ATTRIBUTES_BY_FRONT = { - "span": ["class", "style", "contenteditable"], - # разрещенные атрибуты. -} +ALLOWED_ATTRIBUTES_BY_FRONT = ["class", "style", "contenteditable"] +# разрещенные атрибуты. diff --git a/src/backend/api/v1/profile/serializers.py b/src/backend/api/v1/profile/serializers.py index f0b7ab0..299c084 100644 --- a/src/backend/api/v1/profile/serializers.py +++ b/src/backend/api/v1/profile/serializers.py @@ -1,7 +1,8 @@ import html from typing import ClassVar, Optional -import bleach +from bleach.css_sanitizer import CSSSanitizer +from bleach.sanitizer import Cleaner from django.core.validators import RegexValidator from django.db.models import Q from rest_framework import serializers @@ -14,11 +15,10 @@ ProfessionSerializer, SkillSerializer, ) - -# from api.v1.profile.constants import ( -# ALLOWED_ATTRIBUTES_BY_FRONT, -# ALLOWED_TAGS_BY_FRONT, -# ) +from api.v1.profile.constants import ( + ALLOWED_ATTRIBUTES_BY_FRONT, + ALLOWED_TAGS_BY_FRONT, +) from apps.general.constants import MAX_SKILLS, MAX_SKILLS_MESSAGE from apps.general.models import Profession from apps.profile.constants import MAX_SPECIALISTS, MAX_SPECIALISTS_MESSAGE @@ -31,6 +31,12 @@ USERNAME_REGEX, ) +cleaner = Cleaner( + tags=ALLOWED_TAGS_BY_FRONT, + attributes=ALLOWED_ATTRIBUTES_BY_FRONT, + css_sanitizer=CSSSanitizer(), +) + class CurrentProfile(serializers.CurrentUserDefault): """Получение профиля текущего пользователя.""" @@ -270,6 +276,7 @@ def validate_about(self, value): Метод валидации и защиты от потенциально вредоносных HTML-тегов и атрибутов. """ - - safe_about = bleach.clean(value) + safe_about = cleaner.clean( + value, + ) # защита потенциально вредоносных HTML-тегов и атрибутов return safe_about diff --git a/src/backend/api/v1/projects/serializers.py b/src/backend/api/v1/projects/serializers.py index ab36f2c..8e0f5f0 100644 --- a/src/backend/api/v1/projects/serializers.py +++ b/src/backend/api/v1/projects/serializers.py @@ -2,7 +2,8 @@ from itertools import chain from typing import Any, ClassVar, Dict, Optional, OrderedDict, Tuple -import bleach +from bleach.css_sanitizer import CSSSanitizer +from bleach.sanitizer import Cleaner from django.db import transaction from rest_framework import serializers @@ -36,6 +37,12 @@ ProjectSpecialist, ) +cleaner = Cleaner( + tags=ALLOWED_TAGS_BY_FRONT, + attributes=ALLOWED_ATTRIBUTES_BY_FRONT, + css_sanitizer=CSSSanitizer(), +) + class DirectionSerializer(CustomModelSerializer): """Сериализатор направления разработки.""" @@ -229,10 +236,8 @@ def validate_description(self, value): HTML-тегов и атрибутов. """ - safe_description = bleach.clean( + safe_description = cleaner.clean( value, - tags=ALLOWED_TAGS_BY_FRONT, - attributes=ALLOWED_ATTRIBUTES_BY_FRONT, ) return safe_description @@ -406,10 +411,8 @@ class Meta: fields: ClassVar[Tuple[str, ...]] = ("cover_letter",) def validate_cover_letter(self, value): - escaped_html = bleach.clean( + escaped_html = cleaner.clean( value, - tags=ALLOWED_TAGS_BY_FRONT, - attributes=ALLOWED_ATTRIBUTES_BY_FRONT, ) # защита потенциально вредоносных HTML-тегов и атрибутов return escaped_html @@ -543,10 +546,8 @@ def validate(self, attrs) -> Dict[str, Any]: return attrs def validate_cover_letter(self, value): - escaped_html = bleach.clean( + escaped_html = cleaner.clean( value, - tags=ALLOWED_TAGS_BY_FRONT, - attributes=ALLOWED_ATTRIBUTES_BY_FRONT, ) # защита потенциально вредоносных HTML-тегов и атрибутов return escaped_html @@ -589,14 +590,12 @@ def to_representation(self, instance): def get_is_favorite_profile(self, obj) -> bool: """ - Метод возвращает True если Владелец добавил участника в избранное. - В противном случе возвращает False. + Метод возвращает True если владелец добавил участника в избранное. + Иначе False. """ user = self.context.get("request").user if user.is_authenticated: - return obj.request_participants.favorited_by.filter( - id=user.pk - ).exists() + return obj.user.profile.favorited_by.filter(id=user.pk).exists() return False def get_request_status(self, obj) -> str: @@ -754,10 +753,8 @@ def validate(self, attrs) -> OrderedDict: return attrs def validate_cover_letter(self, value): - escaped_html = bleach.clean( + escaped_html = cleaner.clean( value, - tags=ALLOWED_TAGS_BY_FRONT, - attributes=ALLOWED_ATTRIBUTES_BY_FRONT, ) # защита потенциально вредоносных HTML-тегов и атрибутов return escaped_html diff --git a/src/backend/apps/profile/constants.py b/src/backend/apps/profile/constants.py index 3c1ac8b..f67d424 100644 --- a/src/backend/apps/profile/constants.py +++ b/src/backend/apps/profile/constants.py @@ -16,7 +16,7 @@ REGEX_PROFILE_NAME = r"^[a-zA-Zа-яА-ЯёЁ-]+$" REGEX_PROFILE_NAME_MESSAGE = "Введите кириллицу или латиницу" REGEX_PROFILE_ABOUT = ( - r"^[\wа-яА-ЯёЁ0-9\s<>,.?!'\"/\-+:;@#\$%\^&\*\(\)\[\]\{\}]*$" + r"^[\wа-яА-ЯёЁ0-9\s<>,.?!'\"/\-+:;@#\$%\^&\*\(\)\[\]\{\}]*$|^<.*>$" ) REGEX_PROFILE_ABOUT_MESSAGE = "Введите кириллицу или латиницу" MAX_SPECIALISTS = 2 diff --git a/src/backend/apps/profile/migrations/0011_alter_profile_about_alter_profile_name.py b/src/backend/apps/profile/migrations/0011_alter_profile_about_alter_profile_name.py new file mode 100644 index 0000000..400fe6b --- /dev/null +++ b/src/backend/apps/profile/migrations/0011_alter_profile_about_alter_profile_name.py @@ -0,0 +1,46 @@ +# Generated by Django 5.0.1 on 2025-01-06 11:38 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("profile", "0010_add_trigramextension"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="about", + field=models.TextField( + blank=True, + max_length=1500, + validators=[ + django.core.validators.RegexValidator( + message="Введите кириллицу или латиницу", + regex="^[\\wа-яА-ЯёЁ0-9\\s<>,.?!'\\\"/\\-+:;@#\\$%\\^&\\*\\(\\)\\[\\]\\{\\}]*$|^<.*>$", + ), + django.core.validators.MinLengthValidator(limit_value=20), + ], + verbose_name="О себе", + ), + ), + migrations.AlterField( + model_name="profile", + name="name", + field=models.CharField( + max_length=30, + validators=[ + django.core.validators.RegexValidator( + message="Введите кириллицу или латиницу", + regex="^[a-zA-Zа-яА-ЯёЁ-]+$", + ), + django.core.validators.MinLengthValidator( + limit_value=2, message="Должно быть минимум символов" + ), + ], + verbose_name="Имя", + ), + ), + ] diff --git a/src/backend/apps/projects/migrations/0021_alter_invitationtoproject_answer_and_more.py b/src/backend/apps/projects/migrations/0021_alter_invitationtoproject_answer_and_more.py new file mode 100644 index 0000000..bb58bf8 --- /dev/null +++ b/src/backend/apps/projects/migrations/0021_alter_invitationtoproject_answer_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.1 on 2025-01-06 11:38 + +import apps.general.fields +import django.core.validators +import re +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "projects", + "0020_remove_invitationtoproject_projects_invitationtoproject_unique_request_per_project_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="invitationtoproject", + name="answer", + field=apps.general.fields.BaseTextField( + blank=True, + max_length=750, + null=True, + validators=[ + django.core.validators.RegexValidator( + flags=re.RegexFlag["IGNORECASE"], + message="Поле может содержать: кириллические и латинские символы, цифры и спецсимовлы -!#$%%&'*+/=?^_;():@,.<>`{}~«»", + regex="(^[-%!#$&*'+/=?^_;():@,.<>`{|}~\\\"\\\\\\-«»0-9A-ZА-ЯЁ\\s]+)\\Z", + ), + django.core.validators.MinLengthValidator( + limit_value=5, + message="Длина поля от 5 до 750 символов.", + ), + ], + verbose_name="Ответ", + ), + ), + migrations.AlterField( + model_name="participationrequest", + name="answer", + field=apps.general.fields.BaseTextField( + blank=True, + max_length=750, + null=True, + validators=[ + django.core.validators.RegexValidator( + flags=re.RegexFlag["IGNORECASE"], + message="Поле может содержать: кириллические и латинские символы, цифры и спецсимовлы -!#$%%&'*+/=?^_;():@,.<>`{}~«»", + regex="(^[-%!#$&*'+/=?^_;():@,.<>`{|}~\\\"\\\\\\-«»0-9A-ZА-ЯЁ\\s]+)\\Z", + ), + django.core.validators.MinLengthValidator( + limit_value=5, + message="Длина поля от 5 до 750 символов.", + ), + ], + verbose_name="Ответ", + ), + ), + ] diff --git a/src/backend/apps/projects/models.py b/src/backend/apps/projects/models.py index e5d7b45..3487567 100644 --- a/src/backend/apps/projects/models.py +++ b/src/backend/apps/projects/models.py @@ -264,6 +264,7 @@ class ParticipationRequestAndInvitationBasemodel(CreatedModifiedFields): answer = BaseTextField( verbose_name="Ответ", null=True, + blank=True, ) class Meta: