Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created and implemented Overlay face function #3241

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
issue_count,
like_issue,
newhome,
process_bug_image,
remove_user_from_issue,
resolve,
save_issue,
Expand Down Expand Up @@ -853,6 +854,7 @@
path("owasp/", TemplateView.as_view(template_name="owasp.html"), name="owasp"),
path("batch-send-bacon-tokens/", batch_send_bacon_tokens_view, name="batch_send_bacon_tokens"),
path("pending-transactions/", pending_transactions_view, name="pending_transactions"),
path("process_bug_image", process_bug_image, name="process_bug_image"),
]

if settings.DEBUG:
Expand Down
271 changes: 167 additions & 104 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ aiohttp = "^3.11.11"
drf-yasg = "^1.21.8"
slack-bolt = "^1.22.0"
tld = "0.13"
scout-apm = "^3.3.0"
tsu-ki marked this conversation as resolved.
Show resolved Hide resolved
newrelic = "^10.4.0"
opencv-python = "^4.8.0"
numpy = "^1.24.0"

[tool.poetry.group.dev.dependencies]
black = "^24.8.0"
Expand Down
39 changes: 36 additions & 3 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from enum import Enum
from urllib.parse import urlparse

import cv2
import numpy as np
import requests
from annoying.fields import AutoOneToOneField
from captcha.fields import CaptchaField
Expand All @@ -31,6 +33,8 @@
from mdeditor.fields import MDTextField
from rest_framework.authtoken.models import Token

from .views.privacy import overlay_faces

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -293,12 +297,41 @@ def __str__(self):

def validate_image(fieldfile_obj):
try:
fieldfile_obj.file.seek(0)

# Read image data
filesize = fieldfile_obj.file.size
except:
filesize = fieldfile_obj.size
np_img = np.frombuffer(fieldfile_obj.file.read(), np.uint8)
img = cv2.imdecode(np_img, cv2.IMREAD_UNCHANGED)

if img is None:
raise ValueError("Failed to decode image")

# Apply face blurring
img_with_faces_hidden = overlay_faces(img)

# Encode back to file
ext = os.path.splitext(fieldfile_obj.name)[1].lower()
if ext in [".jpg", ".jpeg"]:
_, buffer = cv2.imencode(".jpg", img_with_faces_hidden, [cv2.IMWRITE_JPEG_QUALITY, 90])
else:
_, buffer = cv2.imencode(".png", img_with_faces_hidden)

# Reset file with processed image
fieldfile_obj.file = ContentFile(buffer.tobytes())
fieldfile_obj.file.seek(0)

except ValidationError:
raise
except Exception:
logger.error("Image validation failed.")
return False

megabyte_limit = 3.0
if filesize > megabyte_limit * 1024 * 1024:
raise ValidationError("Max file size is %sMB" % str(megabyte_limit))
raise ValidationError(f"Max file size is {megabyte_limit}MB")

return True


class Hunt(models.Model):
Expand Down
58 changes: 43 additions & 15 deletions website/templates/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -454,26 +454,54 @@ <h2 class="text-2xl font-semibold leading-7 text-gray-900">{% trans "ADD TEAM ME
event.preventDefault();

}
function clearPreviews() {
manage_div.innerHTML = '';
}

screenshots.addEventListener('change', async (event) => {
clearPreviews();

const fileList = Array.from(event.target.files);
for (let file of fileList) {
let formData = new FormData();
formData.append("file", file);
let res = await fetch("/process_bug_image", {
method: "POST",
body: formData
});
if (!res.ok) {
console.log("Error processing file:", file.name);
continue;
}
let data = await res.json();
if (data.error) {
console.log("Error from server:", data.error);
continue;
}
let base64Img = data.image;
let safeName = file.name;
let safeNameDisplay = safeName.slice(0, 20) + (safeName.length > 20 ? "..." : "");

screenshots.addEventListener('change', (event) => {
// Build preview element
let fileDiv = document.createElement("div");
fileDiv.className = "w-full md:w-[300px] h-[180px] overflow-hidden rounded-lg";

const fileList = Array.from(event.target.files);
let titleDiv = document.createElement("div");
titleDiv.className = "w-full h-10 flex justify-center rounded-t-lg p-2 bg-gray-500";

fileList.map(file => {
let src = URL.createObjectURL(file);
let safeName = $("<div>").text(file.name).html();
let safeNameDisplay = safeName.slice(0, 20) + (safeName.length > 20 ? "..." : "");
// Use the safe name for display and in the onclick handler
let fileDiv = $("<div>").addClass("w-full md:w-[300px] h-[180px] overflow-hidden rounded-lg").attr("onclick", `previewFile('${safeName}')`);
let titleDiv = $("<div>").addClass("w-full h-10 flex justify-center rounded-t-lg p-2 bg-gray-500");
let titleP = $("<p>").addClass("text-xl text-white font-bold").text(safeNameDisplay);
let img = $("<img>").addClass("object-cover").attr("src", escapeHtml(src));
let titleP = document.createElement("p");
titleP.className = "text-xl text-white font-bold";
titleP.textContent = safeNameDisplay;
titleDiv.appendChild(titleP);

titleDiv.append(titleP);
fileDiv.append(titleDiv).append(img);
$("#files_manage").append(fileDiv);
})
let img = document.createElement("img");
img.src = "data:image/jpeg;base64," + base64Img;
img.className = "object-cover";

fileDiv.appendChild(titleDiv);
fileDiv.appendChild(img);
manage_div.appendChild(fileDiv);
}
});


Expand Down
148 changes: 128 additions & 20 deletions website/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import io
import json
import os
import time
import uuid
from datetime import datetime, timezone
from urllib.parse import urlparse

import cv2
import numpy as np
import requests
import six
from allauth.account.models import EmailAddress
Expand All @@ -16,11 +19,13 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.core.cache import cache
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.mail import send_mail
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import transaction
from django.db.models import Count, Prefetch, Q, Sum
from django.db.transaction import atomic
from django.dispatch import receiver
Expand Down Expand Up @@ -69,6 +74,8 @@
safe_redirect_request,
)

from .privacy import overlay_faces


@login_required(login_url="/accounts/login")
def like_issue(request, issue_pk):
Expand Down Expand Up @@ -333,25 +340,61 @@ def newhome(request, template="new_home.html"):
return render(request, template, context)


@transaction.atomic
def delete_issue(request, id):
"""Delete an issue and all related objects"""
tokenauth = False

# Check for token in POST
token_key = request.POST.get("token")

# Rate limiting check for 1 minute
cache_key = f"delete_issue_{request.user.id if request.user.is_authenticated else 'anon'}"
if cache.get(cache_key):
messages.error(request, "Please wait before deleting another issue")
return redirect("/")
cache.set(cache_key, True, 60)

# Token authentication
if token_key:
try:
token = Token.objects.get(key=token_key)
request.user = User.objects.get(id=token.user_id)
tokenauth = True
except (Token.DoesNotExist, User.DoesNotExist):
tokenauth = False

try:
# TODO: Refactor this for a direct query instead of looping through all tokens
for token in Token.objects.all():
if request.POST["token"] == token.key:
request.user = User.objects.get(id=token.user_id)
tokenauth = True
except Token.DoesNotExist:
tokenauth = False
issue = Issue.objects.get(id=id)
except Issue.DoesNotExist:
messages.error(request, "Issue not found")
return redirect("/")

issue = Issue.objects.get(id=id)
if request.user.is_superuser or request.user == issue.user or tokenauth:
screenshots = issue.screenshots.all()
for screenshot in screenshots:
screenshot.delete()
issue.delete()
messages.success(request, "Issue deleted")
try:
with transaction.atomic():
Comment.objects.filter(content_type=ContentType.objects.get_for_model(Issue), object_id=id).delete()
Points.objects.filter(issue=issue).delete()
Activity.objects.filter(content_type=ContentType.objects.get_for_model(Issue), object_id=id).delete()

screenshots = issue.screenshots.all()
for screenshot in screenshots:
screenshot.delete()
issue.delete()

messages.success(request, "Issue deleted successfully")
if tokenauth:
return JsonResponse({"status": "success", "message": "Issue deleted successfully"})

except Exception as e:
messages.error(request, "Error deleting issue (see logs)")
if tokenauth:
return JsonResponse({"status": "error", "message": "An internal error occurred."}, status=500)
else:
messages.error(request, "Permission denied")
if tokenauth:
return JsonResponse("Deleted", safe=False)
return JsonResponse({"status": "error", "message": "Permission denied"}, status=403)

return redirect("/")


Expand Down Expand Up @@ -959,13 +1002,47 @@ def create_issue(self, form):
save=True,
)

# Save screenshots
for screenshot in self.request.FILES.getlist("screenshots"):
filename = screenshot.name
extension = filename.split(".")[-1]
screenshot.name = (filename[:10] + str(uuid.uuid4()))[:40] + "." + extension
default_storage.save(f"screenshots/{screenshot.name}", screenshot)
IssueScreenshot.objects.create(image=f"screenshots/{screenshot.name}", issue=obj)
try:
screenshot.seek(0)

image_data = screenshot.read()
np_img = np.frombuffer(image_data, np.uint8)
img = cv2.imdecode(np_img, cv2.IMREAD_UNCHANGED)

if img is None:
raise ValueError("Failed to decode image")

img_with_faces_hidden = overlay_faces(img)

filename = screenshot.name
extension = filename.split(".")[-1].lower()

if extension not in ["jpg", "jpeg", "png"]:
extension = "jpg"

new_filename = f"{filename[:10]}_{uuid.uuid4()}_{int(time.time())}.{extension}"

if extension in ["jpg", "jpeg"]:
_, buffer = cv2.imencode(".jpg", img_with_faces_hidden, [cv2.IMWRITE_JPEG_QUALITY, 90])
else:
_, buffer = cv2.imencode(".png", img_with_faces_hidden)

if buffer is None:
raise ValueError("Failed to encode processed image")

processed_image = ContentFile(buffer.tobytes())
saved_path = default_storage.save(f"screenshots/{new_filename}", processed_image)

IssueScreenshot.objects.create(image=saved_path, issue=obj)

except Exception as e:
messages.error(self.request, f"Error processing image: {str(e)}")
return render(
self.request,
"report.html",
{"form": self.get_form(), "captcha_form": CaptchaForm()},
)

# Handle team members
team_members_id = [
Expand Down Expand Up @@ -1583,3 +1660,34 @@ def flag_issue(request, issue_pk):

def select_bid(request):
return render(request, "bid_selection.html")


@csrf_exempt
def process_bug_image(request):
if request.method == "POST":
file = request.FILES.get("file")
if not file:
return JsonResponse({"error": "No file provided"}, status=400)
try:
import cv2
import numpy as np

from .privacy import overlay_faces

in_memory_file = file.read()
file_array = np.frombuffer(in_memory_file, np.uint8)
img = cv2.imdecode(file_array, cv2.IMREAD_UNCHANGED)
if img is None:
return JsonResponse({"error": "Could not decode image"}, status=400)

processed_img = overlay_faces(img)

ret, buffer = cv2.imencode(".jpg", processed_img, [cv2.IMWRITE_JPEG_QUALITY, 90])
if not ret:
return JsonResponse({"error": "Failed to encode processed image"}, status=500)

base64_data = base64.b64encode(buffer).decode("utf-8")
return JsonResponse({"image": base64_data}, status=200)
except Exception as e:
return JsonResponse({"error": "An internal error occurred."}, status=500)
return JsonResponse({"error": "Only POST allowed"}, status=405)
Loading
Loading