Merge pull request #153 from Zeecka/feature/image_deletion
Removing Images and Password - closing #149
This commit is contained in:
@@ -4,6 +4,7 @@ MAX_PENDING_TIME=600
|
|||||||
MAX_STORE_TIME=259200
|
MAX_STORE_TIME=259200
|
||||||
CLEAR_AT_RESTART=1
|
CLEAR_AT_RESTART=1
|
||||||
SKIP_IHDR_FILL=0
|
SKIP_IHDR_FILL=0
|
||||||
|
REMOVAL_MIN_AGE_SECONDS=300
|
||||||
|
|
||||||
# Flask configuration
|
# Flask configuration
|
||||||
FLASK_DEBUG=0
|
FLASK_DEBUG=0
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# Python
|
|
||||||
.env
|
.env
|
||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
results/
|
results/
|
||||||
.vscode
|
removed_images/
|
||||||
|
|||||||
+150
-1
@@ -6,6 +6,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -20,7 +21,13 @@ from .utils.sentry import initialize_sentry
|
|||||||
|
|
||||||
initialize_sentry()
|
initialize_sentry()
|
||||||
|
|
||||||
from .config import IMAGE_EXTENSIONS, RESULT_FOLDER, WORKER_FILES
|
from .config import (
|
||||||
|
IMAGE_EXTENSIONS,
|
||||||
|
REMOVAL_MIN_AGE_SECONDS,
|
||||||
|
REMOVED_IMAGES_FOLDER,
|
||||||
|
RESULT_FOLDER,
|
||||||
|
WORKER_FILES,
|
||||||
|
)
|
||||||
from .models import Image, Submission, UploadLog, cleanup_old_entries, db
|
from .models import Image, Submission, UploadLog, cleanup_old_entries, db
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +224,8 @@ def create_app() -> Flask:
|
|||||||
"last_submission_date": image.last_submission_date,
|
"last_submission_date": image.last_submission_date,
|
||||||
"upload_count": image.upload_count,
|
"upload_count": image.upload_count,
|
||||||
"passwords": passwords,
|
"passwords": passwords,
|
||||||
|
"removal_min_age_seconds": REMOVAL_MIN_AGE_SECONDS,
|
||||||
|
"submission_date": submission.date,
|
||||||
}
|
}
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
@@ -251,6 +260,146 @@ def create_app() -> Flask:
|
|||||||
|
|
||||||
return send_file(output_file, as_attachment=True)
|
return send_file(output_file, as_attachment=True)
|
||||||
|
|
||||||
|
@app.route("/remove/<hash_val>", methods=["POST"])
|
||||||
|
def remove_image(hash_val: str) -> tuple[Response, int]:
|
||||||
|
"""Remove an image and associated results if criteria are met."""
|
||||||
|
|
||||||
|
submission = Submission.query.get_or_404(hash_val) # type: ignore
|
||||||
|
image = Image.query.get_or_404(submission.image_hash) # type: ignore
|
||||||
|
|
||||||
|
# Check if image is at least 5 minutes old
|
||||||
|
current_time = time.time()
|
||||||
|
age_seconds = current_time - submission.date
|
||||||
|
if age_seconds < REMOVAL_MIN_AGE_SECONDS:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"error": "Image must be at least 5 minutes old. "
|
||||||
|
f"Current age: {int(age_seconds)}s"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all IPs that uploaded this image
|
||||||
|
upload_logs = UploadLog.query.filter_by(image_hash=image.hash).all()
|
||||||
|
unique_ips = set()
|
||||||
|
for log in upload_logs:
|
||||||
|
if log.ip_address:
|
||||||
|
unique_ips.add(log.ip_address)
|
||||||
|
|
||||||
|
# Check if only one IP has uploaded this image
|
||||||
|
if len(unique_ips) > 1:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"error": "Image uploaded from multiple IP addresses. Removal is not "
|
||||||
|
"allowed.",
|
||||||
|
"ip_count": len(unique_ips),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create archive directory if it doesn't exist
|
||||||
|
REMOVED_IMAGES_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Archive the original image (copy to removed_images folder with metadata)
|
||||||
|
original_image_path = Path(image.file)
|
||||||
|
if original_image_path.exists():
|
||||||
|
dt = datetime.now(timezone.utc).isoformat()
|
||||||
|
archive_filename = f"{image.hash}_{submission.hash}_{dt}{original_image_path.suffix}"
|
||||||
|
archive_path = REMOVED_IMAGES_FOLDER / archive_filename
|
||||||
|
try:
|
||||||
|
shutil.copy2(original_image_path, archive_path)
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
return jsonify({"error": "Failed to archive image"}), 500
|
||||||
|
|
||||||
|
# Delete the results folder
|
||||||
|
result_path = RESULT_FOLDER / str(image.hash) / str(submission.hash)
|
||||||
|
try:
|
||||||
|
if result_path.exists():
|
||||||
|
shutil.rmtree(result_path)
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
return jsonify({"error": "Failed to delete results"}), 500
|
||||||
|
|
||||||
|
# Delete submission from database
|
||||||
|
try:
|
||||||
|
db.session.delete(submission) # pylint: disable=no-member
|
||||||
|
|
||||||
|
# If this was the last submission for this image, delete the image entry too
|
||||||
|
if len(image.submissions) <= 1:
|
||||||
|
# Also delete the original image file
|
||||||
|
if original_image_path.exists():
|
||||||
|
original_image_path.unlink()
|
||||||
|
db.session.delete(image) # pylint: disable=no-member
|
||||||
|
|
||||||
|
db.session.commit() # pylint: disable=no-member
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback() # pylint: disable=no-member
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
return jsonify({"error": "Failed to update database"}), 500
|
||||||
|
|
||||||
|
# Note: UploadLog entries are intentionally kept for audit purposes
|
||||||
|
|
||||||
|
return jsonify({"message": "Image successfully removed"}), 200
|
||||||
|
|
||||||
|
@app.route("/remove_password/<hash_val>", methods=["POST"])
|
||||||
|
def remove_password(hash_val: str) -> tuple[Response, int]:
|
||||||
|
"""Remove a password from a submission if criteria are met."""
|
||||||
|
|
||||||
|
submission = Submission.query.get_or_404(hash_val) # type: ignore
|
||||||
|
|
||||||
|
if not submission.password:
|
||||||
|
return jsonify({"error": "No password to remove"}), 400
|
||||||
|
|
||||||
|
# Check if submission is at least 5 minutes old
|
||||||
|
current_time = time.time()
|
||||||
|
age_seconds = current_time - submission.date
|
||||||
|
if age_seconds < REMOVAL_MIN_AGE_SECONDS:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"error": f"Submission must be at least 5 minutes old. "
|
||||||
|
f"Current age: {int(age_seconds)}s"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all IPs that uploaded this specific submission (password combination)
|
||||||
|
upload_logs = UploadLog.query.filter_by(submission_hash=submission.hash).all()
|
||||||
|
unique_ips = set()
|
||||||
|
for log in upload_logs:
|
||||||
|
if log.ip_address:
|
||||||
|
unique_ips.add(log.ip_address)
|
||||||
|
|
||||||
|
# Check if only one IP has uploaded this submission
|
||||||
|
if len(unique_ips) > 1:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"error": "Submission uploaded from multiple IP addresses. "
|
||||||
|
"Password removal is not allowed.",
|
||||||
|
"ip_count": len(unique_ips),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the password
|
||||||
|
try:
|
||||||
|
submission.password = None
|
||||||
|
db.session.commit() # pylint: disable=no-member
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback() # pylint: disable=no-member
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
return jsonify({"error": "Failed to remove password"}), 500
|
||||||
|
|
||||||
|
return jsonify({"message": "Password successfully removed"}), 200
|
||||||
|
|
||||||
@app.route("/image/<img_name>")
|
@app.route("/image/<img_name>")
|
||||||
@app.route("/image/<hash_val>/<img_name>")
|
@app.route("/image/<hash_val>/<img_name>")
|
||||||
def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response:
|
def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"]
|
|||||||
WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize", "openstego", "pcrt"]
|
WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize", "openstego", "pcrt"]
|
||||||
|
|
||||||
RESULT_FOLDER = Path(__file__).parent.resolve() / "results"
|
RESULT_FOLDER = Path(__file__).parent.resolve() / "results"
|
||||||
|
REMOVED_IMAGES_FOLDER = Path(__file__).parent.resolve() / "removed_images"
|
||||||
|
REMOVAL_MIN_AGE_SECONDS = int(getenv("REMOVAL_MIN_AGE_SECONDS", "300")) # 5 minutes
|
||||||
MAX_PENDING_TIME = int(getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default
|
MAX_PENDING_TIME = int(getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default
|
||||||
MAX_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default
|
MAX_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default
|
||||||
CLEAR_AT_RESTART = int(getenv("CLEAR_AT_RESTART", "0"))
|
CLEAR_AT_RESTART = int(getenv("CLEAR_AT_RESTART", "0"))
|
||||||
|
|||||||
@@ -162,6 +162,52 @@ form {
|
|||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Removal card */
|
||||||
|
.removal-card {
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 2px solid var(--theme-green);
|
||||||
|
background-color: rgba(159, 239, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.removal-card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removal-card p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removal-card button:disabled {
|
||||||
|
background-color: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removal-card #removal-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Password removal styling */
|
||||||
|
.password-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-delete-icon {
|
||||||
|
transition: color 0.2s, transform 0.2s;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-delete-icon:hover {
|
||||||
|
color: #ff0000 !important;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* ======== Forms & Inputs ======== */
|
/* ======== Forms & Inputs ======== */
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
|
|||||||
@@ -224,11 +224,235 @@ async function fetchImageInfo(submission_hash) {
|
|||||||
infoData.size
|
infoData.size
|
||||||
)}</td></tr>`;
|
)}</td></tr>`;
|
||||||
tableInfos.innerHTML += `<tr><td><i class="fa fa-upload"></i> Upload count:</td><td>${infoData.upload_count}</td></tr>`;
|
tableInfos.innerHTML += `<tr><td><i class="fa fa-upload"></i> Upload count:</td><td>${infoData.upload_count}</td></tr>`;
|
||||||
if (Array.isArray(infoData.passwords)) {
|
if (Array.isArray(infoData.passwords) && infoData.passwords.length > 0) {
|
||||||
const passwordList = infoData.passwords
|
await displayPasswordsWithRemoval(tableInfos, infoData.passwords, submission_hash, infoData);
|
||||||
.map((pwd) => `<code>${escapeHtml(pwd)}</code>`)
|
}
|
||||||
.join(", ");
|
|
||||||
tableInfos.innerHTML += `<tr><td><i class="fa fa-key"></i> Common password(s):</td><td>${passwordList}</td></tr>`;
|
// Fetch removal status and update removal UI
|
||||||
|
await updateRemovalUI(submission_hash, infoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function displayPasswordsWithRemoval(tableInfos, passwords, submission_hash, infoData) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const tdLabel = document.createElement("td");
|
||||||
|
const tdContent = document.createElement("td");
|
||||||
|
|
||||||
|
tdLabel.innerHTML = '<i class="fa fa-key"></i> Common password(s):';
|
||||||
|
tr.appendChild(tdLabel);
|
||||||
|
tr.appendChild(tdContent);
|
||||||
|
tableInfos.appendChild(tr);
|
||||||
|
|
||||||
|
// Clear existing password timer if any
|
||||||
|
if (passwordTimerIntervals[submission_hash]) {
|
||||||
|
clearInterval(passwordTimerIntervals[submission_hash]);
|
||||||
|
delete passwordTimerIntervals[submission_hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadTime = infoData.submission_date * 1000;
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
|
||||||
|
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
|
||||||
|
|
||||||
|
for (const pwd of passwords) {
|
||||||
|
const passwordSpan = document.createElement("span");
|
||||||
|
passwordSpan.className = "password-item";
|
||||||
|
passwordSpan.innerHTML = `<code>${escapeHtml(pwd)}</code>`;
|
||||||
|
|
||||||
|
// Check if this password can be removed
|
||||||
|
const canRemove = ageSeconds >= REMOVAL_MIN_AGE_SECONDS;
|
||||||
|
|
||||||
|
if (canRemove) {
|
||||||
|
const deleteIcon = document.createElement("i");
|
||||||
|
deleteIcon.className = "fas fa-times password-delete-icon";
|
||||||
|
deleteIcon.title = "Remove password";
|
||||||
|
deleteIcon.dataset.submissionHash = submission_hash;
|
||||||
|
deleteIcon.dataset.password = pwd;
|
||||||
|
deleteIcon.style.cursor = "pointer";
|
||||||
|
deleteIcon.style.marginLeft = "5px";
|
||||||
|
deleteIcon.style.color = "#ff4d4d";
|
||||||
|
|
||||||
|
deleteIcon.addEventListener("click", async function() {
|
||||||
|
await handlePasswordRemoval(submission_hash, deleteIcon);
|
||||||
|
});
|
||||||
|
|
||||||
|
passwordSpan.appendChild(deleteIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
tdContent.appendChild(passwordSpan);
|
||||||
|
tdContent.appendChild(document.createTextNode(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up timer to refresh when passwords become eligible for removal
|
||||||
|
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
|
||||||
|
const remainingSeconds = REMOVAL_MIN_AGE_SECONDS - ageSeconds;
|
||||||
|
passwordTimerIntervals[submission_hash] = setInterval(async () => {
|
||||||
|
const newUploadTime = infoData.submission_date * 1000;
|
||||||
|
const newCurrentTime = new Date().getTime();
|
||||||
|
const newAgeSeconds = Math.floor((newCurrentTime - newUploadTime) / 1000);
|
||||||
|
|
||||||
|
if (newAgeSeconds >= REMOVAL_MIN_AGE_SECONDS) {
|
||||||
|
// Time has elapsed, refresh the password display
|
||||||
|
clearInterval(passwordTimerIntervals[submission_hash]);
|
||||||
|
delete passwordTimerIntervals[submission_hash];
|
||||||
|
|
||||||
|
// Re-fetch info and refresh display
|
||||||
|
const infoResp = await fetch(`/infos/${submission_hash}`);
|
||||||
|
const newInfoData = await infoResp.json();
|
||||||
|
|
||||||
|
// Clear and rebuild password row
|
||||||
|
tr.remove();
|
||||||
|
await displayPasswordsWithRemoval(tableInfos, newInfoData.passwords, submission_hash, newInfoData);
|
||||||
|
}
|
||||||
|
}, Math.min(remainingSeconds * 1000, 1000)); // Check every second or at the exact time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkPasswordRemovalEligibility(submission_hash, password, infoData) {
|
||||||
|
try {
|
||||||
|
// Check age constraint using submission date, not first_submission_date
|
||||||
|
const uploadTime = infoData.submission_date * 1000; // Convert to milliseconds
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
|
||||||
|
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
|
||||||
|
|
||||||
|
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking password removal eligibility:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePasswordRemoval(submission_hash, iconElement) {
|
||||||
|
try {
|
||||||
|
iconElement.style.pointerEvents = "none";
|
||||||
|
iconElement.style.opacity = "0.5";
|
||||||
|
|
||||||
|
const response = await fetch(`/remove_password/${submission_hash}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Remove the password item from display
|
||||||
|
const passwordSpan = iconElement.closest(".password-item");
|
||||||
|
if (passwordSpan) {
|
||||||
|
passwordSpan.style.transition = "opacity 0.3s";
|
||||||
|
passwordSpan.style.opacity = "0";
|
||||||
|
setTimeout(() => passwordSpan.remove(), 300);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Silently fail - just re-enable the icon
|
||||||
|
iconElement.style.pointerEvents = "auto";
|
||||||
|
iconElement.style.opacity = "1";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Password removal error:", error);
|
||||||
|
iconElement.style.pointerEvents = "auto";
|
||||||
|
iconElement.style.opacity = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image removal functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
let removalTimerIntervals = {};
|
||||||
|
let passwordTimerIntervals = {};
|
||||||
|
|
||||||
|
async function updateRemovalUI(submission_hash, infoData) {
|
||||||
|
const resultRemovalDiv = document.getElementById("result-removal");
|
||||||
|
resultRemovalDiv.innerHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Extract upload timestamp from first_submission_date
|
||||||
|
const uploadTime = new Date(infoData.first_submission_date).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
|
||||||
|
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
|
||||||
|
|
||||||
|
const removalContainer = document.createElement("div");
|
||||||
|
removalContainer.className = "card removal-card";
|
||||||
|
resultRemovalDiv.appendChild(removalContainer);
|
||||||
|
|
||||||
|
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
|
||||||
|
const remainingSeconds = REMOVAL_MIN_AGE_SECONDS - ageSeconds;
|
||||||
|
removalContainer.innerHTML = `
|
||||||
|
<h3><i class="fa fa-trash"></i> Remove Image</h3>
|
||||||
|
<p>Available in <span id="removal-countdown">${remainingSeconds}</span> seconds</p>
|
||||||
|
<button class="btn btn-primary" disabled id="remove-btn">Remove Image</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Clear existing interval if any
|
||||||
|
if (removalTimerIntervals[submission_hash]) {
|
||||||
|
clearInterval(removalTimerIntervals[submission_hash]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start countdown timer
|
||||||
|
removalTimerIntervals[submission_hash] = setInterval(() => {
|
||||||
|
const countdownEl = document.getElementById("removal-countdown");
|
||||||
|
if (countdownEl) {
|
||||||
|
const current = parseInt(countdownEl.textContent);
|
||||||
|
if (current > 1) {
|
||||||
|
countdownEl.textContent = current - 1;
|
||||||
|
} else {
|
||||||
|
clearInterval(removalTimerIntervals[submission_hash]);
|
||||||
|
updateRemovalUI(submission_hash, infoData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// Check for multiple IPs - we'll do this by attempting removal
|
||||||
|
removalContainer.innerHTML = `
|
||||||
|
<h3><i class="fa fa-trash"></i> Remove Image</h3>
|
||||||
|
<button class="btn btn-primary" id="remove-btn">Remove Image</button>
|
||||||
|
<p id="removal-status"></p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const removeBtn = document.getElementById("remove-btn");
|
||||||
|
removeBtn.addEventListener("click", async () => {
|
||||||
|
removeBtn.disabled = true;
|
||||||
|
const statusEl = document.getElementById("removal-status");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/remove/${submission_hash}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
statusEl.innerHTML = `<span class="text-success"><i class="fa fa-check"></i> Image successfully removed</span>`;
|
||||||
|
removeBtn.style.display = "none";
|
||||||
|
setTimeout(() => { document.location = '/'; }, 2000);
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
if (data.error.includes("multiple IP")) {
|
||||||
|
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-warning"></i> ${escapeHtml(data.error)}</span>`;
|
||||||
|
} else {
|
||||||
|
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-warning"></i> ${escapeHtml(data.error)}</span>`;
|
||||||
|
}
|
||||||
|
removeBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-exclamation-circle"></i> Error: ${escapeHtml(data.error)}</span>`;
|
||||||
|
removeBtn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-exclamation-circle"></i> Network error occurred</span>`;
|
||||||
|
removeBtn.disabled = false;
|
||||||
|
console.error("Removal error:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating removal UI:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,5 +34,6 @@
|
|||||||
<div id="result" class="mt-5">
|
<div id="result" class="mt-5">
|
||||||
<div id="result-infos" class="mb-3"></div>
|
<div id="result-infos" class="mb-3"></div>
|
||||||
<div id="result-analyzers" class="mb-3"></div>
|
<div id="result-analyzers" class="mb-3"></div>
|
||||||
|
<div id="result-removal" class="mb-3"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<div id="result" class="mt-5">
|
<div id="result" class="mt-5">
|
||||||
<div id="result-infos" class="mb-3"></div>
|
<div id="result-infos" class="mb-3"></div>
|
||||||
<div id="result-analyzers" class="mb-3"></div>
|
<div id="result-analyzers" class="mb-3"></div>
|
||||||
|
<div id="result-removal" class="mb-3"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer_content %}
|
{% block footer_content %}
|
||||||
|
|||||||
Reference in New Issue
Block a user