mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-04-26 01:17:19 +02:00
485 lines
17 KiB
JavaScript
485 lines
17 KiB
JavaScript
(function () {
|
|
if (window.isDownloadScriptInitialized) return; // Prevent re-execution
|
|
window.isDownloadScriptInitialized = true;
|
|
|
|
const {
|
|
pdfPasswordPrompt,
|
|
multipleInputsForSingleRequest,
|
|
disableMultipleFiles,
|
|
remoteCall,
|
|
sessionExpired,
|
|
refreshPage,
|
|
error,
|
|
} = window.stirlingPDF;
|
|
|
|
function showErrorBanner(message, stackTrace) {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
errorContainer.style.display = 'block'; // Display the banner
|
|
errorContainer.querySelector('.alert-heading').textContent = error;
|
|
errorContainer.querySelector('p').textContent = message;
|
|
document.querySelector('#traceContent').textContent = stackTrace;
|
|
}
|
|
|
|
function showSessionExpiredPrompt() {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
errorContainer.style.display = 'block';
|
|
errorContainer.querySelector('.alert-heading').textContent = sessionExpired;
|
|
errorContainer.querySelector('p').textContent = sessionExpired;
|
|
document.querySelector('#traceContent').textContent = '';
|
|
|
|
// Optional: Add a refresh button
|
|
const refreshButton = document.createElement('button');
|
|
refreshButton.textContent = refreshPage;
|
|
refreshButton.className = 'btn btn-primary mt-3';
|
|
refreshButton.onclick = () => location.reload();
|
|
errorContainer.appendChild(refreshButton);
|
|
}
|
|
|
|
let firstErrorOccurred = false;
|
|
|
|
$(document).ready(function () {
|
|
$('form').submit(async function (event) {
|
|
event.preventDefault();
|
|
firstErrorOccurred = false;
|
|
const url = this.action;
|
|
let files = $('#fileInput-input')[0].files;
|
|
const formData = new FormData(this);
|
|
const submitButton = document.getElementById('submitBtn');
|
|
const showGameBtn = document.getElementById('show-game-btn');
|
|
const originalButtonText = submitButton.textContent;
|
|
var boredWaiting = localStorage.getItem('boredWaiting') || 'disabled';
|
|
|
|
if (showGameBtn) {
|
|
showGameBtn.style.display = 'none';
|
|
}
|
|
|
|
// Remove empty file entries
|
|
for (let [key, value] of formData.entries()) {
|
|
if (value instanceof File && !value.name) {
|
|
formData.delete(key);
|
|
}
|
|
}
|
|
const override = $('#override').val() || '';
|
|
console.log(override);
|
|
|
|
// Set a timeout to show the game button if operation takes more than 5 seconds
|
|
const timeoutId = setTimeout(() => {
|
|
if (boredWaiting === 'enabled' && showGameBtn) {
|
|
showGameBtn.style.display = 'block';
|
|
showGameBtn.parentNode.insertBefore(document.createElement('br'), showGameBtn.nextSibling);
|
|
}
|
|
}, 5000);
|
|
|
|
try {
|
|
if (!url.includes('remove-password')) {
|
|
// Check if any PDF files are encrypted and handle decryption if necessary
|
|
const decryptedFiles = await checkAndDecryptFiles(url, files);
|
|
files = decryptedFiles;
|
|
}
|
|
|
|
submitButton.textContent = 'Processing...';
|
|
submitButton.disabled = true;
|
|
|
|
if (remoteCall === true) {
|
|
if (override === 'multi' || (!multipleInputsForSingleRequest && files.length > 1 && override !== 'single')) {
|
|
await submitMultiPdfForm(url, files, this);
|
|
} else {
|
|
await handleSingleDownload(url, formData);
|
|
}
|
|
}
|
|
|
|
//clearFileInput();
|
|
clearTimeout(timeoutId);
|
|
if (showGameBtn) {
|
|
showGameBtn.style.display = 'none';
|
|
showGameBtn.style.marginTop = '';
|
|
}
|
|
submitButton.textContent = originalButtonText;
|
|
submitButton.disabled = false;
|
|
|
|
// After process finishes, check for boredWaiting and gameDialog open status
|
|
const gameDialog = document.getElementById('game-container-wrapper');
|
|
if (boredWaiting === 'enabled' && gameDialog && gameDialog.open) {
|
|
// Display a green banner at the bottom of the screen saying "Download complete"
|
|
let downloadCompleteText = 'Download Complete';
|
|
if (window.downloadCompleteText) {
|
|
downloadCompleteText = window.downloadCompleteText;
|
|
}
|
|
$('body').append(
|
|
'<div id="download-complete-banner" style="position:fixed;bottom:0;left:0;width:100%;background-color:green;color:white;text-align:center;padding:10px;font-size:16px;z-index:1000;">' +
|
|
downloadCompleteText +
|
|
'</div>'
|
|
);
|
|
setTimeout(function () {
|
|
$('#download-complete-banner').fadeOut('slow', function () {
|
|
$(this).remove(); // Remove the banner after fading out
|
|
});
|
|
}, 5000); // Banner will fade out after 5 seconds
|
|
}
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
showGameBtn.style.display = 'none';
|
|
submitButton.textContent = originalButtonText;
|
|
submitButton.disabled = false;
|
|
handleDownloadError(error);
|
|
console.error(error);
|
|
}
|
|
});
|
|
});
|
|
|
|
async function getPDFPageCount(file) {
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs';
|
|
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
|
|
return pdf.numPages;
|
|
} catch (error) {
|
|
console.error('Error getting PDF page count:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function checkAndDecryptFiles(url, files) {
|
|
const decryptedFiles = [];
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
|
|
|
// Extract the base URL
|
|
const baseUrl = new URL(url);
|
|
let removePasswordUrl = `${baseUrl.origin}`;
|
|
|
|
// Check if there's a path before /api/
|
|
const apiIndex = baseUrl.pathname.indexOf('/api/');
|
|
if (apiIndex > 0) {
|
|
removePasswordUrl += baseUrl.pathname.substring(0, apiIndex);
|
|
}
|
|
|
|
// Append the new endpoint
|
|
removePasswordUrl += '/api/v1/security/remove-password';
|
|
|
|
console.log(`Remove password URL: ${removePasswordUrl}`);
|
|
|
|
for (const file of files) {
|
|
console.log(`Processing file: ${file.name}`);
|
|
if (file.type !== 'application/pdf') {
|
|
console.log(`Skipping non-PDF file: ${file.name}`);
|
|
decryptedFiles.push(file);
|
|
continue;
|
|
}
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
|
|
|
|
console.log(`Attempting to load PDF: ${file.name}`);
|
|
const pdf = await loadingTask.promise;
|
|
console.log(`File is not encrypted: ${file.name}`);
|
|
decryptedFiles.push(file); // If no error, file is not encrypted
|
|
} catch (error) {
|
|
if (error.name === 'PasswordException' && error.code === 1) {
|
|
console.log(`PDF requires password: ${file.name}`, error);
|
|
console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
|
|
const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
|
|
|
|
if (!password) {
|
|
console.error(`No password provided for encrypted PDF: ${file.name}`);
|
|
showErrorBanner(
|
|
`${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
|
|
`${window.translations.decrypt.unexpectedError}`
|
|
);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
// Prepare FormData for the decryption request
|
|
const formData = new FormData();
|
|
formData.append('fileInput', file);
|
|
formData.append('password', password);
|
|
|
|
// Use handleSingleDownload to send the request
|
|
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
|
|
|
|
if (decryptionResult && decryptionResult.blob) {
|
|
const decryptedBlob = await decryptionResult.blob();
|
|
const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
|
|
|
|
/* // Create a link element to download the file
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(decryptedBlob);
|
|
link.download = 'test.pdf';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
*/
|
|
decryptedFiles.push(decryptedFile);
|
|
console.log(`Successfully decrypted PDF: ${file.name}`);
|
|
} else {
|
|
throw new Error('Decryption failed: No valid response from server');
|
|
}
|
|
} catch (decryptError) {
|
|
console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
|
|
showErrorBanner(
|
|
`${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
|
|
`${window.translations.invalidPassword}`
|
|
);
|
|
throw decryptError;
|
|
}
|
|
} else {
|
|
console.log(`Error loading PDF: ${file.name}`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
return decryptedFiles;
|
|
}
|
|
|
|
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
|
|
const startTime = performance.now();
|
|
const file = formData.get('fileInput');
|
|
let success = false;
|
|
let errorMessage = null;
|
|
|
|
try {
|
|
const response = await window.fetchWithCsrf(url, {method: 'POST', body: formData});
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
if (!response.ok) {
|
|
errorMessage = response.status;
|
|
if (response.status === 401) {
|
|
showSessionExpiredPrompt();
|
|
return;
|
|
}
|
|
if (contentType && contentType.includes('application/json')) {
|
|
console.error('Throwing error banner, response was not okay');
|
|
return handleJsonResponse(response);
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = getFilenameFromContentDisposition(contentDisposition);
|
|
|
|
const blob = await response.blob();
|
|
success = true;
|
|
|
|
if (contentType.includes('application/pdf') || contentType.includes('image/')) {
|
|
//clearFileInput();
|
|
return handleResponse(blob, filename, !isMulti, isZip);
|
|
} else {
|
|
//clearFileInput();
|
|
return handleResponse(blob, filename, false, isZip);
|
|
}
|
|
} catch (error) {
|
|
success = false;
|
|
errorMessage = error.message;
|
|
console.error('Error in handleSingleDownload:', error);
|
|
throw error;
|
|
} finally {
|
|
const processingTime = performance.now() - startTime;
|
|
|
|
// Capture analytics
|
|
const pageCount = file && file.type === 'application/pdf' ? await getPDFPageCount(file) : null;
|
|
if (analyticsEnabled) {
|
|
posthog.capture('file_processing', {
|
|
success: success,
|
|
file_type: file ? file.type || 'unknown' : 'unknown',
|
|
file_size: file ? file.size : 0,
|
|
processing_time: processingTime,
|
|
error_message: errorMessage,
|
|
pdf_pages: pageCount,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getFilenameFromContentDisposition(contentDisposition) {
|
|
let filename;
|
|
|
|
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
|
|
filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim();
|
|
} else {
|
|
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
|
|
filename = 'download';
|
|
}
|
|
|
|
return filename;
|
|
}
|
|
|
|
async function handleJsonResponse(response) {
|
|
const json = await response.json();
|
|
const errorMessage = JSON.stringify(json, null, 2);
|
|
if (
|
|
errorMessage.toLowerCase().includes('the password is incorrect') ||
|
|
errorMessage.toLowerCase().includes('Password is not provided') ||
|
|
errorMessage.toLowerCase().includes('PDF contains an encryption dictionary')
|
|
) {
|
|
if (!firstErrorOccurred) {
|
|
firstErrorOccurred = true;
|
|
alert(pdfPasswordPrompt);
|
|
}
|
|
} else {
|
|
showErrorBanner(json.error + ':' + json.message, json.trace);
|
|
}
|
|
}
|
|
|
|
async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) {
|
|
if (!blob) return;
|
|
const downloadOption = localStorage.getItem('downloadOption');
|
|
if (considerViewOptions) {
|
|
if (downloadOption === 'sameWindow') {
|
|
const url = URL.createObjectURL(blob);
|
|
window.location.href = url;
|
|
return;
|
|
} else if (downloadOption === 'newWindow') {
|
|
const url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
return;
|
|
}
|
|
}
|
|
if (!isZip) {
|
|
downloadFile(blob, filename);
|
|
}
|
|
return {filename, blob};
|
|
}
|
|
|
|
function handleDownloadError(error) {
|
|
const errorMessage = error.message;
|
|
showErrorBanner(errorMessage);
|
|
}
|
|
|
|
let urls = []; // An array to hold all the URLs
|
|
|
|
function downloadFile(blob, filename) {
|
|
if (!(blob instanceof Blob)) {
|
|
console.error('Invalid blob passed to downloadFile function');
|
|
return;
|
|
}
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
urls.push(url); // Store the URL so it doesn't get garbage collected too soon
|
|
|
|
return {filename, blob};
|
|
}
|
|
|
|
async function submitMultiPdfForm(url, files, form) {
|
|
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
|
|
const zipFiles = files.length > zipThreshold;
|
|
let jszip = null;
|
|
// Add Space below Progress Bar before Showing
|
|
$('.progressBarContainer').after($('<br>'));
|
|
$('.progressBarContainer').show();
|
|
// Initialize the progress bar
|
|
|
|
let progressBar = $('.progressBar');
|
|
progressBar.css('width', '0%');
|
|
progressBar.attr('aria-valuenow', 0);
|
|
progressBar.attr('aria-valuemax', files.length);
|
|
|
|
if (zipFiles) {
|
|
jszip = new JSZip();
|
|
}
|
|
|
|
// Get the form with the method attribute set to POST
|
|
let postForm = document.querySelector('form[method="POST"]');
|
|
|
|
// Get existing form data
|
|
let formData;
|
|
if (form) {
|
|
formData = new FormData(form);
|
|
} else if (postForm) {
|
|
formData = new FormData($(postForm)[0]); // Convert the form to a jQuery object and get the raw DOM element
|
|
} else {
|
|
console.log('No form with POST method found.');
|
|
}
|
|
//Remove file to reuse parameters for other runs
|
|
formData.delete('fileInput');
|
|
// Remove empty file entries
|
|
for (let [key, value] of formData.entries()) {
|
|
if (value instanceof File && !value.name) {
|
|
formData.delete(key);
|
|
}
|
|
}
|
|
const CONCURRENCY_LIMIT = 8;
|
|
const chunks = [];
|
|
for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) {
|
|
chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT));
|
|
}
|
|
|
|
for (const chunk of chunks) {
|
|
const promises = chunk.map(async (file) => {
|
|
let fileFormData = new FormData();
|
|
fileFormData.append('fileInput', file);
|
|
for (let [key, value] of fileFormData.entries()) {
|
|
console.log(key, value);
|
|
} // Add other form data
|
|
for (let pair of formData.entries()) {
|
|
fileFormData.append(pair[0], pair[1]);
|
|
console.log(pair[0] + ', ' + pair[1]);
|
|
}
|
|
|
|
try {
|
|
const downloadDetails = await handleSingleDownload(url, fileFormData, true, zipFiles);
|
|
console.log(downloadDetails);
|
|
if (zipFiles) {
|
|
jszip.file(downloadDetails.filename, downloadDetails.blob);
|
|
} else {
|
|
//downloadFile(downloadDetails.blob, downloadDetails.filename);
|
|
}
|
|
updateProgressBar(progressBar, Array.from(files).length);
|
|
} catch (error) {
|
|
handleDownloadError(error);
|
|
console.error(error);
|
|
}
|
|
});
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
if (zipFiles) {
|
|
try {
|
|
const content = await jszip.generateAsync({type: 'blob'});
|
|
downloadFile(content, 'files.zip');
|
|
} catch (error) {
|
|
console.error('Error generating ZIP file: ' + error);
|
|
}
|
|
}
|
|
progressBar.css('width', '100%');
|
|
progressBar.attr('aria-valuenow', Array.from(files).length);
|
|
}
|
|
|
|
function updateProgressBar(progressBar, files) {
|
|
let progress = (progressBar.attr('aria-valuenow') / files.length) * 100 + 100 / files.length;
|
|
progressBar.css('width', progress + '%');
|
|
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
|
|
}
|
|
window.addEventListener('unload', () => {
|
|
for (const url of urls) {
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
});
|
|
|
|
// Clear file input after job
|
|
function clearFileInput() {
|
|
let pathname = document.location.pathname;
|
|
if (pathname != '/merge-pdfs') {
|
|
let formElement = document.querySelector('#fileInput-input');
|
|
formElement.value = '';
|
|
let editSectionElement = document.querySelector('#editSection');
|
|
if (editSectionElement) {
|
|
editSectionElement.style.display = 'none';
|
|
}
|
|
let cropPdfCanvas = document.querySelector('#cropPdfCanvas');
|
|
let overlayCanvas = document.querySelector('#overlayCanvas');
|
|
if (cropPdfCanvas && overlayCanvas) {
|
|
cropPdfCanvas.width = 0;
|
|
cropPdfCanvas.height = 0;
|
|
|
|
overlayCanvas.width = 0;
|
|
overlayCanvas.height = 0;
|
|
}
|
|
} else {
|
|
console.log("Disabled for 'Merge'");
|
|
}
|
|
}
|
|
})();
|