feat(pdf-tools): add remove-restrictions tool for unlocking pdfs
Implement new feature to remove security restrictions from PDFs including password protection and digital signature limitations. The tool allows users to unlock PDFs they have legitimate access to for editing and printing purposes. Includes UI components, error handling, and legal disclaimer about proper usage.
This commit is contained in:
@@ -39,6 +39,7 @@ export const singlePdfLoadTools = [
|
||||
'remove-blank-pages',
|
||||
'add-attachments',
|
||||
'sanitize-pdf',
|
||||
'remove-restrictions',
|
||||
];
|
||||
|
||||
export const simpleTools = [
|
||||
|
||||
@@ -401,6 +401,13 @@ export const categories = [
|
||||
icon: 'ruler',
|
||||
subtitle: 'Analyze page size, orientation, and units.',
|
||||
},
|
||||
{
|
||||
id: 'remove-restrictions',
|
||||
name: 'Remove Restrictions',
|
||||
icon: 'unlink',
|
||||
subtitle:
|
||||
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -410,7 +417,7 @@ export const categories = [
|
||||
id: 'encrypt',
|
||||
name: 'Encrypt PDF',
|
||||
icon: 'lock',
|
||||
subtitle: 'Add a password to protect your PDF.',
|
||||
subtitle: 'Lock your PDF by adding a password.',
|
||||
},
|
||||
{
|
||||
id: 'sanitize-pdf',
|
||||
@@ -422,7 +429,7 @@ export const categories = [
|
||||
id: 'decrypt',
|
||||
name: 'Decrypt PDF',
|
||||
icon: 'unlock',
|
||||
subtitle: 'Remove password protection from a PDF.',
|
||||
subtitle: 'Unlock PDF by removing password protection.',
|
||||
},
|
||||
{
|
||||
id: 'flatten',
|
||||
|
||||
@@ -33,7 +33,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
if (
|
||||
state.pdfDoc.isEncrypted &&
|
||||
toolId !== 'decrypt' &&
|
||||
toolId !== 'change-permissions'
|
||||
toolId !== 'change-permissions' &&
|
||||
toolId !== 'remove-restrictions'
|
||||
) {
|
||||
showAlert(
|
||||
'Protected PDF',
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function changePermissions() {
|
||||
const file = state.files[0];
|
||||
const currentPassword = (
|
||||
document.getElementById('current-password') as HTMLInputElement
|
||||
)?.value || '';
|
||||
const newUserPassword = (
|
||||
document.getElementById('new-user-password') as HTMLInputElement
|
||||
)?.value || '';
|
||||
const newOwnerPassword = (
|
||||
document.getElementById('new-owner-password') as HTMLInputElement
|
||||
)?.value || '';
|
||||
const currentPassword =
|
||||
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newUserPassword =
|
||||
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newOwnerPassword =
|
||||
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
@@ -44,14 +48,27 @@ export async function changePermissions() {
|
||||
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
// Get permission checkboxes
|
||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement)?.checked;
|
||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement)?.checked;
|
||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement)?.checked;
|
||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement)?.checked;
|
||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement)?.checked;
|
||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement)?.checked;
|
||||
const allowPageExtraction = (document.getElementById('allow-page-extraction') as HTMLInputElement)?.checked;
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowPageExtraction = (
|
||||
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||
)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
@@ -71,25 +88,25 @@ export async function changePermissions() {
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
|
||||
console.log('qpdf args:', args);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
// Check for various password-related errors
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')) {
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
if (errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')) {
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED');
|
||||
}
|
||||
|
||||
@@ -110,7 +127,8 @@ export async function changePermissions() {
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage = 'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
successMessage =
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage);
|
||||
@@ -139,14 +157,10 @@ export async function changePermissions() {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function decrypt() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function encrypt() {
|
||||
@@ -35,13 +39,7 @@ export async function encrypt() {
|
||||
|
||||
showLoader('Encrypting PDF with 256-bit AES...');
|
||||
|
||||
const args = [
|
||||
inputPath,
|
||||
'--encrypt',
|
||||
userPassword,
|
||||
ownerPassword,
|
||||
'256',
|
||||
];
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
@@ -53,14 +51,12 @@ export async function encrypt() {
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
|
||||
console.log(args);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
|
||||
@@ -64,12 +64,14 @@ import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
import { linearizePdf } from './linearize.js';
|
||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||
import { sanitizePdf } from './sanitize-pdf.js';
|
||||
import { removeRestrictions } from './remove-restrictions.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
split: { process: split, setup: setupSplitTool },
|
||||
encrypt,
|
||||
decrypt,
|
||||
'remove-restrictions': removeRestrictions,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
|
||||
99
src/js/logic/remove-restrictions.ts
Normal file
99
src/js/logic/remove-restrictions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function removeRestrictions() {
|
||||
const file = state.files[0];
|
||||
const password =
|
||||
(document.getElementById('owner-password-remove') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Removing restrictions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.'
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/js/ui.ts
46
src/js/ui.ts
@@ -913,7 +913,7 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
|
||||
'change-permissions': () => `
|
||||
'change-permissions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
|
||||
<p class="mb-6 text-gray-400">Modify passwords and permissions without losing quality.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -936,7 +936,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">📝 How It Works</h3>
|
||||
<h3 class="font-semibold text-base mb-2">How It Works</h3>
|
||||
<ul class="list-disc list-inside text-sm text-gray-300 space-y-1">
|
||||
<li><strong>User Password:</strong> Required to open the PDF</li>
|
||||
<li><strong>Owner Password:</strong> Required to enforce the permissions below</li>
|
||||
@@ -2049,4 +2049,46 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Sanitize PDF & Download</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
'remove-restrictions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Remove PDF Restrictions</h2>
|
||||
<p class="mb-6 text-gray-400">Remove security restrictions and unlock PDF permissions for editing and printing.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="remove-restrictions-options" class="hidden space-y-4 mt-6">
|
||||
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2"> How it Works </h3>
|
||||
<p class="text-sm text-gray-300 mb-2">This operation will:</p>
|
||||
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||
<li>Remove all permission restrictions (printing, copying, editing)</li>
|
||||
<li>Remove encryption even if the file is encrypted</li>
|
||||
<li>Remove security restrictions associated with digitally signed PDF files (will make signature invalid)</li>
|
||||
<li>Create a fully editable, unrestricted PDF</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="owner-password-remove" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (if required)</label>
|
||||
<input type="password" id="owner-password-remove" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave empty if PDF has no password">
|
||||
<p class="text-xs text-gray-500 mt-1">Enter the owner password if the PDF is password-protected</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-red-900/20 border border-red-500/30 text-red-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">Notice</h3>
|
||||
<p class="text-sm text-gray-300 mb-2">This tool is intended for legitimate purposes only, such as:</p>
|
||||
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||
<li>Removing restrictions from PDFs you own or have permission to modify</li>
|
||||
<li>Recovering access to a PDF when you legitimately forgot the password</li>
|
||||
<li>Accessing content you legally purchased or created</li>
|
||||
<li>Editing documents for authorized business purposes</li>
|
||||
<li>Opening documents for legitimate archival, compliance, or recovery workflows</li>
|
||||
<li class="font-semibold">Limitations: this tool can only remove restrictions from weakly protected PDFs or PDFs that do not have an owner password set. It cannot remove or bypass properly applied AES‑256 (256‑bit) encryption.</li>
|
||||
</ul>
|
||||
<p class="text-sm text-gray-300 mt-3 font-semibold">
|
||||
Using this tool to bypass copyright protections, violate intellectual property rights, or access documents without authorization may be illegal in your jurisdiction. We are not liable for any misuse of this tool — if you're unsure, consult legal counsel or the document owner before proceeding.
|
||||
</p>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Remove Restrictions & Download</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user