Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
63 changes: 63 additions & 0 deletions application/single_app/route_backend_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,69 @@
'document_id': document_id
}), 200

@bp.route('/api/documents/extract_metadata', methods=['POST'])
@swagger_route(security=get_auth_security())

Check warning on line 1283 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@login_required

Check warning on line 1284 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@user_required

Check warning on line 1285 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@enabled_required("enable_user_workspace")
def api_extract_user_metadata_batch():
"""
POST /api/documents/extract_metadata
Queues background metadata extraction jobs for selected user documents.
"""
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

settings = get_settings()

Check warning on line 1296 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
if not settings.get('enable_extract_meta_data'):
return jsonify({'error': 'Metadata extraction not enabled'}), 403

payload = request.get_json(silent=True) or {}
document_ids = payload.get('document_ids')
if not isinstance(document_ids, list):
document_id = payload.get('document_id')
document_ids = [document_id] if document_id else []
document_ids = list(dict.fromkeys(
str(document_id).strip()
for document_id in document_ids
if str(document_id or '').strip()
))
if not document_ids:
return jsonify({'error': 'At least one document ID is required.'}), 400

queued = []
errors = []
for document_id in document_ids:
try:

Check warning on line 1316 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
document_item = get_document_metadata(document_id=document_id, user_id=user_id)
if not document_item:
errors.append({'document_id': document_id, 'error': 'Document not found.'})
continue
if document_item.get('user_id') != user_id:
errors.append({'document_id': document_id, 'error': 'Only the document owner can extract metadata for this document.'})
continue

current_app.extensions['executor'].submit_stored(
f"{document_id}_metadata",
process_metadata_extraction_background,
document_id=document_id,
user_id=user_id
)
queued.append({'document_id': document_id})
except Exception as e:

Check warning on line 1332 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
errors.append({'document_id': document_id, 'error': str(e)})

if queued:
invalidate_personal_search_cache(user_id)

Check warning on line 1336 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 1336 in application/single_app/route_backend_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.

status_code = 202 if queued and not errors else (207 if queued else 400)
return jsonify({
'message': f'Queued {len(queued)} document(s) for metadata extraction.',
'queued': queued,
'errors': errors,
}), status_code

@bp.route('/api/documents/reprocess_extraction', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
80 changes: 80 additions & 0 deletions application/single_app/route_backend_group_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,86 @@
'document_id': document_id
}), 200

@bp.route('/api/group_documents/extract_metadata', methods=['POST'])
@swagger_route(security=get_auth_security())

Check warning on line 1189 in application/single_app/route_backend_group_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@login_required

Check warning on line 1190 in application/single_app/route_backend_group_documents.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@user_required
@enabled_required("enable_group_workspaces")
def api_extract_group_metadata_batch():
"""
POST /api/group_documents/extract_metadata
Queues background metadata extraction jobs for selected group documents.
"""
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

settings = get_settings()
if not settings.get('enable_extract_meta_data'):
return jsonify({'error': 'Metadata extraction not enabled'}), 403

active_group_id, group_doc, _, error_response = _require_active_group_document_context(
user_id,
allowed_roles=("Owner", "Admin", "DocumentManager"),
permission_message='You do not have permission to extract metadata for group documents',
)
if error_response:
return error_response

allowed, reason = check_group_status_allows_operation(group_doc, 'upload')
if not allowed:
return jsonify({'error': reason}), 403

payload = request.get_json(silent=True) or {}
document_ids = payload.get('document_ids')
if not isinstance(document_ids, list):
document_id = payload.get('document_id')
document_ids = [document_id] if document_id else []
document_ids = list(dict.fromkeys(
str(document_id).strip()
for document_id in document_ids
if str(document_id or '').strip()
))
if not document_ids:
return jsonify({'error': 'At least one document ID is required.'}), 400

queued = []
errors = []
for document_id in document_ids:
try:
document_item = get_document_metadata(
document_id=document_id,
user_id=user_id,
group_id=active_group_id,
)
if not document_item:
errors.append({'document_id': document_id, 'error': 'Document not found.'})
continue
if document_item.get('group_id') != active_group_id:
errors.append({'document_id': document_id, 'error': 'Only documents in the active group can have metadata extracted.'})
continue

current_app.extensions['executor'].submit_stored(
f"{document_id}_group_metadata",
process_metadata_extraction_background,
document_id=document_id,
user_id=user_id,
group_id=active_group_id
)
queued.append({'document_id': document_id})
except Exception as e:
errors.append({'document_id': document_id, 'error': str(e)})

if queued:
invalidate_group_search_cache(active_group_id)

status_code = 202 if queued and not errors else (207 if queued else 400)
return jsonify({
'message': f'Queued {len(queued)} document(s) for metadata extraction.',
'queued': queued,
'errors': errors,
}), status_code

@bp.route('/api/group_documents/reprocess_extraction', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
75 changes: 75 additions & 0 deletions application/single_app/route_backend_public_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,81 @@ def api_extract_metadata_public_document(doc_id):
executor.submit(process_metadata_extraction_background, document_id=doc_id, user_id=user_id, public_workspace_id=active_ws)
return jsonify({'message':'Extraction queued'}), 200

@bp.route('/api/public_documents/extract_metadata', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
@enabled_required('enable_public_workspaces')
def api_extract_metadata_public_documents_batch():
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

settings = get_settings()
if not settings.get('enable_extract_meta_data'):
return jsonify({'error': 'Metadata extraction not enabled'}), 403

active_ws, ws_doc, _role, error_response = _require_active_public_workspace_response(
user_id,
PUBLIC_WORKSPACE_MANAGER_ROLES,
)
if error_response:
return error_response

allowed, reason = check_public_workspace_status_allows_operation(ws_doc, 'upload')
if not allowed:
return jsonify({'error': reason}), 403

payload = request.get_json(silent=True) or {}
document_ids = payload.get('document_ids')
if not isinstance(document_ids, list):
doc_id = payload.get('document_id')
document_ids = [doc_id] if doc_id else []
document_ids = list(dict.fromkeys(
str(document_id).strip()
for document_id in document_ids
if str(document_id or '').strip()
))
if not document_ids:
return jsonify({'error': 'At least one document ID is required.'}), 400

queued = []
errors = []
for document_id in document_ids:
try:
document_item = get_document_metadata(
document_id=document_id,
user_id=user_id,
public_workspace_id=active_ws,
)
if not document_item:
errors.append({'document_id': document_id, 'error': 'Document not found.'})
continue
if document_item.get('public_workspace_id') != active_ws:
errors.append({'document_id': document_id, 'error': 'Only documents in the active public workspace can have metadata extracted.'})
continue

current_app.extensions['executor'].submit_stored(
f"{document_id}_public_metadata",
process_metadata_extraction_background,
document_id=document_id,
user_id=user_id,
public_workspace_id=active_ws
)
queued.append({'document_id': document_id})
except Exception as e:
errors.append({'document_id': document_id, 'error': str(e)})

if queued:
invalidate_public_workspace_search_cache(active_ws)

status_code = 202 if queued and not errors else (207 if queued else 400)
return jsonify({
'message': f'Queued {len(queued)} document(s) for metadata extraction.',
'queued': queued,
'errors': errors,
}), status_code

@bp.route('/api/public_documents/reprocess_extraction', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
62 changes: 62 additions & 0 deletions application/single_app/static/js/public/public_workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,60 @@ function getPublicDeleteModalContent(documentCount) {
};
}

async function requestPublicSelectedMetadataExtraction(documentIds) {
const response = await fetch('/api/public_documents/extract_metadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ document_ids: documentIds }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok && !(Array.isArray(data.queued) && data.queued.length > 0)) {
throw new Error(data.error || data.message || 'Unable to queue metadata extraction.');
}
return data;
}

function showPublicSelectedMetadataExtractionResult(data) {
const queuedCount = Array.isArray(data.queued) ? data.queued.length : 0;
const errorCount = Array.isArray(data.errors) ? data.errors.length : 0;
const message = errorCount > 0
? `Queued metadata extraction for ${queuedCount} document(s); ${errorCount} item(s) were skipped.`
: (data.message || `Queued metadata extraction for ${queuedCount} document(s).`);
showPublicWorkspaceToast(message, errorCount > 0 ? 'warning' : 'success');
}

async function extractPublicSelectedMetadata() {
const documentIds = Array.from(publicSelectedDocuments);
if (documentIds.length === 0) {
return;
}
if (!(window.enable_extract_meta_data === true || window.enable_extract_meta_data === 'true')) {
showPublicWorkspaceToast('Metadata extraction is not enabled.', 'info');
return;
}

const extractMetadataBtn = document.getElementById('public-extract-selected-metadata-btn');
if (extractMetadataBtn) {
extractMetadataBtn.disabled = true;
extractMetadataBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Extracting...';
}

try {
const data = await requestPublicSelectedMetadataExtraction(documentIds);
showPublicSelectedMetadataExtractionResult(data);
publicSelectedDocuments.clear();
syncPublicSelectionModeUI();
fetchPublicDocs();
} catch (error) {
showPublicWorkspaceToast(error.message, 'danger');
} finally {
if (extractMetadataBtn) {
extractMetadataBtn.disabled = false;
extractMetadataBtn.innerHTML = '<i class="bi bi-magic me-1"></i>Extract Metadata';
}
}
}

function showPublicDocumentDeleteFeedback(message, variant = 'danger') {
if (typeof window.showToast === 'function') {
window.showToast(message, variant);
Expand Down Expand Up @@ -614,11 +668,13 @@ document.addEventListener('DOMContentLoaded', ()=>{
const publicDownloadSelectedBtn = document.getElementById('public-download-selected-btn');
const publicClearSelectionBtn = document.getElementById('public-clear-selection-btn');
const publicChatSelectedBtn = document.getElementById('public-chat-selected-btn');
const publicExtractSelectedMetadataBtn = document.getElementById('public-extract-selected-metadata-btn');

if (publicDeleteSelectedBtn) publicDeleteSelectedBtn.addEventListener('click', deletePublicSelectedDocuments);
if (publicDownloadSelectedBtn) publicDownloadSelectedBtn.addEventListener('click', downloadPublicSelectedDocuments);
if (publicClearSelectionBtn) publicClearSelectionBtn.addEventListener('click', clearPublicSelection);
if (publicChatSelectedBtn) publicChatSelectedBtn.addEventListener('click', chatWithPublicSelected);
if (publicExtractSelectedMetadataBtn) publicExtractSelectedMetadataBtn.addEventListener('click', extractPublicSelectedMetadata);
document.getElementById('public-toggle-selection-btn')?.addEventListener('click', togglePublicSelectionMode);
document.addEventListener('click', handlePublicDocumentCardClick);
});
Expand Down Expand Up @@ -2227,17 +2283,22 @@ function updatePublicBulkActionButtons() {
const deleteBtn = document.getElementById('public-delete-selected-btn');
const downloadBtn = document.getElementById('public-download-selected-btn');
const reprocessDropdown = document.getElementById('public-reprocess-selected-dropdown');
const extractMetadataBtn = document.getElementById('public-extract-selected-metadata-btn');

if (publicSelectedDocuments.size > 0) {
if (bulkActionsBar) bulkActionsBar.style.display = 'block';
if (selectedCountSpan) selectedCountSpan.textContent = publicSelectedDocuments.size;
const canManage = ['Owner', 'Admin', 'DocumentManager'].includes(userRoleInActivePublic);
const canModify = canManage && (window.currentPublicStatus || 'active') === 'active';
const metadataEnabled = window.enable_extract_meta_data === true || window.enable_extract_meta_data === 'true';
if (deleteBtn) deleteBtn.style.display = canManage ? 'inline-block' : 'none';
if (downloadBtn) downloadBtn.classList.toggle('d-none', !publicFileDownloadsEnabled);
if (reprocessDropdown) reprocessDropdown.classList.toggle('d-none', !canManage);
if (extractMetadataBtn) extractMetadataBtn.classList.toggle('d-none', !(canModify && metadataEnabled));
} else {
if (bulkActionsBar) bulkActionsBar.style.display = 'none';
if (downloadBtn) downloadBtn.classList.add('d-none');
if (extractMetadataBtn) extractMetadataBtn.classList.add('d-none');
}
}

Expand Down Expand Up @@ -2511,6 +2572,7 @@ window.clearPublicSelection = clearPublicSelection;
window.chatWithPublicSelected = chatWithPublicSelected;
window.reprocessPublicDocumentExtraction = reprocessPublicDocumentExtraction;
window.reprocessPublicSelectedDocumentExtraction = reprocessPublicSelectedDocumentExtraction;
window.extractPublicSelectedMetadata = extractPublicSelectedMetadata;

// Prompts
function canManagePublicPrompts() {
Expand Down
Loading
Loading