improvements in processing the library item uploading
Release Inventarsystem / release-docker (push) Successful in 2m16s
Release Inventarsystem / release-docker (push) Successful in 2m16s
This commit is contained in:
+27
-26
@@ -9961,18 +9961,21 @@ def fetch_book_info(isbn):
|
|||||||
app.logger.error(f"Error fetching book data: {e}")
|
app.logger.error(f"Error fetching book data: {e}")
|
||||||
return jsonify({"error": f"Failed to fetch book information"}), 500
|
return jsonify({"error": f"Failed to fetch book information"}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/download_book_cover', methods=['POST'])
|
@app.route('/download_book_cover', methods=['POST'])
|
||||||
def download_book_cover():
|
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:
|
if 'username' not in session:
|
||||||
return jsonify({"error": "Not authorized"}), 403
|
return jsonify({"error": "Not authorized"}), 403
|
||||||
|
|
||||||
current_permissions = us.get_effective_permissions(session['username'])
|
current_permissions = us.get_effective_permissions(session['username'])
|
||||||
|
|
||||||
if not current_permissions['actions'].get('can_insert', False):
|
if not current_permissions['actions'].get('can_insert', False):
|
||||||
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
|
return jsonify({"error": "Ihnen fehlen die nötigen Berechtigungen."}), 403
|
||||||
return redirect(url_for('library_view'))
|
|
||||||
if not cfg.MODULES.is_enabled('library'):
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
||||||
|
|
||||||
@@ -9987,17 +9990,11 @@ def download_book_cover():
|
|||||||
if parsed_url.scheme != 'https' or not parsed_url.netloc:
|
if parsed_url.scheme != 'https' or not parsed_url.netloc:
|
||||||
return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400
|
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)
|
response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400
|
return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400
|
||||||
|
|
||||||
# Check content type
|
|
||||||
content_type = response.headers.get('content-type', '')
|
content_type = response.headers.get('content-type', '')
|
||||||
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||||
|
|
||||||
@@ -10006,16 +10003,14 @@ def download_book_cover():
|
|||||||
"error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF"
|
"error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Check content length header
|
|
||||||
content_length = response.headers.get('Content-Length')
|
content_length = response.headers.get('Content-Length')
|
||||||
if content_length:
|
if content_length:
|
||||||
try:
|
try:
|
||||||
if int(content_length) > 5 * 1024 * 1024:
|
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:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Generate a fully unique filename
|
|
||||||
unique_id = str(uuid.uuid4())
|
unique_id = str(uuid.uuid4())
|
||||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||||
|
|
||||||
@@ -10026,23 +10021,29 @@ def download_book_cover():
|
|||||||
extension = '.gif'
|
extension = '.gif'
|
||||||
|
|
||||||
filename = f"book_cover_{unique_id}_{timestamp}{extension}"
|
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)
|
image_data = io.BytesIO()
|
||||||
with open(filepath, 'wb') as f:
|
written = 0
|
||||||
written = 0
|
|
||||||
for chunk in response.iter_content(chunk_size=8192):
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
written += len(chunk)
|
written += len(chunk)
|
||||||
if written > 5 * 1024 * 1024:
|
if written > 5 * 1024 * 1024:
|
||||||
# Clean up the partial file before aborting
|
return jsonify({"error": "Image is too large (max 5MB)"}), 413
|
||||||
os.remove(filepath)
|
image_data.write(chunk)
|
||||||
return jsonify({"error": "Image is too large"}), 413
|
|
||||||
f.write(chunk)
|
image_data.seek(0)
|
||||||
|
|
||||||
|
fs = get_gridfs()
|
||||||
|
fs.put(
|
||||||
|
image_data,
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"message": "Image downloaded successfully"
|
"message": "Image downloaded and stored directly in database"
|
||||||
})
|
})
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
@@ -10050,8 +10051,8 @@ def download_book_cover():
|
|||||||
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
|
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error downloading book cover: {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": "Failed to download image"}), 500
|
||||||
return jsonify({"error": f"Failed to download image"}), 500
|
|
||||||
"""
|
"""
|
||||||
@app.route('/proxy_image')
|
@app.route('/proxy_image')
|
||||||
def proxy_image():
|
def proxy_image():
|
||||||
|
|||||||
@@ -876,12 +876,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Image upload -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="images">Bilder:</label>
|
<label>Buchcover (automatisch):</label>
|
||||||
|
<div id="book-cover-preview-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="images"> Bilder hinzufügen:</label>
|
||||||
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple>
|
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple>
|
||||||
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
|
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
|
||||||
<!-- Add image preview area -->
|
|
||||||
<div class="image-preview-container" id="image-preview-container"></div>
|
<div class="image-preview-container" id="image-preview-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1669,23 +1671,24 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to download book cover image
|
|
||||||
function downloadBookCover(imageUrl) {
|
function downloadBookCover(imageUrl) {
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
console.log('No image URL provided');
|
console.log('No image URL provided');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading indicator for image download
|
const coverPreviewContainer = document.getElementById('book-cover-preview-container');
|
||||||
const imagePreviewContainer = document.getElementById('image-preview-container');
|
|
||||||
if (imagePreviewContainer) {
|
if (!coverPreviewContainer) {
|
||||||
const loadingDiv = document.createElement('div');
|
console.error('Error: "book-cover-preview-container" not found in the DOM.');
|
||||||
loadingDiv.className = 'image-loading';
|
return;
|
||||||
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
|
|
||||||
imagePreviewContainer.appendChild(loadingDiv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download the image via backend
|
const loadingDiv = document.createElement('div');
|
||||||
|
loadingDiv.className = 'image-loading';
|
||||||
|
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
|
||||||
|
coverPreviewContainer.appendChild(loadingDiv);
|
||||||
|
|
||||||
fetch('/download_book_cover', {
|
fetch('/download_book_cover', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -1693,78 +1696,63 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ url: imageUrl })
|
body: JSON.stringify({ url: imageUrl })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Remove loading indicator
|
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
|
||||||
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
|
if (currentLoadingDiv) {
|
||||||
if (loadingDiv) {
|
currentLoadingDiv.remove();
|
||||||
loadingDiv.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
// Create a preview of the downloaded image
|
|
||||||
const imagePreview = document.createElement('div');
|
|
||||||
imagePreview.className = 'book-cover-preview';
|
|
||||||
imagePreview.innerHTML = `
|
|
||||||
<div class="preview-item">
|
|
||||||
<img src="{{ url_for('uploaded_file', filename='') }}${data.filename}"
|
|
||||||
alt="Buchcover" class="book-cover-thumbnail">
|
|
||||||
<p class="book-cover-caption">Buchcover automatisch heruntergeladen</p>
|
|
||||||
<input type="hidden" name="book_cover_image" value="${data.filename}">
|
|
||||||
<button type="button" onclick="removeBookCover(this)"
|
|
||||||
class="remove-book-cover-button">
|
|
||||||
Entfernen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (imagePreviewContainer) {
|
|
||||||
imagePreviewContainer.appendChild(imagePreview);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Book cover downloaded successfully:', data.filename);
|
if (data.success) {
|
||||||
} else {
|
coverPreviewContainer.innerHTML = '';
|
||||||
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
|
const imagePreview = document.createElement('div');
|
||||||
setTimeout(() => errorDiv.remove(), 5000);
|
imagePreview.className = 'book-cover-preview';
|
||||||
|
imagePreview.innerHTML = `
|
||||||
|
<div class="preview-item">
|
||||||
|
<img src="/uploads/${data.filename}"
|
||||||
|
alt="Buchcover" class="book-cover-thumbnail" style="max-width: 150px; border-radius: 4px;">
|
||||||
|
<p class="book-cover-caption" style="font-size: 0.9em; color: #555;">Buchcover automatisch heruntergeladen</p>
|
||||||
|
<input type="hidden" name="book_cover_image" value="${data.filename}">
|
||||||
|
<button type="button" onclick="removeBookCover(this)"
|
||||||
|
class="remove-book-cover-button btn btn-sm btn-danger">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 => {
|
||||||
.catch(error => {
|
console.error('Error downloading book cover:', error);
|
||||||
console.error('Error downloading book cover:', error);
|
|
||||||
// Remove loading indicator
|
|
||||||
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
|
|
||||||
if (loadingDiv) {
|
|
||||||
loadingDiv.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
|
||||||
if (imagePreviewContainer) {
|
if (currentLoadingDiv) {
|
||||||
const errorDiv = document.createElement('div');
|
currentLoadingDiv.remove();
|
||||||
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
|
showCoverError(coverPreviewContainer, 'Netzwerkfehler beim Herunterladen des Buchcovers');
|
||||||
setTimeout(() => errorDiv.remove(), 5000);
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
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 to remove downloaded book cover
|
|
||||||
function removeBookCover(button) {
|
function removeBookCover(button) {
|
||||||
const previewItem = button.closest('.preview-item');
|
const previewItem = button.closest('.preview-item');
|
||||||
if (previewItem) {
|
if (previewItem) {
|
||||||
@@ -1772,7 +1760,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code validation functions
|
|
||||||
function checkCodeUnique(code, excludeId, callback) {
|
function checkCodeUnique(code, excludeId, callback) {
|
||||||
if (!code || code.trim() === '') {
|
if (!code || code.trim() === '') {
|
||||||
callback(true);
|
callback(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user