Skip to content

Commit fe2f192

Browse files
committed
Convert additional library files to TypeScript
- Convert pdfsecurity.ts: PDF encryption with typed class and permissions - Convert FileSaver.ts: File saving utilities with proper option types - Convert rgbcolor.ts: Color parsing class with TypeScript types All conversions use ES6 classes instead of constructor functions, add proper type annotations for parameters and return values, and maintain backward compatibility with original functionality.
1 parent 795cb75 commit fe2f192

File tree

3 files changed

+635
-0
lines changed

3 files changed

+635
-0
lines changed

src/libs/FileSaver.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @license
3+
* FileSaver.js
4+
* A saveAs() FileSaver implementation.
5+
*
6+
* By Eli Grey, http://eligrey.com
7+
*
8+
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
9+
* source : http://purl.eligrey.com/github/FileSaver.js
10+
*/
11+
12+
import { globalObject as _global } from "./globalObject.js";
13+
import { console } from "./console.js";
14+
15+
interface SaveAsOptions {
16+
autoBom?: boolean;
17+
}
18+
19+
function bom(blob: Blob, opts?: SaveAsOptions | boolean): Blob {
20+
let options: SaveAsOptions;
21+
if (typeof opts === "undefined") {
22+
options = { autoBom: false };
23+
} else if (typeof opts !== "object") {
24+
console.warn("Deprecated: Expected third argument to be a object");
25+
options = { autoBom: !opts };
26+
} else {
27+
options = opts;
28+
}
29+
30+
// prepend BOM for UTF-8 XML and text/* types (including HTML)
31+
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
32+
if (
33+
options.autoBom &&
34+
/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(
35+
blob.type
36+
)
37+
) {
38+
return new Blob([String.fromCharCode(0xfeff), blob], { type: blob.type });
39+
}
40+
return blob;
41+
}
42+
43+
function download(url: string, name: string, opts?: SaveAsOptions): void {
44+
const xhr = new XMLHttpRequest();
45+
xhr.open("GET", url);
46+
xhr.responseType = "blob";
47+
xhr.onload = function() {
48+
saveAs(xhr.response, name, opts);
49+
};
50+
xhr.onerror = function() {
51+
console.error("could not download file");
52+
};
53+
xhr.send();
54+
}
55+
56+
function corsEnabled(url: string): boolean {
57+
const xhr = new XMLHttpRequest();
58+
// use sync to avoid popup blocker
59+
xhr.open("HEAD", url, false);
60+
try {
61+
xhr.send();
62+
} catch (e) {}
63+
return xhr.status >= 200 && xhr.status <= 299;
64+
}
65+
66+
// `a.click()` doesn't work for all browsers (#465)
67+
function click(node: HTMLAnchorElement): void {
68+
try {
69+
node.dispatchEvent(new MouseEvent("click"));
70+
} catch (e) {
71+
const evt = document.createEvent("MouseEvents");
72+
evt.initMouseEvent(
73+
"click",
74+
true,
75+
true,
76+
window,
77+
0,
78+
0,
79+
0,
80+
80,
81+
20,
82+
false,
83+
false,
84+
false,
85+
false,
86+
0,
87+
null
88+
);
89+
node.dispatchEvent(evt);
90+
}
91+
}
92+
93+
type SaveAsFunction = (blob: Blob | string, name?: string, opts?: SaveAsOptions, popup?: Window | null) => void;
94+
95+
const saveAs: SaveAsFunction =
96+
_global.saveAs ||
97+
// probably in some web worker
98+
(typeof window !== "object" || window !== _global
99+
? function saveAs(): void {
100+
/* noop */
101+
}
102+
: // Use download attribute first if possible (#193 Lumia mobile) unless this is a native app
103+
typeof HTMLAnchorElement !== "undefined" &&
104+
"download" in HTMLAnchorElement.prototype
105+
? function saveAs(blob: Blob | string, name?: string, opts?: SaveAsOptions): void {
106+
const URL = _global.URL || _global.webkitURL;
107+
const a = document.createElement("a");
108+
name = name || (blob as any).name || "download";
109+
110+
a.download = name;
111+
a.rel = "noopener"; // tabnabbing
112+
113+
// TODO: detect chrome extensions & packaged apps
114+
// a.target = '_blank'
115+
116+
if (typeof blob === "string") {
117+
// Support regular links
118+
a.href = blob;
119+
if (a.origin !== location.origin) {
120+
corsEnabled(a.href)
121+
? download(blob, name, opts)
122+
: click(a as any);
123+
} else {
124+
click(a);
125+
}
126+
} else {
127+
// Support blobs
128+
a.href = URL.createObjectURL(blob);
129+
setTimeout(function() {
130+
URL.revokeObjectURL(a.href);
131+
}, 4e4); // 40s
132+
setTimeout(function() {
133+
click(a);
134+
}, 0);
135+
}
136+
}
137+
: // Use msSaveOrOpenBlob as a second approach
138+
"msSaveOrOpenBlob" in navigator
139+
? function saveAs(blob: Blob | string, name?: string, opts?: SaveAsOptions): void {
140+
name = name || (blob as any).name || "download";
141+
142+
if (typeof blob === "string") {
143+
if (corsEnabled(blob)) {
144+
download(blob, name, opts);
145+
} else {
146+
const a = document.createElement("a");
147+
a.href = blob;
148+
a.target = "_blank";
149+
setTimeout(function() {
150+
click(a);
151+
});
152+
}
153+
} else {
154+
(navigator as any).msSaveOrOpenBlob(bom(blob, opts), name);
155+
}
156+
}
157+
: // Fallback to using FileReader and a popup
158+
function saveAs(blob: Blob | string, name?: string, opts?: SaveAsOptions, popup?: Window | null): void {
159+
// Open a popup immediately do go around popup blocker
160+
// Mostly only available on user interaction and the fileReader is async so...
161+
popup = popup || open("", "_blank");
162+
if (popup) {
163+
popup.document.title = popup.document.body.innerText =
164+
"downloading...";
165+
}
166+
167+
if (typeof blob === "string") return download(blob, name, opts);
168+
169+
const force = blob.type === "application/octet-stream";
170+
const isSafari =
171+
/constructor/i.test((_global as any).HTMLElement) || (_global as any).safari;
172+
const isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
173+
174+
if (
175+
(isChromeIOS || (force && isSafari)) &&
176+
typeof FileReader === "object"
177+
) {
178+
// Safari doesn't allow downloading of blob URLs
179+
const reader = new FileReader();
180+
reader.onloadend = function() {
181+
let url = reader.result as string;
182+
url = isChromeIOS
183+
? url
184+
: url.replace(/^data:[^;]*;/, "data:attachment/file;");
185+
if (popup) popup.location.href = url;
186+
else (location as any) = url;
187+
popup = null; // reverse-tabnabbing #460
188+
};
189+
reader.readAsDataURL(blob);
190+
} else {
191+
const URL = _global.URL || _global.webkitURL;
192+
const url = URL.createObjectURL(blob);
193+
if (popup) (popup.location as any) = url;
194+
else location.href = url;
195+
popup = null; // reverse-tabnabbing #460
196+
setTimeout(function() {
197+
URL.revokeObjectURL(url);
198+
}, 4e4); // 40s
199+
}
200+
});
201+
202+
export { saveAs };

src/libs/pdfsecurity.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* @license
3+
* Licensed under the MIT License.
4+
* http://opensource.org/licenses/mit-license
5+
* Author: Owen Leong (@owenl131)
6+
* Date: 15 Oct 2020
7+
* References:
8+
* https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt
9+
* https://github.com/foliojs/pdfkit/blob/master/lib/security.js
10+
* http://www.fpdf.org/en/script/script37.php
11+
*/
12+
13+
import { md5Bin } from "./md5.js";
14+
import { rc4 } from "./rc4.js";
15+
16+
type Permission = "print" | "modify" | "copy" | "annot-forms";
17+
18+
const permissionOptions: Record<Permission, number> = {
19+
print: 4,
20+
modify: 8,
21+
copy: 16,
22+
"annot-forms": 32
23+
};
24+
25+
/**
26+
* Initializes encryption settings
27+
*
28+
* @name constructor
29+
* @function
30+
* @param {Array} permissions Permissions allowed for user, "print", "modify", "copy" and "annot-forms".
31+
* @param {String} userPassword Permissions apply to this user. Leaving this empty means the document
32+
* is not password protected but viewer has the above permissions.
33+
* @param {String} ownerPassword Owner has full functionalities to the file.
34+
* @param {String} fileId As hex string, should be same as the file ID in the trailer.
35+
* @example
36+
* var security = new PDFSecurity(["print"])
37+
*/
38+
class PDFSecurity {
39+
v: number;
40+
r: number;
41+
padding: string;
42+
O: string;
43+
P: number;
44+
encryptionKey: string;
45+
U: string;
46+
47+
constructor(permissions: Permission[], userPassword: string, ownerPassword: string, fileId: string) {
48+
this.v = 1; // algorithm 1, future work can add in more recent encryption schemes
49+
this.r = 2; // revision 2
50+
51+
// set flags for what functionalities the user can access
52+
let protection = 192;
53+
permissions.forEach((perm) => {
54+
if (typeof permissionOptions[perm] === "undefined") {
55+
throw new Error("Invalid permission: " + perm);
56+
}
57+
protection += permissionOptions[perm];
58+
});
59+
60+
// padding is used to pad the passwords to 32 bytes, also is hashed and stored in the final PDF
61+
this.padding =
62+
"\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" +
63+
"\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A";
64+
const paddedUserPassword = (userPassword + this.padding).substr(0, 32);
65+
const paddedOwnerPassword = (ownerPassword + this.padding).substr(0, 32);
66+
67+
this.O = this.processOwnerPassword(paddedUserPassword, paddedOwnerPassword);
68+
this.P = -((protection ^ 255) + 1);
69+
this.encryptionKey = md5Bin(
70+
paddedUserPassword +
71+
this.O +
72+
this.lsbFirstWord(this.P) +
73+
this.hexToBytes(fileId)
74+
).substr(0, 5);
75+
this.U = rc4(this.encryptionKey, this.padding);
76+
}
77+
78+
/**
79+
* Breaks down a 4-byte number into its individual bytes, with the least significant bit first
80+
*
81+
* @name lsbFirstWord
82+
* @function
83+
* @param {number} data 32-bit number
84+
* @returns {string}
85+
*/
86+
lsbFirstWord(data: number): string {
87+
return String.fromCharCode(
88+
(data >> 0) & 0xff,
89+
(data >> 8) & 0xff,
90+
(data >> 16) & 0xff,
91+
(data >> 24) & 0xff
92+
);
93+
}
94+
95+
/**
96+
* Converts a byte string to a hex string
97+
*
98+
* @name toHexString
99+
* @function
100+
* @param {string} byteString Byte string
101+
* @returns {string}
102+
*/
103+
toHexString(byteString: string): string {
104+
return byteString
105+
.split("")
106+
.map((byte) => {
107+
return ("0" + (byte.charCodeAt(0) & 0xff).toString(16)).slice(-2);
108+
})
109+
.join("");
110+
}
111+
112+
/**
113+
* Converts a hex string to a byte string
114+
*
115+
* @name hexToBytes
116+
* @function
117+
* @param {string} hex Hex string
118+
* @returns {string}
119+
*/
120+
hexToBytes(hex: string): string {
121+
const bytes: string[] = [];
122+
for (let c = 0; c < hex.length; c += 2) {
123+
bytes.push(String.fromCharCode(parseInt(hex.substr(c, 2), 16)));
124+
}
125+
return bytes.join("");
126+
}
127+
128+
/**
129+
* Computes the 'O' field in the encryption dictionary
130+
*
131+
* @name processOwnerPassword
132+
* @function
133+
* @param {string} paddedUserPassword Byte string of padded user password
134+
* @param {string} paddedOwnerPassword Byte string of padded owner password
135+
* @returns {string}
136+
*/
137+
processOwnerPassword(
138+
paddedUserPassword: string,
139+
paddedOwnerPassword: string
140+
): string {
141+
const key = md5Bin(paddedOwnerPassword).substr(0, 5);
142+
return rc4(key, paddedUserPassword);
143+
}
144+
145+
/**
146+
* Returns an encryptor function which can take in a byte string and returns the encrypted version
147+
*
148+
* @name encryptor
149+
* @function
150+
* @param {number} objectId
151+
* @param {number} generation Not sure what this is for, you can set it to 0
152+
* @returns {Function}
153+
* @example
154+
* out("stream");
155+
* encryptor = security.encryptor(object.id, 0);
156+
* out(encryptor(data));
157+
* out("endstream");
158+
*/
159+
encryptor(objectId: number, generation: number): (data: string) => string {
160+
const key = md5Bin(
161+
this.encryptionKey +
162+
String.fromCharCode(
163+
objectId & 0xff,
164+
(objectId >> 8) & 0xff,
165+
(objectId >> 16) & 0xff,
166+
generation & 0xff,
167+
(generation >> 8) & 0xff
168+
)
169+
).substr(0, 10);
170+
return function(data: string): string {
171+
return rc4(key, data);
172+
};
173+
}
174+
}
175+
176+
export { PDFSecurity };

0 commit comments

Comments
 (0)