diff --git a/Web/app.py b/Web/app.py index 0d18962..6cc2388 100755 --- a/Web/app.py +++ b/Web/app.py @@ -9961,25 +9961,28 @@ def fetch_book_info(isbn): app.logger.error(f"Error fetching book data: {e}") return jsonify({"error": f"Failed to fetch book information"}), 500 + @app.route('/download_book_cover', methods=['POST']) def download_book_cover(): """ - API endpoint to download and save a book cover image from URL + API endpoint to download a book cover image from URL + and save it directly to MongoDB GridFS. """ if 'username' not in session: return jsonify({"error": "Not authorized"}), 403 + current_permissions = us.get_effective_permissions(session['username']) - + if not current_permissions['actions'].get('can_insert', False): - flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') - return redirect(url_for('library_view')) + return jsonify({"error": "Ihnen fehlen die nötigen Berechtigungen."}), 403 + if not cfg.MODULES.is_enabled('library'): return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 - + try: data = request.get_json() image_url = data.get('url') - + if not image_url: return jsonify({"error": "No image URL provided"}), 400 @@ -9987,71 +9990,69 @@ def download_book_cover(): if parsed_url.scheme != 'https' or not parsed_url.netloc: return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400 - # 2. SSRF Protection: Strict Allowlist Check - # if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: - # return jsonify({"error": "Target host is not an allowed book cover provider"}), 403 - - # Download the image (allow_redirects=False prevents redirecting to internal IPs) response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False) - + if response.status_code != 200: return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400 - - # Check content type + content_type = response.headers.get('content-type', '') allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'] - + if not any(allowed_type in content_type.lower() for allowed_type in allowed_types): return jsonify({ "error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF" }), 400 - # Check content length header content_length = response.headers.get('Content-Length') if content_length: try: if int(content_length) > 5 * 1024 * 1024: - return jsonify({"error": "Image is too large"}), 413 + return jsonify({"error": "Image is too large (max 5MB)"}), 413 except ValueError: pass - - # Generate a fully unique filename + unique_id = str(uuid.uuid4()) timestamp = time.strftime("%Y%m%d%H%M%S") - + extension = '.jpg' # default if 'image/png' in content_type.lower(): extension = '.png' elif 'image/gif' in content_type.lower(): extension = '.gif' - + filename = f"book_cover_{unique_id}_{timestamp}{extension}" - filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) - - # Save image in chunks (prevents memory exhaustion and enforces size limits) - with open(filepath, 'wb') as f: - written = 0 - for chunk in response.iter_content(chunk_size=8192): - written += len(chunk) - if written > 5 * 1024 * 1024: - # Clean up the partial file before aborting - os.remove(filepath) - return jsonify({"error": "Image is too large"}), 413 - f.write(chunk) - + + image_data = io.BytesIO() + written = 0 + + for chunk in response.iter_content(chunk_size=8192): + written += len(chunk) + if written > 5 * 1024 * 1024: + return jsonify({"error": "Image is too large (max 5MB)"}), 413 + image_data.write(chunk) + + image_data.seek(0) + + fs = get_gridfs() + fs.put( + image_data, + filename=filename, + content_type=content_type + ) + return jsonify({ "success": True, "filename": filename, - "message": "Image downloaded successfully" + "message": "Image downloaded and stored directly in database" }) - + except requests.exceptions.RequestException as e: app.logger.error(f"Network error downloading book cover: {e}") return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500 except Exception as e: app.logger.error(f"Error downloading book cover: {e}") - # Fixed syntax here: Removed the injected HTML that was appended to this line - return jsonify({"error": f"Failed to download image"}), 500 + return jsonify({"error": "Failed to download image"}), 500 + """ @app.route('/proxy_image') def proxy_image(): diff --git a/Web/templates/upload_admin.html b/Web/templates/upload_admin.html index edf12a9..3c33787 100755 --- a/Web/templates/upload_admin.html +++ b/Web/templates/upload_admin.html @@ -876,12 +876,14 @@ -
- + +
+
+
+
Erlaubte Formate: JPG, JPEG, PNG, GIF
-
@@ -1669,23 +1671,24 @@ }, 3000); } - // Function to download book cover image function downloadBookCover(imageUrl) { if (!imageUrl) { console.log('No image URL provided'); return; } - - // Show loading indicator for image download - const imagePreviewContainer = document.getElementById('image-preview-container'); - if (imagePreviewContainer) { - const loadingDiv = document.createElement('div'); - loadingDiv.className = 'image-loading'; - loadingDiv.innerHTML = '
Buchcover wird heruntergeladen...
'; - imagePreviewContainer.appendChild(loadingDiv); + + const coverPreviewContainer = document.getElementById('book-cover-preview-container'); + + if (!coverPreviewContainer) { + console.error('Error: "book-cover-preview-container" not found in the DOM.'); + return; } - - // Download the image via backend + + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'image-loading'; + loadingDiv.innerHTML = '
Buchcover wird heruntergeladen...
'; + coverPreviewContainer.appendChild(loadingDiv); + fetch('/download_book_cover', { method: 'POST', headers: { @@ -1693,78 +1696,63 @@ }, body: JSON.stringify({ url: imageUrl }) }) - .then(response => response.json()) - .then(data => { - // Remove loading indicator - const loadingDiv = imagePreviewContainer?.querySelector('.image-loading'); - if (loadingDiv) { - loadingDiv.remove(); - } - - if (data.success) { - // Create a preview of the downloaded image - const imagePreview = document.createElement('div'); - imagePreview.className = 'book-cover-preview'; - imagePreview.innerHTML = ` -
- Buchcover -

Buchcover automatisch heruntergeladen

- - -
- `; - - if (imagePreviewContainer) { - imagePreviewContainer.appendChild(imagePreview); + .then(response => response.json()) + .then(data => { + const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading'); + if (currentLoadingDiv) { + currentLoadingDiv.remove(); } - - console.log('Book cover downloaded successfully:', data.filename); - } else { - console.error('Failed to download book cover:', data.error); - // Show error message to user - if (imagePreviewContainer) { - const errorDiv = document.createElement('div'); - errorDiv.className = 'error-message'; - errorDiv.textContent = 'Fehler beim Herunterladen des Buchcovers: ' + data.error; - errorDiv.style.fontSize = '0.8em'; - errorDiv.style.padding = '5px'; - errorDiv.style.marginTop = '5px'; - imagePreviewContainer.appendChild(errorDiv); - - // Remove error message after 5 seconds - setTimeout(() => errorDiv.remove(), 5000); + + if (data.success) { + coverPreviewContainer.innerHTML = ''; + + const imagePreview = document.createElement('div'); + imagePreview.className = 'book-cover-preview'; + imagePreview.innerHTML = ` +
+ Buchcover +

Buchcover automatisch heruntergeladen

+ + +
+ `; + + coverPreviewContainer.appendChild(imagePreview); + console.log('Book cover downloaded successfully:', data.filename); + } else { + console.error('Failed to download book cover:', data.error); + showCoverError(coverPreviewContainer, 'Fehler beim Herunterladen des Buchcovers: ' + data.error); } - } - }) - .catch(error => { - console.error('Error downloading book cover:', error); - // Remove loading indicator - const loadingDiv = imagePreviewContainer?.querySelector('.image-loading'); - if (loadingDiv) { - loadingDiv.remove(); - } - - // Show error message - if (imagePreviewContainer) { - const errorDiv = document.createElement('div'); - errorDiv.className = 'error-message'; - errorDiv.textContent = 'Netzwerkfehler beim Herunterladen des Buchcovers'; - errorDiv.style.fontSize = '0.8em'; - errorDiv.style.padding = '5px'; - errorDiv.style.marginTop = '5px'; - imagePreviewContainer.appendChild(errorDiv); - - // Remove error message after 5 seconds - setTimeout(() => errorDiv.remove(), 5000); - } - }); + }) + .catch(error => { + console.error('Error downloading book cover:', error); + + const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading'); + if (currentLoadingDiv) { + currentLoadingDiv.remove(); + } + + showCoverError(coverPreviewContainer, 'Netzwerkfehler beim Herunterladen des Buchcovers'); + }); } - - // Function to remove downloaded book cover + + function showCoverError(container, message) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.textContent = message; + errorDiv.style.fontSize = '0.8em'; + errorDiv.style.color = 'red'; + errorDiv.style.padding = '5px'; + errorDiv.style.marginTop = '5px'; + container.appendChild(errorDiv); + + setTimeout(() => errorDiv.remove(), 5000); + } + function removeBookCover(button) { const previewItem = button.closest('.preview-item'); if (previewItem) { @@ -1772,7 +1760,6 @@ } } - // Code validation functions function checkCodeUnique(code, excludeId, callback) { if (!code || code.trim() === '') { callback(true);