diff --git a/.gitignore b/.gitignore index 2c40e63..faacab1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/settings.json autorename-workspace.code-workspace +/lib/TwitterAPI/TwitterAPIKey.js \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d922a98..d52f20d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -94,6 +94,10 @@ "message": "About" }, + "common_section_downloads": { + "message": "Downloads" + }, + "common_section_file_name": { "message": "File Name" }, @@ -250,6 +254,10 @@ "message": "The Date/Time Format you entered is either empty or invalid" }, + "error_download_queue_invalid_file_name": { + "message": "You have entered an invalid file name" + }, + "error_validation_prefix": { "message": "The Prefix you entered contains special characters" }, @@ -339,6 +347,10 @@ "message": "Enable notification when extension is updated" }, + "general_settings_store_images_to_custom_folder": { + "message": "Store Images to AutoRename Folder" + }, + "twitter_settings_desciption": { "message": "Customize File Name Format for Twitter" }, @@ -359,6 +371,10 @@ "message": "Include Website Title [Twitter]" }, + "twitter_settings_save_image_to_folder_based_on_username": { + "message": "Save image to folder based on username" + }, + "lineblog_settings_description": { "message": "Customize File Name Format for LINE BLOG" }, @@ -397,5 +413,75 @@ "reddit_settings_include_date": { "message": "Include Date (Based on System Date)" + }, + + "downloads_section_description": { + "message": "View files that are pending for download" + }, + + "downloads_section_dialog_empty_queue_title": { + "message": "Download Queue is Empty" + }, + + "downloads_section_empty_queue": { + "message": "Download queue is empty. To get started, right click on an image > AutoRename > Add to Download Queue" + }, + + "downloads_section_dialog_remove_from_queue_title": { + "message": "Remove from Queue" + }, + + "downloads_section_dialog_enter_zip_name": { + "message": "Enter name for ZIP file" + }, + + "downloads_section_dialog_queue_cleared": { + "message": "Download Queue Cleared" + }, + + "downloads_section_dialog_enter_zip_name_button_create": { + "message": "Create ZIP" + }, + + "downloads_section_dialog_remove_from_queue_body": { + "message": "Are you sure you want to remove this?" + }, + + "downloads_section_count_label": { + "message": "Download Queue -" + }, + + "downloads_section_button_remove": { + "message": "Remove" + }, + + "downloads_section_button_download": { + "message": "Download" + }, + + "downloads_section_button_clear_queue": { + "message": "Clear Queue" + }, + + "downloads_section_saving_file": { + "message": "Saving file:" + }, + + "downloads_section_saving_file_2": { + "message": "to ZIP -", + "description": "Dash is required since the percentage is after that" + }, + + "downloads_section_saving_file_3": { + "message": "Your files are currently downloading. Please wait..." + }, + + "downloads_section_downlaod_in_progress": { + "message": "Download in progress" + }, + + "downloads_section_downlaod_complete_title": { + "message": "Download Complete" } + } \ No newline at end of file diff --git a/assets/icons8-clock-48.png b/assets/icons8-clock-48.png new file mode 100644 index 0000000..500da44 Binary files /dev/null and b/assets/icons8-clock-48.png differ diff --git a/assets/icons8-data-pending-48.png b/assets/icons8-data-pending-48.png new file mode 100644 index 0000000..dfe28b5 Binary files /dev/null and b/assets/icons8-data-pending-48.png differ diff --git a/assets/icons8-download-48.png b/assets/icons8-download-48.png new file mode 100644 index 0000000..31a8275 Binary files /dev/null and b/assets/icons8-download-48.png differ diff --git a/css/stylesheet.css b/css/stylesheet.css index 42592bc..11e730a 100644 --- a/css/stylesheet.css +++ b/css/stylesheet.css @@ -1,6 +1,6 @@ body { - background: #fff; - font-family: 'Segoe UI', 'Roboto', '-apple-system', 'BlinkMacSystemFont', 'Arial', sans-serif !important; + background: #eeeeee3b; + font-family: 'Segoe UI Variable Display', 'Segoe UI', 'Roboto', '-apple-system', 'BlinkMacSystemFont', 'Arial', sans-serif !important; font-size: 1.1em; margin: 0; padding: 0; @@ -364,12 +364,75 @@ a:-webkit-any-link { grid-template-columns: auto auto; margin-top: 20px; } + +.download-card { + align-items: center; + background-color: #fff; + box-shadow: 1px 1px 4px #e5e5e5; + border-radius: 5px; + display: flex; + flex-direction: row; + margin-bottom: 5px; + padding: 3px; +} + +.download-card .download-card-actions { + visibility: hidden; +} + +.download-card:hover .download-card-actions { + visibility: visible; +} + +.image-thumbnail { + width: 40px; + height: 40px; + margin: 5px 5px 5px 5px; + object-fit: cover; +} + +.download-card-site { + background-color: #000; + border-radius: 5px; + color: #fff; + display: inline-flex; + margin-top: 5px; + padding: 2px; + font-size: 12px; +} + +.download-card-actions { + margin-left: auto; +} + +.download-card-actions-button-primary { + background-color: #03B4D3; + color: #fff; + margin-right: 10px; + +} + + +.download-card-actions-button-secondary { + background-color: #fff0; + margin-right: 1px; +} + +.beta_tag { + padding: 4px; + background-color: #FFCD03; + color: black; + border-radius: 6px; + margin-left: 5px; + font-weight: 600; +} + /** EVERYTHING BELOW IS FOR DARK MODE ONLY **/ @media (prefers-color-scheme: dark) { body { - background-color: #121212; + background-color: #161616; } hr { @@ -377,7 +440,7 @@ a:-webkit-any-link { } .navbar { - background-color: #121212; + background-color: #181818; border-right: 1px solid #1f1f1f; color: #fff; } @@ -414,6 +477,46 @@ a:-webkit-any-link { background: #242424; box-shadow: 0 0 28px #00000059; } + + .download_card_container { + color: #B0B0B0; + } + + .download-card { + background-color: #222222; + box-shadow: 1px 1px 4px #000; + } + + .download-card-site { + color: #fff; + margin-top: 5px; + font-size: 12px; + } + + .download-card-actions { + margin-left: auto; + } + + .download-card-actions-button-primary { + background-color: #03B4D3; + color: #fff; + margin-right: 10px; + + } + + .download-card-actions-button-secondary { + background-color: #fff0; + color: #B0B0B0; + margin-right: 1px; + } + + .download-card-file { + color: #B0B0B0; + } + + .download-card-info { + color: #B0B0B0; + } } /** ANIMATION **/ diff --git a/js/BackgroundService.js b/js/BackgroundService.js new file mode 100644 index 0000000..5f8b9a0 --- /dev/null +++ b/js/BackgroundService.js @@ -0,0 +1,43 @@ +/** MIT License + * + * Copyright (c) 2023 Dasutein + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * See more: https://github.com/ddasutein/AutoRename/blob/master/LICENSE + * + */ + +const GLOBAL_VARIABLES = { + + UA_HEADER: `${navigator.userAgent} ${chrome.runtime.getManifest().name}/${chrome.runtime.getManifest().version}` + +} + +const Backgroundscripts = [ + "/js/Settings.js", + "/js/Runtime.js", + "/lib/Moment/moment.js", + "/lib/Moment/moment-with-locales.js", + "/js/Common/Utility.js", + "/js/Common/Debugger.js", + "/js/Common/SetTimeDate.js", + "/js/Twitter/TwitterContent.js", + "/js/LINE BLOG/LineBlogContent.js", + "/js/Reddit/RedditContent.js", + "/js/DownloadManager.js", + "/js/SaveAsEventHandle.js" +] + +try { + Backgroundscripts.forEach((x)=>{ + importScripts(x); + }); + +}catch(e){ + console.error(e); +} \ No newline at end of file diff --git a/js/Common/Utility.js b/js/Common/Utility.js index fa8a5b8..37573a9 100644 --- a/js/Common/Utility.js +++ b/js/Common/Utility.js @@ -75,6 +75,27 @@ let Utility = { } break; } + }), + + SetBadgeText : ((str)=>{ + if (str == 0 || str == null){ + chrome.action.setBadgeText({ + text: "" + }); + } else if (str > 0){ + chrome.action.setBadgeText({ + text: str.toString() + }); + } + + }), + + CreateNewTab : ((url)=>{ + return chrome.tabs.create({ + url: url + }); }) + + } \ No newline at end of file diff --git a/js/DownloadManager.js b/js/DownloadManager.js index 932b92d..4e24b51 100644 --- a/js/DownloadManager.js +++ b/js/DownloadManager.js @@ -1,6 +1,6 @@ /** MIT License * - * Copyright (c) 2021 Dasutein + * Copyright (c) 2023 Dasutein * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, @@ -12,64 +12,111 @@ * */ -/** - * Start downloading files - * - * @param {object} downloadQueue Images that are in queue to download - */ -function StartDownload(downloadQueue){ - - Object.values(Settings.Load().General.map((key, index)=>{ - switch (index){ - - // global_enable_save_as_window - case 1: - if (key.value == true){ - downloadQueue.forEach((dq)=>{ - chrome.downloads.download({ - url: dq.url, - filename: dq.filename, - saveAs: true - }); - }); - } else { - downloadQueue.forEach((dq)=>{ - chrome.downloads.download({ - url: dq.url, - filename: dq.filename, - saveAs: false - }); - }); - } - break; +var DownloadManager = { + + UpdateBadge: (async ()=>{ + generalSettings = Settings.Load().General.map((data) => { + return { + "key": data.key, + "value": data.value + } + }).reduce((obj, data) => { + obj[data.key] = data; + return obj; + }, {}); + + let downloadData = generalSettings["global_download_queue_data"].value; + if (downloadData.length > 0) { + downloadData = JSON.parse(downloadData) + } else { + downloadData = []; } - })); -} -chrome.downloads.onChanged.addListener(function (downloadDelta) { - chrome.storage.local.get({ - showDownloadFolderCheckbox: false - }, function (items) { - if (downloadDelta.state && downloadDelta.state.current == "complete") { + Utility.SetBadgeText(downloadData.length); + }), + + AddDownloadQueue: ((data) => { - if (DevMode){ - const DEBUG_TAG = "downloadsOnChangedListener => "; - console.log(DEBUG_TAG + downloadDelta.state.current); + generalSettings = Settings.Load().General.map((data) => { + return { + "key": data.key, + "value": data.value } + }).reduce((obj, data) => { + obj[data.key] = data; + return obj; + }, {}); - let generalSettings = Settings.Load().General; + let downloadData = generalSettings["global_download_queue_data"].value; + if (downloadData.length > 0) { + downloadData = JSON.parse(downloadData) + } else { + downloadData = []; + } - generalSettings.map((key, index) => { - switch (index){ - case 0: - if (key.value){ - chrome.downloads.showDefaultFolder(); - } - break; - } + data.forEach((x) => { + downloadData.push({ + filename: x.filename, + url: x.url, + website: x.website }); + }); + + Utility.SetBadgeText(downloadData.length); + Settings.Save("global_download_queue_data", JSON.stringify(downloadData)); + + }), + + ClearDownloadQueue: (() => { + Settings.Save("global_download_queue_data", ""); + Utility.SetBadgeText(null); + }), + + StartDownload: ((data) => { + + let fileData = {}; + + generalSettings = Settings.Load().General.map((data) => { + return { + "key": data.key, + "value": data.value + } + }).reduce((obj, data) => { + obj[data.key] = data; + return obj; + }, {}); + + data.forEach((x) => { + chrome.downloads.download({ + url: x.url, + filename: `${x.filename}`, + saveAs: generalSettings["global_enable_save_as_window"].value + }, ((id) => { + + chrome.downloads.onChanged.addListener((delta) => { + + if (id == delta.id) { + + if (delta.state && delta.state.current == "in_progress") { + fileData["size"] = delta.fileSize.current; + fileData["url"] = delta.url.current; + fileData["id"] = delta.id; + fileData["name"] = x.filename; + } + + if (delta.state && delta.state.current == "complete") { + generalSettings["global_show_download_folder"].value ? chrome.downloads.showDefaultFolder() : null; + tmp = []; + tmp.push(fileData); + Settings.Save("global_download_history_data", JSON.stringify(tmp)); + DownloadManager.UpdateBadge(); + } + } + }); + + })); + }); + }), + +} - return; - } - }); -}); \ No newline at end of file diff --git a/js/ExtensionInfo.js b/js/ExtensionInfo.js index 15dfe58..26e22f7 100644 --- a/js/ExtensionInfo.js +++ b/js/ExtensionInfo.js @@ -18,37 +18,37 @@ /** * Loads extension data from Manifest */ -function LoadExtensionData(){ - let request = new XMLHttpRequest(); - - request.onload = function (e) { - if (request.readyState === 4) { - if (request.readyState === 200) { - console.log(request.responseText); - } else { - let jsonConfig = this.responseText; - let jsonParse = JSON.parse(jsonConfig); - document.getElementById("main_extension_name").textContent = jsonParse.short_name; - - if ((jsonParse.version).split(".")[3] >= 1000) { - document.getElementById("main_extension_version").textContent = `${jsonParse.version} (dev-build)`; - - } else if ((jsonParse.version).split(".")[3] == undefined) { - document.getElementById("main_extension_version").textContent = `${jsonParse.version}`; +function LoadExtensionData() { + let request = new XMLHttpRequest(); + + request.onload = function (e) { + if (request.readyState === 4) { + if (request.readyState === 200) { + console.log(request.responseText); + } else { + let jsonConfig = this.responseText; + let jsonParse = JSON.parse(jsonConfig); + document.getElementById("main_extension_name").textContent = jsonParse.short_name; + + if ((jsonParse.version).split(".")[3] >= 1000) { + document.getElementById("main_extension_version").textContent = `${jsonParse.version} (dev-build)`; + + } else if ((jsonParse.version).split(".")[3] == undefined) { + document.getElementById("main_extension_version").textContent = `${jsonParse.version}`; + } + + // document.getElementById("extension_description").innerHTML = jsonParse.description; + console.log("AND IT SHALL BE BESTOWNED UPON YOU, THE STAR WHICH YOU HAVE LONGED FOR—"); + console.log("Status: " + request.statusText + "👍"); + } } - - // document.getElementById("extension_description").innerHTML = jsonParse.description; - console.log("AND IT SHALL BE BESTOWNED UPON YOU, THE STAR WHICH YOU HAVE LONGED FOR—"); - console.log("Status: " + request.statusText + "👍"); - } } - } - request.open('GET', chrome.extension.getURL('manifest.json'), true); - request.onerror = function (e) { - console.error(request.statusText); - }; - - request.send(null); + request.open('GET', chrome.runtime.getURL('manifest.json'), true); + request.onerror = function (e) { + console.error(request.statusText); + }; + + request.send(null); } @@ -57,124 +57,139 @@ function LoadExtensionData(){ */ function GetSystemInfo() { - let appVersion = navigator.appVersion; - let platformData = {}; - - function isChromium() { - for (brand_version_pair of navigator.userAgentData.brands) { - if (brand_version_pair.brand == "Chromium") { - return true; - } - } - return false; - } - - function queryOtherOS(av){ - console.log(av) - function getMacOS(av){ - let macVersion = av.substring(31, av.lastIndexOf("OS X") + 12); - return `macOS ${macVersion.replace(/_/gi, ".")}`; - } - - function getChromeOS(av){ - let chromeOSVersion = av.substring(31, av.lastIndexOf("CrOS") + 5); - return `Chrome OS ${chromeOSVersion}` - } - - if (av.includes("Windows NT 6.1")){ - return "Windows 7"; - } else if (av.includes("Windows NT 6.2")){ - return "Windows 8"; - } else if (av.includes("Windows NT 6.3")){ - return "Windows 8.1"; - } else if (av.includes("Macintosh")){ - return getMacOS(av); - } else if (av.includes("CrOS")){ - return getChromeOS(av); - } else { - return null; - } - - } - - if (isChromium() == true) { - navigator.userAgentData.getHighEntropyValues( - ["architecture", "model", "platform", "platformVersion", "uaFullVersion"]) - .then(ua => { - - if (!!ua.uaFullVersion){ - platformData["browserVersion"] = `(${ua.uaFullVersion})`; - } - - if (ua.platform == "Windows"){ - switch (ua.platformVersion){ - - case "0.0.0": - // Fallback to legacy OS query - queryOtherOS(appVersion); - break; - - case "1.0.0": - platformData["os"] = "Windows 10 version 1507"; - break; - - case "2.0.0": - platformData["os"] = "Windows 10 version 1511"; - break; - - case "3.0.0": - platformData["os"] = "Windows 10 version 1607"; - break; - - case "4.0.0": - platformData["os"] = "Windows 10 version 1703"; - break; - - case "5.0.0": - platformData["os"] = "Windows 10 version 1709"; - break; - - case "6.0.0": - platformData["os"] = "Windows 10 version 1803"; - break; - - case "7.0.0": - platformData["os"] = "Windows 10 version 1809"; - break; - - case "8.0.0": - platformData["os"] = "Windows 10 version 1903/1909"; - break; - - case "10.0.0": - platformData["os"] = "Windows 10 version 2004/20H2/21H1"; - break; - - case "14.0.0": - platformData["os"] = "Windows 11 version 21H2"; - break; - - default: - platformData["os"] = "-"; - break; - - } - } else { - platformData["os"] = queryOtherOS(appVersion); - } - - - if (ua.architecture == "x86"){ - if (ua.platform == "Windows"){ - platformData["architecture"] = `${ua.platform} - Intel/AMD x86-64`; - } - } - document.getElementById("system_information_platform_value").innerHTML = platformData.architecture; - document.getElementById("system_information_os_value").innerHTML = platformData.os; - document.getElementById("system_information_browser_value").innerHTML = `${ua.brands[1].brand} ${platformData.browserVersion}`; - - }); - } + let appVersion = navigator.appVersion; + let platformData = {}; + + function isChromium() { + for (brand_version_pair of navigator.userAgentData.brands) { + if (brand_version_pair.brand == "Chromium") { + return true; + } + } + return false; + } + + function queryOtherOS(av) { + console.log(av) + + function getMacOS(av) { + let macVersion = av.substring(31, av.lastIndexOf("OS X") + 12); + return `macOS ${macVersion.replace(/_/gi, ".")}`; + } + + function getChromeOS(av) { + let chromeOSVersion = av.substring(31, av.lastIndexOf("CrOS") + 5); + return `Chrome OS ${chromeOSVersion}` + } + + if (av.includes("Windows NT 6.1")) { + return "Windows 7"; + } else if (av.includes("Windows NT 6.2")) { + return "Windows 8"; + } else if (av.includes("Windows NT 6.3")) { + return "Windows 8.1"; + } else if (av.includes("Macintosh")) { + return getMacOS(av); + } else if (av.includes("CrOS")) { + return getChromeOS(av); + } else { + return null; + } + + } + + if (isChromium() == true) { + navigator.userAgentData.getHighEntropyValues( + ["architecture", "model", "platform", "platformVersion", "uaFullVersion"]) + .then(ua => { + + if (!!ua.uaFullVersion) { + platformData["browserVersion"] = `(${ua.uaFullVersion})`; + } + + if (ua.platform == "Windows") { + switch (ua.platformVersion) { + + case "0.0.0": + // Fallback to legacy OS query + queryOtherOS(appVersion); + break; + + case "1.0.0": + platformData["os"] = "Windows 10 version 1507"; + break; + + case "2.0.0": + platformData["os"] = "Windows 10 version 1511"; + break; + + case "3.0.0": + platformData["os"] = "Windows 10 version 1607"; + break; + + case "4.0.0": + platformData["os"] = "Windows 10 version 1703"; + break; + + case "5.0.0": + platformData["os"] = "Windows 10 version 1709"; + break; + + case "6.0.0": + platformData["os"] = "Windows 10 version 1803"; + break; + + case "7.0.0": + platformData["os"] = "Windows 10 version 1809"; + break; + + case "8.0.0": + platformData["os"] = "Windows 10 version 1903/1909"; + break; + + case "10.0.0": + platformData["os"] = "Windows 10 version 2004/20H2/21H1"; + break; + + case "14.0.0": + platformData["os"] = "Windows 11 version 21H2"; + break; + + case "15.0.0": + platformData["os"] = "Windows 11 version 22H2"; + break; + + default: + platformData["os"] = "-"; + break; + + } + } else { + platformData["os"] = queryOtherOS(appVersion); + } + + + if (ua.architecture == "x86") { + if (ua.platform == "Windows") { + platformData["architecture"] = `${ua.platform} - Intel/AMD x86-64`; + } else if (ua.platform == "macOS"){ + platformData["architecture"] = `${ua.platform} - Intel/AMD x86-64`; + } + } + + if (ua.architecture == "arm"){ + if (ua.platform == "macOS"){ + platformData["architecture"] = `${ua.platform} - Apple Silicon`; + } else if (ua.platform == "Windows"){ + platformData["architecture"] = `${ua.platform} - ARM`; + } + } + document.getElementById("system_information_platform_value").innerHTML = platformData.architecture; + document.getElementById("system_information_os_value").innerHTML = platformData.os; + document.getElementById("system_information_browser_value").innerHTML = `${ua.brands[1].brand} ${platformData.browserVersion}`; + + }); + } } diff --git a/js/LINE BLOG/LineBlogContent.js b/js/LINE BLOG/LineBlogContent.js index 46e9bb5..200b187 100644 --- a/js/LINE BLOG/LineBlogContent.js +++ b/js/LINE BLOG/LineBlogContent.js @@ -12,7 +12,7 @@ * */ -function SaveLINEBlogMedia(tabUrl, url, customObj){ +function SaveLINEBlogMedia(tabUrl, url, linkUrl, customObj){ if (BrowserTabInfo.URL.match(Website.LINE_BLOG)){ lineblogTitle = BrowserTabInfo.Title.split("-")[1] != undefined ? BrowserTabInfo.Title.split("-")[1].trim().toString() : ""; @@ -114,7 +114,11 @@ function SaveLINEBlogMedia(tabUrl, url, customObj){ filename: buildFileName(fileNameObj) + ".jpg", url: url }); - - StartDownload(lineBlogImageFile); + console.log(customObj); + if (customObj.download_queue == true){ + AddToDownloadQueue(lineBlogImageFile[0].url, lineBlogImageFile[0].filename, "LINE BLOG") + } else { + StartDownload(lineBlogImageFile); + } } \ No newline at end of file diff --git a/js/OptionsPage.js b/js/OptionsPage.js index cc1e4b7..7c31083 100644 --- a/js/OptionsPage.js +++ b/js/OptionsPage.js @@ -1,6 +1,6 @@ /** MIT License * - * Copyright (c) 2022 Dasutein + * Copyright (c) 2023 Dasutein * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, @@ -21,7 +21,7 @@ const messageBox = { Save: (() => { - return swal ({ + return swal({ title: "", text: chrome.i18n.getMessage("context_save_success"), icon: "success", @@ -31,7 +31,7 @@ const messageBox = { }); }), - Warning : ((title, message)=>{ + Warning: ((title, message) => { return swal({ title: title, text: message, @@ -54,18 +54,18 @@ const specialCharacters = new RegExp(/[?!@#$%^&*(),';\:*"<>|/]/g); * @param {*} dateString Date string * @returns */ -function ValidateDateTimeFormat(settingsKey, dateString){ +function ValidateDateTimeFormat(settingsKey, dateString) { let output = {}; - if (dateString == "" || dateString == null || dateString == undefined){ + if (dateString == "" || dateString == null || dateString == undefined) { output["title"] = chrome.i18n.getMessage("error_title_invalid"); output["message"] = chrome.i18n.getMessage("error_validation_date_time_format"); output["is_error"] = true; } else { // Check for special characters that are not allowed by the operating system - if (dateString.match(specialCharacters)){ + if (dateString.match(specialCharacters)) { isValidDateFormat = false; output["title"] = chrome.i18n.getMessage("error_title_invalid"); @@ -76,9 +76,9 @@ function ValidateDateTimeFormat(settingsKey, dateString){ output["is_error"] = false; } - } + } - if (output.is_error){ + if (output.is_error) { return output; } else { Settings.Save(settingsKey, dateString); @@ -93,10 +93,10 @@ function ValidateDateTimeFormat(settingsKey, dateString){ * @param {*} prefixString * @returns */ -function ValidatePrefix(settingsKey, prefixString){ +function ValidatePrefix(settingsKey, prefixString) { let output = {}; - if (prefixString.match(specialCharacters)){ + if (prefixString.match(specialCharacters)) { output["title"] = chrome.i18n.getMessage("error_title_invalid"); output["message"] = chrome.i18n.getMessage("error_validation_prefix"); output["is_error"] = true; @@ -104,7 +104,7 @@ function ValidatePrefix(settingsKey, prefixString){ output["is_error"] = false; } - if (output.is_error){ + if (output.is_error) { return output; } else { Settings.Save(settingsKey, prefixString); @@ -128,6 +128,9 @@ document.onreadystatechange = () => { case "global_notifications_updated": document.getElementById("general_settings_notification_updated").checked = x.value break; + case "global_use_autorename_folder": + document.getElementById("general_settings_use_autorename_folder").checked = x.value; + break; } }); @@ -167,6 +170,10 @@ document.onreadystatechange = () => { case "twitter_settings_custom_prefix": document.getElementById("twitter_settings_custom_prefix").value = x.value; break; + + case "twitter_save_image_to_folder_based_on_username": + document.getElementById("twitter_settings_save_to_folder_by_username").checked = x.value; + break; } }); @@ -249,7 +256,7 @@ document.onreadystatechange = () => { } - }); + }); } } @@ -260,19 +267,44 @@ document.onreadystatechange = () => { * Perform execution after page has loaded * */ -document.addEventListener("DOMContentLoaded", (()=>{ +document.addEventListener("DOMContentLoaded", (() => { //#region Tab Switching Logic const tabs = document.querySelectorAll("[data-tab-target]"); const tabContents = document.querySelectorAll("[data-tab-content]"); - tabs.forEach((tab) => { - tab.addEventListener("click", () => { - const target = document.querySelector(tab.dataset.tabTarget); - tabContents.forEach(tabContent => tabContent.classList.remove("active")); - tabs.forEach(tabContent => tabContent.classList.remove("active")); - tab.classList.add("active"); - target.classList.add("active"); + const optionsConfig = Settings.Load().OptionsUI.map((data)=>{ + return { + "key": data.key, + "value": data.value + } + }).reduce((obj, data)=>{ + obj[data.key] = data; + return obj; + }, {}); + + createDownloadCardItem(); + + tabs.forEach((tab, idx) => { + + if (idx == optionsConfig["optionsUITabIndexNumber"].value){ + const target = document.querySelector(optionsConfig["optionsUITabName"].value); + tabContents.forEach(tabContent => tabContent.classList.remove("active")); + tabs.forEach(tabContent => tabContent.classList.remove("active")); + tab.classList.add("active"); + target.classList.add("active"); + + } + + tab.addEventListener("click", () => { + const target = document.querySelector(tab.dataset.tabTarget); + tabContents.forEach(tabContent => tabContent.classList.remove("active")); + tabs.forEach(tabContent => tabContent.classList.remove("active")); + tab.classList.add("active"); + target.classList.add("active"); + + Settings.Save("optionsUITabName", tab.dataset.tabTarget); + Settings.Save("optionsUITabIndexNumber", idx); }); }); @@ -304,65 +336,71 @@ document.addEventListener("DOMContentLoaded", (()=>{ */ switch (buttons.id) { - + + // case "download-primary": + // buttons.addEventListener("click", (() => { + // alert("TEST " + buttons.id) + // })); + // break; + case "button_help_twitter": - buttons.addEventListener("click", (()=>{ + buttons.addEventListener("click", (() => { chrome.tabs.create({ url: "https://github.com/ddasutein/AutoRename/wiki/%E2%9A%99-Settings#twitter" }) })); break; - + case "button_help_lineblog": - buttons.addEventListener("click", (()=>{ + buttons.addEventListener("click", (() => { chrome.tabs.create({ url: "https://github.com/ddasutein/AutoRename/wiki/%E2%9A%99-Settings#line-blog" }) })); break; - + case "button_help_reddit": - buttons.addEventListener("click", (()=>{ + buttons.addEventListener("click", (() => { chrome.tabs.create({ url: "https://github.com/ddasutein/AutoRename/wiki/%E2%9A%99-Settings#reddit" }) })); break; - case "button_save_general": buttons.addEventListener("click", (() => { Settings.Save("global_show_download_folder", document.getElementById("general_settings_auto_show_download_folder").checked); Settings.Save("global_enable_save_as_window", document.getElementById("general_settings_enable_save_as_window").checked); Settings.Save("global_notifications_updated", document.getElementById("general_settings_notification_updated").checked); + Settings.Save("global_use_autorename_folder", document.getElementById("general_settings_use_autorename_folder").checked) messageBox.Save(); })); - + break; - + case "button_save_twitter": - + buttons.addEventListener("click", (() => { try { - - if (temp.length > 0){ + + if (temp.length > 0) { temp = []; errorCode = {}; } - if (document.getElementById("twitter_settings_include_date_checkbox").checked && !document.getElementById("twitter_settings_prefer_locale_format").checked && document.getElementById("twitter_settings_select_date_format").value == "custom"){ + if (document.getElementById("twitter_settings_include_date_checkbox").checked && !document.getElementById("twitter_settings_prefer_locale_format").checked && document.getElementById("twitter_settings_select_date_format").value == "custom") { temp.push(ValidateDateTimeFormat("twitter_settings_custom_date_format", document.getElementById("twitter_settings_custom_date_format").value)); } temp.push(ValidatePrefix("twitter_settings_custom_prefix", document.getElementById("twitter_settings_custom_prefix").value)); - temp.forEach((x)=>{ - if (x.is_error == true){ + temp.forEach((x) => { + if (x.is_error == true) { errorCode["title"] = x.title; errorCode["message"] = x.message; throw errorCode; - } + } }); - + Settings.Save("twitter_include_mention_symbol", document.getElementById("twitter_settings_include_mention_symbol").checked); Settings.Save("twitter_include_tweet_id", document.getElementById("twitter_settings_include_tweet_id").checked); Settings.Save("twitter_include_date", document.getElementById("twitter_settings_include_date_checkbox").checked); @@ -370,46 +408,46 @@ document.addEventListener("DOMContentLoaded", (()=>{ Settings.Save("twitter_random_string_length", document.getElementById("twitter_settings_string_length").value); Settings.Save("twitter_include_website_title", document.getElementById("twitter_settings_site_title").checked); Settings.Save("twitter_prefer_locale_format", document.getElementById("twitter_settings_prefer_locale_format").checked); + Settings.Save("twitter_save_image_to_folder_based_on_username", document.getElementById("twitter_settings_save_to_folder_by_username").checked); // See dev note #1 Settings.Save("twitter_settings_custom_date_format", document.getElementById("twitter_settings_custom_date_format").value); messageBox.Save(); - - } catch(e){ + + } catch (e) { console.error(e); messageBox.Warning(e.title, e.message); } - + })); - + break; - + case "button_save_lineblog": buttons.addEventListener("click", (() => { - + try { - if (temp.length > 0){ + if (temp.length > 0) { temp = []; errorCode = {}; } - if (document.getElementById("lineblog_settings_include_date").checked && !document.getElementById("lineblog_settings_prefer_locale_format").checked && document.getElementById("lineblog_settings_select_date_format").value == "custom"){ + if (document.getElementById("lineblog_settings_include_date").checked && !document.getElementById("lineblog_settings_prefer_locale_format").checked && document.getElementById("lineblog_settings_select_date_format").value == "custom") { temp.push(ValidateDateTimeFormat("lineblogCustomDateFormat", document.getElementById("lineblog_settings_custom_date_format").value)); } - + temp.push(ValidatePrefix("lineblogCustomPrefix", document.getElementById("lineblog_settings_custom_prefix").value)); - temp.forEach((x)=>{ - if (x.is_error == true){ + temp.forEach((x) => { + if (x.is_error == true) { errorCode["title"] = x.title; errorCode["message"] = x.message; throw errorCode; - } + } }); - console.log( document.getElementById("lineblog_settings_custom_date_format").value) Settings.Save("lbPrefIncludeWebsiteTitle", document.getElementById("lineblog_settings_include_site_title").checked); Settings.Save("lbPrefIncludeBlogTitle", document.getElementById("lineblog_include_blog_title").checked); Settings.Save("lbPrefUseDate", document.getElementById("lineblog_settings_include_date").checked); @@ -417,45 +455,45 @@ document.addEventListener("DOMContentLoaded", (()=>{ Settings.Save("lineblog_convert_title_romaji", document.getElementById("lineblog_settings_convert_title_romaji").checked); Settings.Save("lineblogDateFormat", document.getElementById("lineblog_settings_select_date_format").value); Settings.Save("lineblogCustomPrefix", document.getElementById("lineblog_settings_custom_prefix").value); - + // See dev note #1 Settings.Save("lineblogCustomDateFormat", document.getElementById("lineblog_settings_custom_date_format").value); messageBox.Save(); - - } catch(e){ + + } catch (e) { console.error(e) messageBox.Warning(e.title, e.message); } - + })); - + break; - + case "button_save_reddit": buttons.addEventListener("click", (() => { try { - if (temp.length > 0){ + if (temp.length > 0) { temp = []; errorCode = {}; } - if (document.getElementById("reddit_settings_include_date").checked && !document.getElementById("reddit_settings_prefer_locale_format").checked &&document.getElementById("reddit_settings_custom_date_format").value == "custom"){ + if (document.getElementById("reddit_settings_include_date").checked && !document.getElementById("reddit_settings_prefer_locale_format").checked && document.getElementById("reddit_settings_custom_date_format").value == "custom") { ValidateDateTimeFormat("redditCustomDateFormat", document.getElementById("reddit_settings_custom_date_format").value); } temp.push(ValidatePrefix("redditCustomPrefix", document.getElementById("reddit_settings_custom_prefix").value)); - temp.forEach((x)=>{ - if (x.is_error == true){ + temp.forEach((x) => { + if (x.is_error == true) { errorCode["title"] = x.title; errorCode["message"] = x.message; throw errorCode; - } + } }); - + Settings.Save("redditIncludeWebsite", document.getElementById("reddit_settings_site_title").checked); Settings.Save("redditIncludePostID", document.getElementById("reddit_settings_subreddit_post_id").checked); Settings.Save("redditIncludeDate", document.getElementById("reddit_settings_include_date").checked); @@ -467,15 +505,15 @@ document.addEventListener("DOMContentLoaded", (()=>{ Settings.Save("redditCustomDateFormat", document.getElementById("reddit_settings_custom_date_format").value); messageBox.Save(); - } catch(e){ + } catch (e) { console.error(e); messageBox.Warning(e.title, e.message); } })); - + break; - + case "button_help_general": buttons.addEventListener("click", (() => { chrome.tabs.create({ @@ -483,7 +521,6 @@ document.addEventListener("DOMContentLoaded", (()=>{ }); })); break; - case "button_help_twitter": buttons.addEventListener("click", (() => { chrome.tabs.create({ @@ -491,7 +528,6 @@ document.addEventListener("DOMContentLoaded", (()=>{ }); })); break; - case "button_help_lineblog": buttons.addEventListener("click", (() => { chrome.tabs.create({ @@ -499,7 +535,6 @@ document.addEventListener("DOMContentLoaded", (()=>{ }); })); break; - case "button_help_reddit": buttons.addEventListener("click", (() => { chrome.tabs.create({ @@ -507,16 +542,120 @@ document.addEventListener("DOMContentLoaded", (()=>{ }); })); break; - - + + case "button_download_all": + buttons.addEventListener("click", (()=>{ + let isDone = false; + function createZip(zipName){ + + let downloadJSONData = Settings.Load().General; + downloadJSONData = downloadJSONData.filter((x) => x.key == "global_download_queue_data").map((x) => x.value)[0]; + if (typeof downloadJSONData == "string" && downloadJSONData.length == 0) { + return swal({ + title: chrome.i18n.getMessage("downloads_section_dialog_empty_queue_title"), + text: chrome.i18n.getMessage("downloads_section_empty_queue"), + icon: "error", + buttons: false, + dangerMode: false + }); + } + + downloadJSONData = JSON.parse(downloadJSONData); + + let zip = new JSZip(); + downloadJSONData.forEach((x)=>{ + zip.file(x.filename, urlToPromise(x.url), { binary: true}); + }); + zip.generateAsync({ + type: "blob",streamFiles: true + }, function updateCallback(metaData){ + + if(metaData.currentFile) { + + if (metaData.percent > 0){ + msg = `${chrome.i18n.getMessage("downloads_section_saving_file")} ${metaData.currentFile} ${chrome.i18n.getMessage("downloads_section_saving_file_2")} ${metaData.percent.toFixed(2)}%`; + } else if (metaData.percent == 0){ + msg = `${chrome.i18n.getMessage("downloads_section_saving_file_3")}` + } + + swal({ + title: chrome.i18n.getMessage("downloads_section_downlaod_in_progress"), + text: `${msg}`, + closeOnClickOutside: false, + buttons: false + }) + + } + + }).then(function callback(blob){ + saveAs(blob, zipName) + swal({ + title: chrome.i18n.getMessage("downloads_section_downlaod_complete_title"), + text: zipName, + icon: "success" + }).then(()=>{ + Settings.Save("global_download_queue_data", ""); + window.location.reload(); + DownloadManager.UpdateBadge(); + Utility.SetBadgeText(0); + }); + + }); + return isDone; + } + + + swal({ + text: chrome.i18n.getMessage("downloads_section_dialog_enter_zip_name"), + content: "input", + button: { + text: chrome.i18n.getMessage("downloads_section_dialog_enter_zip_name_button_create"), + closeModal: false, + }, + }) + .then(name => { + if (!name) throw null; + + return createZip(name); + }).catch(error =>{ + if (error == null){ + swal({ + title: chrome.i18n.getMessage("error_title"), + text: chrome.i18n.getMessage("error_download_queue_invalid_file_name"), + icon: "warning", + }) + } + console.error(error); + }) + + + })); + break; + + case "button_clear_queue": + buttons.addEventListener("click", (()=>{ + swal({ + title: "", + text: chrome.i18n.getMessage("downloads_section_dialog_queue_cleared"), + icon: "success", + buttons: false, + dangerMode: false, + timer: messageBox.autoCloseTimer + }); + DownloadManager.ClearDownloadQueue(); + window.location.reload(); + })); + break; + + } }); - document.querySelectorAll("input[type=checkbox]").forEach((input) => { - - switch (input.id){ + document.querySelectorAll("input[type=checkbox]").forEach((input) => { + + switch (input.id) { case "twitter_settings_include_date_checkbox": - input.addEventListener("change", (()=>{ + input.addEventListener("change", (() => { Utility.UpdateFieldDisplay("checkbox", input.checked, "twitter_settings_prefer_locale_format"); Utility.UpdateFieldDisplay("checkbox", input.checked, "twitter_settings_select_date_format"); @@ -526,7 +665,7 @@ document.addEventListener("DOMContentLoaded", (()=>{ break; case "lineblog_settings_include_date": - input.addEventListener("change", (()=>{ + input.addEventListener("change", (() => { Utility.UpdateFieldDisplay("checkbox", input.checked, "lineblog_settings_prefer_locale_format"); Utility.UpdateFieldDisplay("checkbox", input.checked, "lineblog_settings_select_date_format"); Utility.UpdateFieldDisplay("checkbox", input.checked, "lineblog_settings_custom_date_format"); @@ -534,7 +673,7 @@ document.addEventListener("DOMContentLoaded", (()=>{ break; case "reddit_settings_include_date": - input.addEventListener("change", (()=>{ + input.addEventListener("change", (() => { Utility.UpdateFieldDisplay("checkbox", input.checked, "reddit_settings_prefer_locale_format"); Utility.UpdateFieldDisplay("checkbox", input.checked, "reddit_settings_select_date_format"); Utility.UpdateFieldDisplay("checkbox", input.checked, "reddit_settings_custom_date_format"); @@ -543,4 +682,180 @@ document.addEventListener("DOMContentLoaded", (()=>{ } }); -})); \ No newline at end of file +})); + +function urlToPromise(url) { + return new Promise(function (resolve, reject) { + JSZipUtils.getBinaryContent(url, function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +function createDownloadCardItem(indexNumber, objData){ + + let download_card_container = document.getElementById("download_card_container"); + let download_card_container_history = document.getElementById("download_card_container_history"); + let download_queue_label = document.getElementById("download-queue-label"); + download_queue_label.textContent = `${chrome.i18n.getMessage("downloads_section_count_label")} (0)`; + + let downloadJSONData = Settings.Load().General; + downloadJSONData = downloadJSONData.filter((x) => x.key == "global_download_queue_data").map((x) => x.value)[0]; + + /** + * This scenario triggers on a fresh installation as by default, it is a string value. + */ + if (typeof downloadJSONData == "string" && downloadJSONData.length == 0){ + download_card_container.innerHTML += `

${chrome.i18n.getMessage("downloads_section_empty_queue")}

`; + return; + } + + downloadJSONData = JSON.parse(downloadJSONData); + if (downloadJSONData.length == 0){ + download_card_container.innerHTML += `

${chrome.i18n.getMessage("downloads_section_empty_queue")}

`; + return; + } + + + download_queue_label.textContent = `${chrome.i18n.getMessage("downloads_section_count_label")} (${downloadJSONData.length})`; + + let buttonIds = []; + downloadJSONData.forEach((x, idx)=>{ + + buttonIds.push({ + primary: `download-primary-${idx}`, + secondary: `download-secondary-${idx}`, + download_card_ids: `download-card-${idx}`, + index_count: idx + }); + download_card_container.innerHTML += ` +
+ +
+
${x.website}
+
${x.filename}
+
+
+ + +
+
+ `; + }); + updateDownloadButtonListeners(buttonIds); + buttonIds = []; // Clear when done + +} + +function createDownloadHistoryCardItem(indexNumber, objData){ + + let download_card_container = document.getElementById("download_card_container_history"); + let download_card_container_history = document.getElementById("download_card_container_history"); + let download_queue_label = document.getElementById("download-queue-label"); + download_queue_label.textContent = `${chrome.i18n.getMessage("downloads_section_count_label")} (0)`; + + let downloadJSONData = Settings.Load().General; + downloadJSONData = downloadJSONData.filter((x) => x.key == "global_download_history_data").map((x) => x.value)[0]; + + /** + * This scenario triggers on a fresh installation as by default, it is a string value. + */ + if (typeof downloadJSONData == "string" && downloadJSONData.length == 0){ + download_card_container.innerHTML += `

${chrome.i18n.getMessage("downloads_section_empty_queue")}

`; + return; + } + + downloadJSONData = JSON.parse(downloadJSONData); + if (downloadJSONData.length == 0){ + download_card_container.innerHTML += `

${chrome.i18n.getMessage("downloads_section_empty_queue")}

`; + return; + } + + + download_queue_label.textContent = `${chrome.i18n.getMessage("downloads_section_count_label")} (${downloadJSONData.length})`; + + let buttonIds = []; + downloadJSONData.forEach((x, idx)=>{ + + buttonIds.push({ + primary: `download-primary-${idx}`, + secondary: `download-secondary-${idx}`, + download_card_ids: `download-card-${idx}`, + index_count: idx + }); + download_card_container.innerHTML += ` +
+ +
+
${x.website}
+
${x.filename}
+
+
+ + +
+
+ `; + }); + updateDownloadButtonListeners(buttonIds); + buttonIds = []; // Clear when done + +} + +function updateDownloadButtonListeners(downloadBtns){ + + let downloadJSONData = Settings.Load().General; + downloadJSONData = downloadJSONData.filter((x) => x.key == "global_download_queue_data").map((x) => x.value)[0]; + downloadJSONData = JSON.parse(downloadJSONData); + if (downloadJSONData.length == 0 || typeof downloadJSONData != "object") return; + + downloadBtns.forEach((x)=>{ + + document.querySelectorAll("button").forEach((buttons) => { + + if (x.secondary == buttons.id){ + buttons.addEventListener("click", ((e) => { + + swal({ + title: "Remove from Queue", + text: "Are you sure you want to remove this file?", + icon: "warning", + buttons: { + cancel: true, + confirm: true + } + }).then((result)=>{ + if (result){ + let id = e.target.id; + id = id.split("-")[2]; // Temp workaround as for some reason, there is no value despite it being entered in the for-loop + + downloadJSONData = downloadJSONData.filter((v,idx)=>idx != +id); + Settings.Save("global_download_queue_data", JSON.stringify(downloadJSONData)); + Utility.SetBadgeText(downloadJSONData.length); + window.location.reload(); + } + }).catch((error)=> console.error(error)); + })); + } + + if (x.primary == buttons.id){ + buttons.addEventListener("click", ((e) => { + data = downloadJSONData.filter((v,idx)=>idx == e.target.value)[0]; + DownloadManager.StartDownload([{filename: data.filename, url: data.url}]); + + // Remove once downloaded + data = downloadJSONData.filter((v, idx) => idx != e.target.value); + Settings.Save("global_download_queue_data", JSON.stringify(data)); + + })); + } + + }); + + }); + +} diff --git a/js/Runtime.js b/js/Runtime.js index e7b8a46..b08e62a 100644 --- a/js/Runtime.js +++ b/js/Runtime.js @@ -30,7 +30,7 @@ function ShowUpdateMsg(){ title: chrome.i18n.getMessage("message_toast_button_ok"), } ], - "iconUrl": chrome.extension.getURL("assets/autorename-128px.png"), + "iconUrl": chrome.runtime.getURL("assets/autorename-128px.png"), "type": "basic", "title": getExtensionName, "message": chrome.i18n.getMessage("message_toast_new_version") + " " + getExtensionVersion @@ -46,7 +46,7 @@ function ShowWelcomeMsg(){ }, { title: chrome.i18n.getMessage("message_toast_button_ok") }], - "iconUrl": chrome.extension.getURL("assets/autorename-128px.png"), + "iconUrl": chrome.runtime.getURL("assets/autorename-128px.png"), "type": "basic", "title": `${chrome.i18n.getMessage("message_toast_welcome_title")} ${getExtensionName}`, "message": `${chrome.i18n.getMessage("message_toast_welcome_body")}` diff --git a/js/SaveAsEventHandle.js b/js/SaveAsEventHandle.js index d784a02..0e6e863 100644 --- a/js/SaveAsEventHandle.js +++ b/js/SaveAsEventHandle.js @@ -20,7 +20,8 @@ const contextMenuId = { saveImage: "saveImage", saveImageWithCustomPrefix: "saveImageWithCustomPrefix", - viewOriginalImage: "viewOriginalImage" + viewOriginalImage: "viewOriginalImage", + addDownloadQueue: "downloadqueue" } chrome.runtime.onInstalled.addListener(()=>{ @@ -30,13 +31,19 @@ chrome.runtime.onInstalled.addListener(()=>{ id: contextMenuId.saveImage, title: chrome.i18n.getMessage("context_menu_save_image_as"), contexts: ["image"] - }); + }, () => chrome.runtime.lastError ); chrome.contextMenus.create({ id: contextMenuId.saveImageWithCustomPrefix, title: "Save image as (AutoRename) with Prefix", contexts: ["image"] - }); + }, () => chrome.runtime.lastError ); + + chrome.contextMenus.create({ + id: contextMenuId.addDownloadQueue, + title: "Add to Download Queue", + contexts: ["image"] + }, () => chrome.runtime.lastError ); //#endregion @@ -45,8 +52,12 @@ chrome.runtime.onInstalled.addListener(()=>{ id: contextMenuId.viewOriginalImage, title: chrome.i18n.getMessage("context_menu_view_original_image"), contexts: ["image"] - }); + }, () => chrome.runtime.lastError ); + //#endregion + + DownloadManager.UpdateBadge(); + }); //#endregion @@ -80,7 +91,9 @@ let BrowserTabInfo = { chrome.tabs.onActivated.addListener((activeInfo) => { DevMode ? console.log("tabs onActivated") : ""; - QueryTab(); + console.log(activeInfo.tabId) + + QueryTab(activeInfo); }); @@ -97,38 +110,41 @@ chrome.tabs.onUpdated.addListener((tabId, selectInfo) => { }); -/** - * Queries tab data - */ -function QueryTab() { - setTimeout(() => { +chrome.action.setBadgeBackgroundColor({ + color: "#181818" +}); - chrome.tabs.query({ - active: true, - currentWindow: true - }, ((tabs)=>{ - let url; - let title; +let count = 0; +/** + * Queries tab data + */ +function QueryTab(tabData) { - if (tabs[0] != undefined){ - DevMode ? console.log(BrowserTabInfo) : ""; - url = tabs[0].url; - title = tabs[0].title; - } + chrome.tabs.query({ + active: true, + currentWindow: true + }, ((tabs)=>{ - url = url.split("/"); - url = url[2]; - DevMode ? console.log(url) : ""; - UpdateContextMenus(url); + let url; + let title; - BrowserTabInfo.Title = title; - BrowserTabInfo.URL = url; + if (tabs[0] != undefined){ DevMode ? console.log(BrowserTabInfo) : ""; - })); + url = tabs[0].url; + title = tabs[0].title; + } - }, 500); + url = url.split("/"); + url = url[2]; + DevMode ? console.log(url) : ""; + UpdateContextMenus(url, tabs[0].url); + + BrowserTabInfo.Title = title; + BrowserTabInfo.URL = url; + DevMode ? console.log(BrowserTabInfo) : ""; + })); }; @@ -139,10 +155,25 @@ function QueryTab() { * * @param {string} url URL of the website */ -function UpdateContextMenus(url) { +function UpdateContextMenus(url, urlFull) { switch(url){ case Website.Twitter: + + if (urlFull.includes("messages")){ + chrome.contextMenus.update(contextMenuId.saveImage, { + visible: false + }); + + chrome.contextMenus.update(contextMenuId.viewOriginalImage, { + visible: false + }); + + chrome.contextMenus.update(contextMenuId.saveImageWithCustomPrefix, { + visible: false + }); + return; + } chrome.contextMenus.update(contextMenuId.saveImage, { visible: true @@ -159,6 +190,21 @@ function UpdateContextMenus(url) { case Website.Mobile_Twitter: + if (urlFull.includes("messages")){ + chrome.contextMenus.update(contextMenuId.saveImage, { + visible: false + }); + + chrome.contextMenus.update(contextMenuId.viewOriginalImage, { + visible: false + }); + + chrome.contextMenus.update(contextMenuId.saveImageWithCustomPrefix, { + visible: false + }); + return; + } + chrome.contextMenus.update(contextMenuId.saveImage, { visible: true }); @@ -270,20 +316,28 @@ chrome.contextMenus.onClicked.addListener(function (info, tab) { currentUrl = currentUrl.split("/"); currentUrl = currentUrl[2]; + let data = {}; + data["tab_url"] = tab.url; + data["info_url"] = info.srcUrl; + data["link_url"] = info.linkUrl; + data["use_prefix"] = false; + data["download_queue"] = false; + console.log(data); switch (currentUrl) { case Website.Twitter: if (info.menuItemId === contextMenuId.viewOriginalImage){ - ViewOriginalMedia(info.srcUrl); + Twitter.ViewOriginalImage(data); return; } - else if (info.menuItemId === contextMenuId.saveImage){ - temp["use_prefix"] = false; - SaveTwitterMedia(tab.url, info.srcUrl, info.linkUrl, temp); - + else if (info.menuItemId === contextMenuId.saveImage) { + Twitter.SaveMedia(data); } else if (info.menuItemId == contextMenuId.saveImageWithCustomPrefix){ - temp["use_prefix"] = true; - SaveTwitterMedia(tab.url, info.srcUrl, info.linkUrl, temp) + data.use_prefix = true; + Twitter.SaveMedia(data); + } else if ( info.menuItemId === contextMenuId.addDownloadQueue){ + data.download_queue = true; + Twitter.SaveMedia(data); } break; @@ -305,6 +359,10 @@ chrome.contextMenus.onClicked.addListener(function (info, tab) { } else if (info.menuItemId == contextMenuId.saveImageWithCustomPrefix){ temp["use_prefix"] = true; SaveLINEBlogMedia(tab.url, info.srcUrl, temp); + } else if (info.menuItemId == contextMenuId.addDownloadQueue){ + temp["use_prefix"] = false; + temp["download_queue"] = true; + SaveLINEBlogMedia(tab.url, info.srcUrl, info.linkUrl, temp); } break; diff --git a/js/Settings.js b/js/Settings.js index 8f19034..3607962 100644 --- a/js/Settings.js +++ b/js/Settings.js @@ -1,6 +1,6 @@ /** MIT License * - * Copyright (c) 2022 Dasutein + * Copyright (c) 2023 Dasutein * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, @@ -22,7 +22,8 @@ */ let Settings = {}; -function StartSettingsService() { +(()=>{ + console.log("Settings service started") let SettingsMap = []; @@ -30,7 +31,8 @@ function StartSettingsService() { General: "General", LINE_BLOG: "LINE BLOG", Twitter: "Twitter", - Reddit: "Reddit" + Reddit: "Reddit", + Options: "OptionsUI" } chrome.storage.local.get({ @@ -47,6 +49,9 @@ function StartSettingsService() { global_show_download_folder: false, global_enable_save_as_window: true, global_notifications_updated: true, + global_use_autorename_folder: false, + global_download_queue_data: "", + global_download_history_data: "", //#endregion //#region Twitter Settings @@ -59,6 +64,7 @@ function StartSettingsService() { twitter_date_format: "custom", twitter_settings_custom_date_format: "", twitter_settings_custom_prefix: "", + twitter_save_image_to_folder_based_on_username: false, //#endregion //#region LINE BLOG Settings @@ -82,6 +88,10 @@ function StartSettingsService() { redditDateFormat: "custom", redditCustomDateFormat: "", redditCustomPrefix: "", + + //#region Options UI settings + optionsUITabName: "#general", + optionsUITabIndexNumber: 2 // Default //#endregion @@ -107,6 +117,21 @@ function StartSettingsService() { name: chrome.i18n.getMessage("general_settings_enable_notifications_on_update"), value: items.global_notifications_updated, key: "global_notifications_updated" + }, { + category: Category.General, + name: "Save image to AutoRename folder", + value: items.global_use_autorename_folder, + key: "global_use_autorename_folder" + }, { + category: Category.General, + name: "Download Queue", + value: items.global_download_queue_data, + key: "global_download_queue_data" + }, { + category: Category.General, + name: "Download History", + value: items.global_download_history_data, + key: "global_download_history_data" }, //#endregion @@ -157,6 +182,11 @@ function StartSettingsService() { name: chrome.i18n.getMessage("common_section_custom_prefix"), value: items.twitter_settings_custom_prefix, key: "twitter_settings_custom_prefix" + }, { + category: Category.Twitter, + name: "Save image to folder based on username", + value: items.twitter_save_image_to_folder_based_on_username, + key: "twitter_save_image_to_folder_based_on_username" }, //#endregion @@ -252,6 +282,20 @@ function StartSettingsService() { key: "redditCustomPrefix" }, //#endregion + + //#region Options UI + { + category: Category.Options, + name: "Options Tab", + value: items.optionsUITabName, + key: "optionsUITabName" + }, + { + category: Category.Options, + name: "Options Tab Index", + value: items.optionsUITabIndexNumber, + key: "optionsUITabIndexNumber" + } ); }); @@ -296,9 +340,20 @@ function StartSettingsService() { }), Reddit: SettingsMap.filter((key)=>{ return key.category == Category.Reddit; + }), + OptionsUI: SettingsMap.filter((key)=>{ + return key.category == Category.Options; + }), + DownloadQueue: SettingsMap.filter((key)=>{ + return key.category == Category.General && key.key == "global_download_queue_data"; + }).map((x,idx,arr)=>{ + console.log(arr.length) + if (arr.length > 1){ + return JSON.parse(arr[0].value); + } else { + return null + } }) } }); - -}; -StartSettingsService(); \ No newline at end of file +})(); diff --git a/js/Twitter/TwitterContent.js b/js/Twitter/TwitterContent.js index c5c0228..83ad5e8 100644 --- a/js/Twitter/TwitterContent.js +++ b/js/Twitter/TwitterContent.js @@ -1,6 +1,6 @@ /** MIT License * - * Copyright (c) 2022 Dasutein + * Copyright (c) 2023 Dasutein * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, @@ -12,73 +12,13 @@ * */ -/** - * Twitter supports various image sizes. When using `original`, - * the extension can save images up to 4096x4096 resolution - */ - const size = { - small: "&name=small", - medium: "&name=medium", - large: "&name=large", - original: "&name=orig" -} - -/** - * Return if the image is either a JPG or PNG - * - * @param {string} url - */ -let getImageFormat = ((url) => "." + url.split("?format=")[1].substring(0, 3)); - -/** - * View the Twitter image in it's original size - * - * @param {string} url - */ -function ViewOriginalMedia(url) { - - let updatedUrl = null; - - const twitterImageSize = url.substring(0, url.lastIndexOf("&name=") + 0); +var Twitter = { - if (url.includes("&name=")) { - updatedUrl = twitterImageSize + size.original - } else { - updatedUrl = url; - } - - chrome.tabs.create({ - url: updatedUrl - }); -} - -function SaveTwitterMedia(tabUrl, url, linkUrl, customObj){ - - if (Utility.SplitURL(tabUrl, 5) == null || Utility.SplitURL(tabUrl, 5).length == 0){ - - if (!linkUrl){ - alert(chrome.i18n.getMessage("error_tweet_detail_alert_prompt")); - return; - } - - } - - function buildFileName(fileNameObj){ + BuildFileName: ((twitterConfig, fileNameObj)=>{ let temp; temp = `{prefix}-{website_title}-{username}-{tweetId}-{date}-{randomstring}`; temp = temp.split("-"); - const twitterConfig = Settings.Load().Twitter.map((data)=>{ - return { - "key": data.key, - "value": data.value - } - }).reduce((obj, data)=>{ - obj[data.key] = data; - return obj; - }, {}); - - if (!twitterConfig["twitter_include_mention_symbol"].value){ temp[temp.indexOf("{username}")] = fileNameObj.username; } else { @@ -124,12 +64,13 @@ function SaveTwitterMedia(tabUrl, url, linkUrl, customObj){ } if (twitterConfig["twitter_random_string_length"].value == "0"){ - Utility.RemoveUnusedParameter(temp, "{randomstring}"); + temp[temp.indexOf("{randomstring}")] = fileNameObj.photo_count; + } else { temp[temp.indexOf("{randomstring}")] = Utility.GenerateRandomString(twitterConfig["twitter_random_string_length"].value); } - if (customObj.use_prefix == true){ + if (fileNameObj.use_prefix == true){ if (twitterConfig["twitter_settings_custom_prefix"].value == ""){ Utility.RemoveUnusedParameter(temp, "{prefix}"); @@ -142,42 +83,108 @@ function SaveTwitterMedia(tabUrl, url, linkUrl, customObj){ } return temp.toString().replace(/,/g, "-"); - } - - let twitterImageFile = []; - let fileNameObj = {}; - let twitterMediaSrc; - let tweetId; - const specialCharacters = /[?!@#$%^&*(),';:*-.]/g; - - // Rule 1: When full tweet is clicked then it should be prioritized first - if (!!tabUrl){ - if (specialCharacters.test(Utility.SplitURL(tabUrl, 5))){ - tweetId = Utility.SplitURL(tabUrl, 5).split(specialCharacters)[0]; + }), + + ImageFormatType : ((url) => "." + url.split("?format=")[1].substring(0, 3)), + + ImageSizeType : (()=> { + const size = [{ + small: "&name=small", + medium: "&name=medium", + large: "&name=large", + original: "&name=orig" + }]; + return size.map((x => x))[0]; + }), + + MediaSrc : ((linkUrl)=>{ + let src = ""; + src = linkUrl.substring(0, linkUrl.lastIndexOf("&name=") + 0) + Twitter.ImageSizeType().original + return src; + }), + + ViewOriginalImage : (async (url)=>{ + + if (url.info_url.includes("&name=")) { + updatedUrl = url.info_url.substring(0, url.info_url.lastIndexOf("&name=") + 0) + Twitter.ImageSizeType().original } else { - tweetId = Utility.SplitURL(tabUrl, 5); + updatedUrl = url.info_url; } - } - - // Rule 2: If Tweet ID is still empty then retrieve it from linkUrl - if (tweetId == "" || tweetId == undefined || tweetId == null){ - if (!!linkUrl){ - if (specialCharacters.test(Utility.SplitURL(linkUrl, 5))){ - tweetId = Utility.SplitURL(linkUrl, 5).split(specialCharacters)[0]; + Utility.CreateNewTab(updatedUrl) + + }), + + SaveMedia : ( (data)=>{ + + let filename = ""; + let tweetId; + let fileNameObj = {}; + + twitterConfig = Settings.Load().Twitter.map((data)=>{ + return { + "key": data.key, + "value": data.value + } + }).reduce((obj, data)=>{ + obj[data.key] = data; + return obj; + }, {}); + + generalSettings = Settings.Load().General.map((data)=>{ + return { + "key": data.key, + "value": data.value + } + }).reduce((obj, data)=>{ + obj[data.key] = data; + return obj; + }, {}); + + + const specialCharacters = /[?!@#$%^&*(),';:*-.]/g; + + // Rule 1: When full tweet is clicked then it should be prioritized first + if (!!data.tab_url){ + if (specialCharacters.test(Utility.SplitURL(data.tab_url, 5))){ + tweetId = Utility.SplitURL(data.tab_url, 5).split(specialCharacters)[0]; } else { - tweetId = Utility.SplitURL(linkUrl, 5); + tweetId = Utility.SplitURL(data.tab_url, 5); + } + } + + // Rule 2: If Tweet ID is still empty then retrieve it from linkUrl + if (tweetId == "" || tweetId == undefined || tweetId == null){ + if (!!data.link_url){ + if (specialCharacters.test(Utility.SplitURL(data.link_url, 5))){ + tweetId = Utility.SplitURL(data.link_url, 5).split(specialCharacters)[0]; + } else { + tweetId = Utility.SplitURL(data.link_url, 5); + } } } - } - fileNameObj["username"] = linkUrl != undefined ? Utility.SplitURL(linkUrl, 3) : Utility.SplitURL(tabUrl, 3); - fileNameObj["tweetId"] = tweetId; - twitterMediaSrc = url.substring(0, url.lastIndexOf("&name=") + 0) + size.original; - twitterImageFile.push({ - filename: buildFileName(fileNameObj) + getImageFormat(url), - url: twitterMediaSrc - }); - - DevMode ? console.log(twitterImageFile) : ""; - StartDownload(twitterImageFile); + + fileNameObj["username"] = data.link_url != undefined ? Utility.SplitURL(data.link_url, 3) : Utility.SplitURL(data.tab_url, 3); + fileNameObj["tweetId"] = tweetId; + fileNameObj["use_prefix"] = data.use_prefix; + fileNameObj["photo_count"] = data.link_url != undefined ? `img${Utility.SplitURL(data.link_url, 7)}` : "img1"; + filename = Twitter.BuildFileName(twitterConfig, fileNameObj) + Twitter.ImageFormatType(data.info_url); + + if (generalSettings["global_use_autorename_folder"].value == true && twitterConfig["twitter_save_image_to_folder_based_on_username"].value == true){ + filename = `AutoRename/Twitter/${fileNameObj.username}/${filename}` + } else if (generalSettings["global_use_autorename_folder"].value == true && twitterConfig["twitter_save_image_to_folder_based_on_username"].value == false){ + filename = `AutoRename/Twitter/${filename}` + } + + let twitterFileProp = []; + twitterFileProp.push({ + filename: filename, + url: Twitter.MediaSrc(data.info_url), + website: "Twitter", + + }); + + data.download_queue == false ? DownloadManager.StartDownload(twitterFileProp) : DownloadManager.AddDownloadQueue(twitterFileProp); + + }) } \ No newline at end of file diff --git a/lib/FileSaver/FileSaver.js b/lib/FileSaver/FileSaver.js new file mode 100644 index 0000000..54fc090 --- /dev/null +++ b/lib/FileSaver/FileSaver.js @@ -0,0 +1,188 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof exports !== "undefined") { + factory(); + } else { + var mod = { + exports: {} + }; + factory(); + global.FileSaver = mod.exports; + } +})(this, function () { + "use strict"; + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + };else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + + try { + xhr.send(); + } catch (e) {} + + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } // Detect WebView inside a native macOS app by ruling out all browsers + // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too + // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + + + var isMacOSWebView = /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = _global.saveAs || ( // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() {} + /* noop */ + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; + var a = document.createElement('a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url;else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url;else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }); + _global.saveAs = saveAs.saveAs = saveAs; + + if (typeof module !== 'undefined') { + module.exports = saveAs; + } +}); diff --git a/lib/JSZip/jszip-utils.js b/lib/JSZip/jszip-utils.js new file mode 100644 index 0000000..e98159b --- /dev/null +++ b/lib/JSZip/jszip-utils.js @@ -0,0 +1,160 @@ +/*@preserve + +JSZipUtils - A collection of cross-browser utilities to go along with JSZip. + + +(c) 2014-2019 Stuart Knightley, David Duponchel +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip-utils/master/LICENSE.markdown. + +*/ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.JSZipUtils=e():"undefined"!=typeof global?global.JSZipUtils=e():"undefined"!=typeof self&&(self.JSZipUtils=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o + +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. + +JSZip uses the library pako released under the MIT license : +https://github.com/nodeca/pako/blob/main/LICENSE +*/ + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.JSZip = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = remainingBytes > 1 ? (((chr2 & 15) << 2) | (chr3 >> 6)) : 64; + enc4 = remainingBytes > 2 ? (chr3 & 63) : 64; + + output.push(_keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4)); + + } + + return output.join(""); +}; + +// public method for decoding +exports.decode = function(input) { + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0, resultIndex = 0; + + var dataUrlPrefix = "data:"; + + if (input.substr(0, dataUrlPrefix.length) === dataUrlPrefix) { + // This is a common error: people give a data url + // (data:image/png;base64,iVBOR...) with a {base64: true} and + // wonders why things don't work. + // We can detect that the string input looks like a data url but we + // *can't* be sure it is one: removing everything up to the comma would + // be too dangerous. + throw new Error("Invalid base64 input, it looks like a data url."); + } + + input = input.replace(/[^A-Za-z0-9+/=]/g, ""); + + var totalLength = input.length * 3 / 4; + if(input.charAt(input.length - 1) === _keyStr.charAt(64)) { + totalLength--; + } + if(input.charAt(input.length - 2) === _keyStr.charAt(64)) { + totalLength--; + } + if (totalLength % 1 !== 0) { + // totalLength is not an integer, the length does not match a valid + // base64 content. That can happen if: + // - the input is not a base64 content + // - the input is *almost* a base64 content, with a extra chars at the + // beginning or at the end + // - the input uses a base64 variant (base64url for example) + throw new Error("Invalid base64 input, bad content length."); + } + var output; + if (support.uint8array) { + output = new Uint8Array(totalLength|0); + } else { + output = new Array(totalLength|0); + } + + while (i < input.length) { + + enc1 = _keyStr.indexOf(input.charAt(i++)); + enc2 = _keyStr.indexOf(input.charAt(i++)); + enc3 = _keyStr.indexOf(input.charAt(i++)); + enc4 = _keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output[resultIndex++] = chr1; + + if (enc3 !== 64) { + output[resultIndex++] = chr2; + } + if (enc4 !== 64) { + output[resultIndex++] = chr3; + } + + } + + return output; +}; + +},{"./support":30,"./utils":32}],2:[function(require,module,exports){ +"use strict"; + +var external = require("./external"); +var DataWorker = require("./stream/DataWorker"); +var Crc32Probe = require("./stream/Crc32Probe"); +var DataLengthProbe = require("./stream/DataLengthProbe"); + +/** + * Represent a compressed object, with everything needed to decompress it. + * @constructor + * @param {number} compressedSize the size of the data compressed. + * @param {number} uncompressedSize the size of the data after decompression. + * @param {number} crc32 the crc32 of the decompressed file. + * @param {object} compression the type of compression, see lib/compressions.js. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the compressed data. + */ +function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data) { + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + this.crc32 = crc32; + this.compression = compression; + this.compressedContent = data; +} + +CompressedObject.prototype = { + /** + * Create a worker to get the uncompressed content. + * @return {GenericWorker} the worker. + */ + getContentWorker: function () { + var worker = new DataWorker(external.Promise.resolve(this.compressedContent)) + .pipe(this.compression.uncompressWorker()) + .pipe(new DataLengthProbe("data_length")); + + var that = this; + worker.on("end", function () { + if (this.streamInfo["data_length"] !== that.uncompressedSize) { + throw new Error("Bug : uncompressed data size mismatch"); + } + }); + return worker; + }, + /** + * Create a worker to get the compressed content. + * @return {GenericWorker} the worker. + */ + getCompressedWorker: function () { + return new DataWorker(external.Promise.resolve(this.compressedContent)) + .withStreamInfo("compressedSize", this.compressedSize) + .withStreamInfo("uncompressedSize", this.uncompressedSize) + .withStreamInfo("crc32", this.crc32) + .withStreamInfo("compression", this.compression) + ; + } +}; + +/** + * Chain the given worker with other workers to compress the content with the + * given compression. + * @param {GenericWorker} uncompressedWorker the worker to pipe. + * @param {Object} compression the compression object. + * @param {Object} compressionOptions the options to use when compressing. + * @return {GenericWorker} the new worker compressing the content. + */ +CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions) { + return uncompressedWorker + .pipe(new Crc32Probe()) + .pipe(new DataLengthProbe("uncompressedSize")) + .pipe(compression.compressWorker(compressionOptions)) + .pipe(new DataLengthProbe("compressedSize")) + .withStreamInfo("compression", compression); +}; + +module.exports = CompressedObject; + +},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./stream/GenericWorker"); + +exports.STORE = { + magic: "\x00\x00", + compressWorker : function () { + return new GenericWorker("STORE compression"); + }, + uncompressWorker : function () { + return new GenericWorker("STORE decompression"); + } +}; +exports.DEFLATE = require("./flate"); + +},{"./flate":7,"./stream/GenericWorker":28}],4:[function(require,module,exports){ +"use strict"; + +var utils = require("./utils"); + +/** + * The following functions come from pako, from pako/lib/zlib/crc32.js + * released under the MIT license, see pako https://github.com/nodeca/pako/ + */ + +// Use ordinary array, since untyped makes no boost here +function makeTable() { + var c, table = []; + + for(var n =0; n < 256; n++){ + c = n; + for(var k =0; k < 8; k++){ + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + table[n] = c; + } + + return table; +} + +// Create table on load. Just 255 signed longs. Not a problem. +var crcTable = makeTable(); + + +function crc32(crc, buf, len, pos) { + var t = crcTable, end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++ ) { + crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + +// That's all for the pako functions. + +/** + * Compute the crc32 of a string. + * This is almost the same as the function crc32, but for strings. Using the + * same function for the two use cases leads to horrible performances. + * @param {Number} crc the starting value of the crc. + * @param {String} str the string to use. + * @param {Number} len the length of the string. + * @param {Number} pos the starting position for the crc32 computation. + * @return {Number} the computed crc32. + */ +function crc32str(crc, str, len, pos) { + var t = crcTable, end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++ ) { + crc = (crc >>> 8) ^ t[(crc ^ str.charCodeAt(i)) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + +module.exports = function crc32wrapper(input, crc) { + if (typeof input === "undefined" || !input.length) { + return 0; + } + + var isArray = utils.getTypeOf(input) !== "string"; + + if(isArray) { + return crc32(crc|0, input, input.length, 0); + } else { + return crc32str(crc|0, input, input.length, 0); + } +}; + +},{"./utils":32}],5:[function(require,module,exports){ +"use strict"; +exports.base64 = false; +exports.binary = false; +exports.dir = false; +exports.createFolders = true; +exports.date = null; +exports.compression = null; +exports.compressionOptions = null; +exports.comment = null; +exports.unixPermissions = null; +exports.dosPermissions = null; + +},{}],6:[function(require,module,exports){ +"use strict"; + +// load the global object first: +// - it should be better integrated in the system (unhandledRejection in node) +// - the environment may have a custom Promise implementation (see zone.js) +var ES6Promise = null; +if (typeof Promise !== "undefined") { + ES6Promise = Promise; +} else { + ES6Promise = require("lie"); +} + +/** + * Let the user use/change some implementations. + */ +module.exports = { + Promise: ES6Promise +}; + +},{"lie":37}],7:[function(require,module,exports){ +"use strict"; +var USE_TYPEDARRAY = (typeof Uint8Array !== "undefined") && (typeof Uint16Array !== "undefined") && (typeof Uint32Array !== "undefined"); + +var pako = require("pako"); +var utils = require("./utils"); +var GenericWorker = require("./stream/GenericWorker"); + +var ARRAY_TYPE = USE_TYPEDARRAY ? "uint8array" : "array"; + +exports.magic = "\x08\x00"; + +/** + * Create a worker that uses pako to inflate/deflate. + * @constructor + * @param {String} action the name of the pako function to call : either "Deflate" or "Inflate". + * @param {Object} options the options to use when (de)compressing. + */ +function FlateWorker(action, options) { + GenericWorker.call(this, "FlateWorker/" + action); + + this._pako = null; + this._pakoAction = action; + this._pakoOptions = options; + // the `meta` object from the last chunk received + // this allow this worker to pass around metadata + this.meta = {}; +} + +utils.inherits(FlateWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +FlateWorker.prototype.processChunk = function (chunk) { + this.meta = chunk.meta; + if (this._pako === null) { + this._createPako(); + } + this._pako.push(utils.transformTo(ARRAY_TYPE, chunk.data), false); +}; + +/** + * @see GenericWorker.flush + */ +FlateWorker.prototype.flush = function () { + GenericWorker.prototype.flush.call(this); + if (this._pako === null) { + this._createPako(); + } + this._pako.push([], true); +}; +/** + * @see GenericWorker.cleanUp + */ +FlateWorker.prototype.cleanUp = function () { + GenericWorker.prototype.cleanUp.call(this); + this._pako = null; +}; + +/** + * Create the _pako object. + * TODO: lazy-loading this object isn't the best solution but it's the + * quickest. The best solution is to lazy-load the worker list. See also the + * issue #446. + */ +FlateWorker.prototype._createPako = function () { + this._pako = new pako[this._pakoAction]({ + raw: true, + level: this._pakoOptions.level || -1 // default compression + }); + var self = this; + this._pako.onData = function(data) { + self.push({ + data : data, + meta : self.meta + }); + }; +}; + +exports.compressWorker = function (compressionOptions) { + return new FlateWorker("Deflate", compressionOptions); +}; +exports.uncompressWorker = function () { + return new FlateWorker("Inflate", {}); +}; + +},{"./stream/GenericWorker":28,"./utils":32,"pako":38}],8:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("../stream/GenericWorker"); +var utf8 = require("../utf8"); +var crc32 = require("../crc32"); +var signature = require("../signature"); + +/** + * Transform an integer into a string in hexadecimal. + * @private + * @param {number} dec the number to convert. + * @param {number} bytes the number of bytes to generate. + * @returns {string} the result. + */ +var decToHex = function(dec, bytes) { + var hex = "", i; + for (i = 0; i < bytes; i++) { + hex += String.fromCharCode(dec & 0xff); + dec = dec >>> 8; + } + return hex; +}; + +/** + * Generate the UNIX part of the external file attributes. + * @param {Object} unixPermissions the unix permissions or null. + * @param {Boolean} isDir true if the entry is a directory, false otherwise. + * @return {Number} a 32 bit integer. + * + * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute : + * + * TTTTsstrwxrwxrwx0000000000ADVSHR + * ^^^^____________________________ file type, see zipinfo.c (UNX_*) + * ^^^_________________________ setuid, setgid, sticky + * ^^^^^^^^^________________ permissions + * ^^^^^^^^^^______ not used ? + * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only + */ +var generateUnixExternalFileAttr = function (unixPermissions, isDir) { + + var result = unixPermissions; + if (!unixPermissions) { + // I can't use octal values in strict mode, hence the hexa. + // 040775 => 0x41fd + // 0100664 => 0x81b4 + result = isDir ? 0x41fd : 0x81b4; + } + return (result & 0xFFFF) << 16; +}; + +/** + * Generate the DOS part of the external file attributes. + * @param {Object} dosPermissions the dos permissions or null. + * @param {Boolean} isDir true if the entry is a directory, false otherwise. + * @return {Number} a 32 bit integer. + * + * Bit 0 Read-Only + * Bit 1 Hidden + * Bit 2 System + * Bit 3 Volume Label + * Bit 4 Directory + * Bit 5 Archive + */ +var generateDosExternalFileAttr = function (dosPermissions) { + // the dir flag is already set for compatibility + return (dosPermissions || 0) & 0x3F; +}; + +/** + * Generate the various parts used in the construction of the final zip file. + * @param {Object} streamInfo the hash with information about the compressed file. + * @param {Boolean} streamedContent is the content streamed ? + * @param {Boolean} streamingEnded is the stream finished ? + * @param {number} offset the current offset from the start of the zip file. + * @param {String} platform let's pretend we are this platform (change platform dependents fields) + * @param {Function} encodeFileName the function to encode the file name / comment. + * @return {Object} the zip parts. + */ +var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) { + var file = streamInfo["file"], + compression = streamInfo["compression"], + useCustomEncoding = encodeFileName !== utf8.utf8encode, + encodedFileName = utils.transformTo("string", encodeFileName(file.name)), + utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)), + comment = file.comment, + encodedComment = utils.transformTo("string", encodeFileName(comment)), + utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)), + useUTF8ForFileName = utfEncodedFileName.length !== file.name.length, + useUTF8ForComment = utfEncodedComment.length !== comment.length, + dosTime, + dosDate, + extraFields = "", + unicodePathExtraField = "", + unicodeCommentExtraField = "", + dir = file.dir, + date = file.date; + + + var dataInfo = { + crc32 : 0, + compressedSize : 0, + uncompressedSize : 0 + }; + + // if the content is streamed, the sizes/crc32 are only available AFTER + // the end of the stream. + if (!streamedContent || streamingEnded) { + dataInfo.crc32 = streamInfo["crc32"]; + dataInfo.compressedSize = streamInfo["compressedSize"]; + dataInfo.uncompressedSize = streamInfo["uncompressedSize"]; + } + + var bitflag = 0; + if (streamedContent) { + // Bit 3: the sizes/crc32 are set to zero in the local header. + // The correct values are put in the data descriptor immediately + // following the compressed data. + bitflag |= 0x0008; + } + if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) { + // Bit 11: Language encoding flag (EFS). + bitflag |= 0x0800; + } + + + var extFileAttr = 0; + var versionMadeBy = 0; + if (dir) { + // dos or unix, we set the dos dir flag + extFileAttr |= 0x00010; + } + if(platform === "UNIX") { + versionMadeBy = 0x031E; // UNIX, version 3.0 + extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir); + } else { // DOS or other, fallback to DOS + versionMadeBy = 0x0014; // DOS, version 2.0 + extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir); + } + + // date + // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html + // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html + // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html + + dosTime = date.getUTCHours(); + dosTime = dosTime << 6; + dosTime = dosTime | date.getUTCMinutes(); + dosTime = dosTime << 5; + dosTime = dosTime | date.getUTCSeconds() / 2; + + dosDate = date.getUTCFullYear() - 1980; + dosDate = dosDate << 4; + dosDate = dosDate | (date.getUTCMonth() + 1); + dosDate = dosDate << 5; + dosDate = dosDate | date.getUTCDate(); + + if (useUTF8ForFileName) { + // set the unicode path extra field. unzip needs at least one extra + // field to correctly handle unicode path, so using the path is as good + // as any other information. This could improve the situation with + // other archive managers too. + // This field is usually used without the utf8 flag, with a non + // unicode path in the header (winrar, winzip). This helps (a bit) + // with the messy Windows' default compressed folders feature but + // breaks on p7zip which doesn't seek the unicode path extra field. + // So for now, UTF-8 everywhere ! + unicodePathExtraField = + // Version + decToHex(1, 1) + + // NameCRC32 + decToHex(crc32(encodedFileName), 4) + + // UnicodeName + utfEncodedFileName; + + extraFields += + // Info-ZIP Unicode Path Extra Field + "\x75\x70" + + // size + decToHex(unicodePathExtraField.length, 2) + + // content + unicodePathExtraField; + } + + if(useUTF8ForComment) { + + unicodeCommentExtraField = + // Version + decToHex(1, 1) + + // CommentCRC32 + decToHex(crc32(encodedComment), 4) + + // UnicodeName + utfEncodedComment; + + extraFields += + // Info-ZIP Unicode Path Extra Field + "\x75\x63" + + // size + decToHex(unicodeCommentExtraField.length, 2) + + // content + unicodeCommentExtraField; + } + + var header = ""; + + // version needed to extract + header += "\x0A\x00"; + // general purpose bit flag + header += decToHex(bitflag, 2); + // compression method + header += compression.magic; + // last mod file time + header += decToHex(dosTime, 2); + // last mod file date + header += decToHex(dosDate, 2); + // crc-32 + header += decToHex(dataInfo.crc32, 4); + // compressed size + header += decToHex(dataInfo.compressedSize, 4); + // uncompressed size + header += decToHex(dataInfo.uncompressedSize, 4); + // file name length + header += decToHex(encodedFileName.length, 2); + // extra field length + header += decToHex(extraFields.length, 2); + + + var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields; + + var dirRecord = signature.CENTRAL_FILE_HEADER + + // version made by (00: DOS) + decToHex(versionMadeBy, 2) + + // file header (common to file and central directory) + header + + // file comment length + decToHex(encodedComment.length, 2) + + // disk number start + "\x00\x00" + + // internal file attributes TODO + "\x00\x00" + + // external file attributes + decToHex(extFileAttr, 4) + + // relative offset of local header + decToHex(offset, 4) + + // file name + encodedFileName + + // extra field + extraFields + + // file comment + encodedComment; + + return { + fileRecord: fileRecord, + dirRecord: dirRecord + }; +}; + +/** + * Generate the EOCD record. + * @param {Number} entriesCount the number of entries in the zip file. + * @param {Number} centralDirLength the length (in bytes) of the central dir. + * @param {Number} localDirLength the length (in bytes) of the local dir. + * @param {String} comment the zip file comment as a binary string. + * @param {Function} encodeFileName the function to encode the comment. + * @return {String} the EOCD record. + */ +var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) { + var dirEnd = ""; + var encodedComment = utils.transformTo("string", encodeFileName(comment)); + + // end of central dir signature + dirEnd = signature.CENTRAL_DIRECTORY_END + + // number of this disk + "\x00\x00" + + // number of the disk with the start of the central directory + "\x00\x00" + + // total number of entries in the central directory on this disk + decToHex(entriesCount, 2) + + // total number of entries in the central directory + decToHex(entriesCount, 2) + + // size of the central directory 4 bytes + decToHex(centralDirLength, 4) + + // offset of start of central directory with respect to the starting disk number + decToHex(localDirLength, 4) + + // .ZIP file comment length + decToHex(encodedComment.length, 2) + + // .ZIP file comment + encodedComment; + + return dirEnd; +}; + +/** + * Generate data descriptors for a file entry. + * @param {Object} streamInfo the hash generated by a worker, containing information + * on the file entry. + * @return {String} the data descriptors. + */ +var generateDataDescriptors = function (streamInfo) { + var descriptor = ""; + descriptor = signature.DATA_DESCRIPTOR + + // crc-32 4 bytes + decToHex(streamInfo["crc32"], 4) + + // compressed size 4 bytes + decToHex(streamInfo["compressedSize"], 4) + + // uncompressed size 4 bytes + decToHex(streamInfo["uncompressedSize"], 4); + + return descriptor; +}; + + +/** + * A worker to concatenate other workers to create a zip file. + * @param {Boolean} streamFiles `true` to stream the content of the files, + * `false` to accumulate it. + * @param {String} comment the comment to use. + * @param {String} platform the platform to use, "UNIX" or "DOS". + * @param {Function} encodeFileName the function to encode file names and comments. + */ +function ZipFileWorker(streamFiles, comment, platform, encodeFileName) { + GenericWorker.call(this, "ZipFileWorker"); + // The number of bytes written so far. This doesn't count accumulated chunks. + this.bytesWritten = 0; + // The comment of the zip file + this.zipComment = comment; + // The platform "generating" the zip file. + this.zipPlatform = platform; + // the function to encode file names and comments. + this.encodeFileName = encodeFileName; + // Should we stream the content of the files ? + this.streamFiles = streamFiles; + // If `streamFiles` is false, we will need to accumulate the content of the + // files to calculate sizes / crc32 (and write them *before* the content). + // This boolean indicates if we are accumulating chunks (it will change a lot + // during the lifetime of this worker). + this.accumulate = false; + // The buffer receiving chunks when accumulating content. + this.contentBuffer = []; + // The list of generated directory records. + this.dirRecords = []; + // The offset (in bytes) from the beginning of the zip file for the current source. + this.currentSourceOffset = 0; + // The total number of entries in this zip file. + this.entriesCount = 0; + // the name of the file currently being added, null when handling the end of the zip file. + // Used for the emitted metadata. + this.currentFile = null; + + + + this._sources = []; +} +utils.inherits(ZipFileWorker, GenericWorker); + +/** + * @see GenericWorker.push + */ +ZipFileWorker.prototype.push = function (chunk) { + + var currentFilePercent = chunk.meta.percent || 0; + var entriesCount = this.entriesCount; + var remainingFiles = this._sources.length; + + if(this.accumulate) { + this.contentBuffer.push(chunk); + } else { + this.bytesWritten += chunk.data.length; + + GenericWorker.prototype.push.call(this, { + data : chunk.data, + meta : { + currentFile : this.currentFile, + percent : entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100 + } + }); + } +}; + +/** + * The worker started a new source (an other worker). + * @param {Object} streamInfo the streamInfo object from the new source. + */ +ZipFileWorker.prototype.openedSource = function (streamInfo) { + this.currentSourceOffset = this.bytesWritten; + this.currentFile = streamInfo["file"].name; + + var streamedContent = this.streamFiles && !streamInfo["file"].dir; + + // don't stream folders (because they don't have any content) + if(streamedContent) { + var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + this.push({ + data : record.fileRecord, + meta : {percent:0} + }); + } else { + // we need to wait for the whole file before pushing anything + this.accumulate = true; + } +}; + +/** + * The worker finished a source (an other worker). + * @param {Object} streamInfo the streamInfo object from the finished source. + */ +ZipFileWorker.prototype.closedSource = function (streamInfo) { + this.accumulate = false; + var streamedContent = this.streamFiles && !streamInfo["file"].dir; + var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + + this.dirRecords.push(record.dirRecord); + if(streamedContent) { + // after the streamed file, we put data descriptors + this.push({ + data : generateDataDescriptors(streamInfo), + meta : {percent:100} + }); + } else { + // the content wasn't streamed, we need to push everything now + // first the file record, then the content + this.push({ + data : record.fileRecord, + meta : {percent:0} + }); + while(this.contentBuffer.length) { + this.push(this.contentBuffer.shift()); + } + } + this.currentFile = null; +}; + +/** + * @see GenericWorker.flush + */ +ZipFileWorker.prototype.flush = function () { + + var localDirLength = this.bytesWritten; + for(var i = 0; i < this.dirRecords.length; i++) { + this.push({ + data : this.dirRecords[i], + meta : {percent:100} + }); + } + var centralDirLength = this.bytesWritten - localDirLength; + + var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName); + + this.push({ + data : dirEnd, + meta : {percent:100} + }); +}; + +/** + * Prepare the next source to be read. + */ +ZipFileWorker.prototype.prepareNextSource = function () { + this.previous = this._sources.shift(); + this.openedSource(this.previous.streamInfo); + if (this.isPaused) { + this.previous.pause(); + } else { + this.previous.resume(); + } +}; + +/** + * @see GenericWorker.registerPrevious + */ +ZipFileWorker.prototype.registerPrevious = function (previous) { + this._sources.push(previous); + var self = this; + + previous.on("data", function (chunk) { + self.processChunk(chunk); + }); + previous.on("end", function () { + self.closedSource(self.previous.streamInfo); + if(self._sources.length) { + self.prepareNextSource(); + } else { + self.end(); + } + }); + previous.on("error", function (e) { + self.error(e); + }); + return this; +}; + +/** + * @see GenericWorker.resume + */ +ZipFileWorker.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if (!this.previous && this._sources.length) { + this.prepareNextSource(); + return true; + } + if (!this.previous && !this._sources.length && !this.generatedError) { + this.end(); + return true; + } +}; + +/** + * @see GenericWorker.error + */ +ZipFileWorker.prototype.error = function (e) { + var sources = this._sources; + if(!GenericWorker.prototype.error.call(this, e)) { + return false; + } + for(var i = 0; i < sources.length; i++) { + try { + sources[i].error(e); + } catch(e) { + // the `error` exploded, nothing to do + } + } + return true; +}; + +/** + * @see GenericWorker.lock + */ +ZipFileWorker.prototype.lock = function () { + GenericWorker.prototype.lock.call(this); + var sources = this._sources; + for(var i = 0; i < sources.length; i++) { + sources[i].lock(); + } +}; + +module.exports = ZipFileWorker; + +},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(require,module,exports){ +"use strict"; + +var compressions = require("../compressions"); +var ZipFileWorker = require("./ZipFileWorker"); + +/** + * Find the compression to use. + * @param {String} fileCompression the compression defined at the file level, if any. + * @param {String} zipCompression the compression defined at the load() level. + * @return {Object} the compression object to use. + */ +var getCompression = function (fileCompression, zipCompression) { + + var compressionName = fileCompression || zipCompression; + var compression = compressions[compressionName]; + if (!compression) { + throw new Error(compressionName + " is not a valid compression method !"); + } + return compression; +}; + +/** + * Create a worker to generate a zip file. + * @param {JSZip} zip the JSZip instance at the right root level. + * @param {Object} options to generate the zip file. + * @param {String} comment the comment to use. + */ +exports.generateWorker = function (zip, options, comment) { + + var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName); + var entriesCount = 0; + try { + + zip.forEach(function (relativePath, file) { + entriesCount++; + var compression = getCompression(file.options.compression, options.compression); + var compressionOptions = file.options.compressionOptions || options.compressionOptions || {}; + var dir = file.dir, date = file.date; + + file._compressWorker(compression, compressionOptions) + .withStreamInfo("file", { + name : relativePath, + dir : dir, + date : date, + comment : file.comment || "", + unixPermissions : file.unixPermissions, + dosPermissions : file.dosPermissions + }) + .pipe(zipFileWorker); + }); + zipFileWorker.entriesCount = entriesCount; + } catch (e) { + zipFileWorker.error(e); + } + + return zipFileWorker; +}; + +},{"../compressions":3,"./ZipFileWorker":8}],10:[function(require,module,exports){ +"use strict"; + +/** + * Representation a of zip file in js + * @constructor + */ +function JSZip() { + // if this constructor is used without `new`, it adds `new` before itself: + if(!(this instanceof JSZip)) { + return new JSZip(); + } + + if(arguments.length) { + throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide."); + } + + // object containing the files : + // { + // "folder/" : {...}, + // "folder/data.txt" : {...} + // } + // NOTE: we use a null prototype because we do not + // want filenames like "toString" coming from a zip file + // to overwrite methods and attributes in a normal Object. + this.files = Object.create(null); + + this.comment = null; + + // Where we are in the hierarchy + this.root = ""; + this.clone = function() { + var newObj = new JSZip(); + for (var i in this) { + if (typeof this[i] !== "function") { + newObj[i] = this[i]; + } + } + return newObj; + }; +} +JSZip.prototype = require("./object"); +JSZip.prototype.loadAsync = require("./load"); +JSZip.support = require("./support"); +JSZip.defaults = require("./defaults"); + +// TODO find a better way to handle this version, +// a require('package.json').version doesn't work with webpack, see #327 +JSZip.version = "3.10.1"; + +JSZip.loadAsync = function (content, options) { + return new JSZip().loadAsync(content, options); +}; + +JSZip.external = require("./external"); +module.exports = JSZip; + +},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(require,module,exports){ +"use strict"; +var utils = require("./utils"); +var external = require("./external"); +var utf8 = require("./utf8"); +var ZipEntries = require("./zipEntries"); +var Crc32Probe = require("./stream/Crc32Probe"); +var nodejsUtils = require("./nodejsUtils"); + +/** + * Check the CRC32 of an entry. + * @param {ZipEntry} zipEntry the zip entry to check. + * @return {Promise} the result. + */ +function checkEntryCRC32(zipEntry) { + return new external.Promise(function (resolve, reject) { + var worker = zipEntry.decompressed.getContentWorker().pipe(new Crc32Probe()); + worker.on("error", function (e) { + reject(e); + }) + .on("end", function () { + if (worker.streamInfo.crc32 !== zipEntry.decompressed.crc32) { + reject(new Error("Corrupted zip : CRC32 mismatch")); + } else { + resolve(); + } + }) + .resume(); + }); +} + +module.exports = function (data, options) { + var zip = this; + options = utils.extend(options || {}, { + base64: false, + checkCRC32: false, + optimizedBinaryString: false, + createFolders: false, + decodeFileName: utf8.utf8decode + }); + + if (nodejsUtils.isNode && nodejsUtils.isStream(data)) { + return external.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")); + } + + return utils.prepareContent("the loaded zip file", data, true, options.optimizedBinaryString, options.base64) + .then(function (data) { + var zipEntries = new ZipEntries(options); + zipEntries.load(data); + return zipEntries; + }).then(function checkCRC32(zipEntries) { + var promises = [external.Promise.resolve(zipEntries)]; + var files = zipEntries.files; + if (options.checkCRC32) { + for (var i = 0; i < files.length; i++) { + promises.push(checkEntryCRC32(files[i])); + } + } + return external.Promise.all(promises); + }).then(function addFiles(results) { + var zipEntries = results.shift(); + var files = zipEntries.files; + for (var i = 0; i < files.length; i++) { + var input = files[i]; + + var unsafeName = input.fileNameStr; + var safeName = utils.resolve(input.fileNameStr); + + zip.file(safeName, input.decompressed, { + binary: true, + optimizedBinaryString: true, + date: input.date, + dir: input.dir, + comment: input.fileCommentStr.length ? input.fileCommentStr : null, + unixPermissions: input.unixPermissions, + dosPermissions: input.dosPermissions, + createFolders: options.createFolders + }); + if (!input.dir) { + zip.file(safeName).unsafeOriginalName = unsafeName; + } + } + if (zipEntries.zipComment.length) { + zip.comment = zipEntries.zipComment; + } + + return zip; + }); +}; + +},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("../stream/GenericWorker"); + +/** + * A worker that use a nodejs stream as source. + * @constructor + * @param {String} filename the name of the file entry for this stream. + * @param {Readable} stream the nodejs stream. + */ +function NodejsStreamInputAdapter(filename, stream) { + GenericWorker.call(this, "Nodejs stream input adapter for " + filename); + this._upstreamEnded = false; + this._bindStream(stream); +} + +utils.inherits(NodejsStreamInputAdapter, GenericWorker); + +/** + * Prepare the stream and bind the callbacks on it. + * Do this ASAP on node 0.10 ! A lazy binding doesn't always work. + * @param {Stream} stream the nodejs stream to use. + */ +NodejsStreamInputAdapter.prototype._bindStream = function (stream) { + var self = this; + this._stream = stream; + stream.pause(); + stream + .on("data", function (chunk) { + self.push({ + data: chunk, + meta : { + percent : 0 + } + }); + }) + .on("error", function (e) { + if(self.isPaused) { + this.generatedError = e; + } else { + self.error(e); + } + }) + .on("end", function () { + if(self.isPaused) { + self._upstreamEnded = true; + } else { + self.end(); + } + }); +}; +NodejsStreamInputAdapter.prototype.pause = function () { + if(!GenericWorker.prototype.pause.call(this)) { + return false; + } + this._stream.pause(); + return true; +}; +NodejsStreamInputAdapter.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if(this._upstreamEnded) { + this.end(); + } else { + this._stream.resume(); + } + + return true; +}; + +module.exports = NodejsStreamInputAdapter; + +},{"../stream/GenericWorker":28,"../utils":32}],13:[function(require,module,exports){ +"use strict"; + +var Readable = require("readable-stream").Readable; + +var utils = require("../utils"); +utils.inherits(NodejsStreamOutputAdapter, Readable); + +/** +* A nodejs stream using a worker as source. +* @see the SourceWrapper in http://nodejs.org/api/stream.html +* @constructor +* @param {StreamHelper} helper the helper wrapping the worker +* @param {Object} options the nodejs stream options +* @param {Function} updateCb the update callback. +*/ +function NodejsStreamOutputAdapter(helper, options, updateCb) { + Readable.call(this, options); + this._helper = helper; + + var self = this; + helper.on("data", function (data, meta) { + if (!self.push(data)) { + self._helper.pause(); + } + if(updateCb) { + updateCb(meta); + } + }) + .on("error", function(e) { + self.emit("error", e); + }) + .on("end", function () { + self.push(null); + }); +} + + +NodejsStreamOutputAdapter.prototype._read = function() { + this._helper.resume(); +}; + +module.exports = NodejsStreamOutputAdapter; + +},{"../utils":32,"readable-stream":16}],14:[function(require,module,exports){ +"use strict"; + +module.exports = { + /** + * True if this is running in Nodejs, will be undefined in a browser. + * In a browser, browserify won't include this file and the whole module + * will be resolved an empty object. + */ + isNode : typeof Buffer !== "undefined", + /** + * Create a new nodejs Buffer from an existing content. + * @param {Object} data the data to pass to the constructor. + * @param {String} encoding the encoding to use. + * @return {Buffer} a new Buffer. + */ + newBufferFrom: function(data, encoding) { + if (Buffer.from && Buffer.from !== Uint8Array.from) { + return Buffer.from(data, encoding); + } else { + if (typeof data === "number") { + // Safeguard for old Node.js versions. On newer versions, + // Buffer.from(number) / Buffer(number, encoding) already throw. + throw new Error("The \"data\" argument must not be a number"); + } + return new Buffer(data, encoding); + } + }, + /** + * Create a new nodejs Buffer with the specified size. + * @param {Integer} size the size of the buffer. + * @return {Buffer} a new Buffer. + */ + allocBuffer: function (size) { + if (Buffer.alloc) { + return Buffer.alloc(size); + } else { + var buf = new Buffer(size); + buf.fill(0); + return buf; + } + }, + /** + * Find out if an object is a Buffer. + * @param {Object} b the object to test. + * @return {Boolean} true if the object is a Buffer, false otherwise. + */ + isBuffer : function(b){ + return Buffer.isBuffer(b); + }, + + isStream : function (obj) { + return obj && + typeof obj.on === "function" && + typeof obj.pause === "function" && + typeof obj.resume === "function"; + } +}; + +},{}],15:[function(require,module,exports){ +"use strict"; +var utf8 = require("./utf8"); +var utils = require("./utils"); +var GenericWorker = require("./stream/GenericWorker"); +var StreamHelper = require("./stream/StreamHelper"); +var defaults = require("./defaults"); +var CompressedObject = require("./compressedObject"); +var ZipObject = require("./zipObject"); +var generate = require("./generate"); +var nodejsUtils = require("./nodejsUtils"); +var NodejsStreamInputAdapter = require("./nodejs/NodejsStreamInputAdapter"); + + +/** + * Add a file in the current folder. + * @private + * @param {string} name the name of the file + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data of the file + * @param {Object} originalOptions the options of the file + * @return {Object} the new file. + */ +var fileAdd = function(name, data, originalOptions) { + // be sure sub folders exist + var dataType = utils.getTypeOf(data), + parent; + + + /* + * Correct options. + */ + + var o = utils.extend(originalOptions || {}, defaults); + o.date = o.date || new Date(); + if (o.compression !== null) { + o.compression = o.compression.toUpperCase(); + } + + if (typeof o.unixPermissions === "string") { + o.unixPermissions = parseInt(o.unixPermissions, 8); + } + + // UNX_IFDIR 0040000 see zipinfo.c + if (o.unixPermissions && (o.unixPermissions & 0x4000)) { + o.dir = true; + } + // Bit 4 Directory + if (o.dosPermissions && (o.dosPermissions & 0x0010)) { + o.dir = true; + } + + if (o.dir) { + name = forceTrailingSlash(name); + } + if (o.createFolders && (parent = parentFolder(name))) { + folderAdd.call(this, parent, true); + } + + var isUnicodeString = dataType === "string" && o.binary === false && o.base64 === false; + if (!originalOptions || typeof originalOptions.binary === "undefined") { + o.binary = !isUnicodeString; + } + + + var isCompressedEmpty = (data instanceof CompressedObject) && data.uncompressedSize === 0; + + if (isCompressedEmpty || o.dir || !data || data.length === 0) { + o.base64 = false; + o.binary = true; + data = ""; + o.compression = "STORE"; + dataType = "string"; + } + + /* + * Convert content to fit. + */ + + var zipObjectContent = null; + if (data instanceof CompressedObject || data instanceof GenericWorker) { + zipObjectContent = data; + } else if (nodejsUtils.isNode && nodejsUtils.isStream(data)) { + zipObjectContent = new NodejsStreamInputAdapter(name, data); + } else { + zipObjectContent = utils.prepareContent(name, data, o.binary, o.optimizedBinaryString, o.base64); + } + + var object = new ZipObject(name, zipObjectContent, o); + this.files[name] = object; + /* + TODO: we can't throw an exception because we have async promises + (we can have a promise of a Date() for example) but returning a + promise is useless because file(name, data) returns the JSZip + object for chaining. Should we break that to allow the user + to catch the error ? + + return external.Promise.resolve(zipObjectContent) + .then(function () { + return object; + }); + */ +}; + +/** + * Find the parent folder of the path. + * @private + * @param {string} path the path to use + * @return {string} the parent folder, or "" + */ +var parentFolder = function (path) { + if (path.slice(-1) === "/") { + path = path.substring(0, path.length - 1); + } + var lastSlash = path.lastIndexOf("/"); + return (lastSlash > 0) ? path.substring(0, lastSlash) : ""; +}; + +/** + * Returns the path with a slash at the end. + * @private + * @param {String} path the path to check. + * @return {String} the path with a trailing slash. + */ +var forceTrailingSlash = function(path) { + // Check the name ends with a / + if (path.slice(-1) !== "/") { + path += "/"; // IE doesn't like substr(-1) + } + return path; +}; + +/** + * Add a (sub) folder in the current folder. + * @private + * @param {string} name the folder's name + * @param {boolean=} [createFolders] If true, automatically create sub + * folders. Defaults to false. + * @return {Object} the new folder. + */ +var folderAdd = function(name, createFolders) { + createFolders = (typeof createFolders !== "undefined") ? createFolders : defaults.createFolders; + + name = forceTrailingSlash(name); + + // Does this folder already exist? + if (!this.files[name]) { + fileAdd.call(this, name, null, { + dir: true, + createFolders: createFolders + }); + } + return this.files[name]; +}; + +/** +* Cross-window, cross-Node-context regular expression detection +* @param {Object} object Anything +* @return {Boolean} true if the object is a regular expression, +* false otherwise +*/ +function isRegExp(object) { + return Object.prototype.toString.call(object) === "[object RegExp]"; +} + +// return the actual prototype of JSZip +var out = { + /** + * @see loadAsync + */ + load: function() { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); + }, + + + /** + * Call a callback function for each entry at this folder level. + * @param {Function} cb the callback function: + * function (relativePath, file) {...} + * It takes 2 arguments : the relative path and the file. + */ + forEach: function(cb) { + var filename, relativePath, file; + // ignore warning about unwanted properties because this.files is a null prototype object + /* eslint-disable-next-line guard-for-in */ + for (filename in this.files) { + file = this.files[filename]; + relativePath = filename.slice(this.root.length, filename.length); + if (relativePath && filename.slice(0, this.root.length) === this.root) { // the file is in the current root + cb(relativePath, file); // TODO reverse the parameters ? need to be clean AND consistent with the filter search fn... + } + } + }, + + /** + * Filter nested files/folders with the specified function. + * @param {Function} search the predicate to use : + * function (relativePath, file) {...} + * It takes 2 arguments : the relative path and the file. + * @return {Array} An array of matching elements. + */ + filter: function(search) { + var result = []; + this.forEach(function (relativePath, entry) { + if (search(relativePath, entry)) { // the file matches the function + result.push(entry); + } + + }); + return result; + }, + + /** + * Add a file to the zip file, or search a file. + * @param {string|RegExp} name The name of the file to add (if data is defined), + * the name of the file to find (if no data) or a regex to match files. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data The file data, either raw or base64 encoded + * @param {Object} o File options + * @return {JSZip|Object|Array} this JSZip object (when adding a file), + * a file (when searching by string) or an array of files (when searching by regex). + */ + file: function(name, data, o) { + if (arguments.length === 1) { + if (isRegExp(name)) { + var regexp = name; + return this.filter(function(relativePath, file) { + return !file.dir && regexp.test(relativePath); + }); + } + else { // text + var obj = this.files[this.root + name]; + if (obj && !obj.dir) { + return obj; + } else { + return null; + } + } + } + else { // more than one argument : we have data ! + name = this.root + name; + fileAdd.call(this, name, data, o); + } + return this; + }, + + /** + * Add a directory to the zip file, or search. + * @param {String|RegExp} arg The name of the directory to add, or a regex to search folders. + * @return {JSZip} an object with the new directory as the root, or an array containing matching folders. + */ + folder: function(arg) { + if (!arg) { + return this; + } + + if (isRegExp(arg)) { + return this.filter(function(relativePath, file) { + return file.dir && arg.test(relativePath); + }); + } + + // else, name is a new folder + var name = this.root + arg; + var newFolder = folderAdd.call(this, name); + + // Allow chaining by returning a new object with this folder as the root + var ret = this.clone(); + ret.root = newFolder.name; + return ret; + }, + + /** + * Delete a file, or a directory and all sub-files, from the zip + * @param {string} name the name of the file to delete + * @return {JSZip} this JSZip object + */ + remove: function(name) { + name = this.root + name; + var file = this.files[name]; + if (!file) { + // Look for any folders + if (name.slice(-1) !== "/") { + name += "/"; + } + file = this.files[name]; + } + + if (file && !file.dir) { + // file + delete this.files[name]; + } else { + // maybe a folder, delete recursively + var kids = this.filter(function(relativePath, file) { + return file.name.slice(0, name.length) === name; + }); + for (var i = 0; i < kids.length; i++) { + delete this.files[kids[i].name]; + } + } + + return this; + }, + + /** + * @deprecated This method has been removed in JSZip 3.0, please check the upgrade guide. + */ + generate: function() { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); + }, + + /** + * Generate the complete zip file as an internal stream. + * @param {Object} options the options to generate the zip file : + * - compression, "STORE" by default. + * - type, "base64" by default. Values are : string, base64, uint8array, arraybuffer, blob. + * @return {StreamHelper} the streamed zip file. + */ + generateInternalStream: function(options) { + var worker, opts = {}; + try { + opts = utils.extend(options || {}, { + streamFiles: false, + compression: "STORE", + compressionOptions : null, + type: "", + platform: "DOS", + comment: null, + mimeType: "application/zip", + encodeFileName: utf8.utf8encode + }); + + opts.type = opts.type.toLowerCase(); + opts.compression = opts.compression.toUpperCase(); + + // "binarystring" is preferred but the internals use "string". + if(opts.type === "binarystring") { + opts.type = "string"; + } + + if (!opts.type) { + throw new Error("No output type specified."); + } + + utils.checkSupport(opts.type); + + // accept nodejs `process.platform` + if( + opts.platform === "darwin" || + opts.platform === "freebsd" || + opts.platform === "linux" || + opts.platform === "sunos" + ) { + opts.platform = "UNIX"; + } + if (opts.platform === "win32") { + opts.platform = "DOS"; + } + + var comment = opts.comment || this.comment || ""; + worker = generate.generateWorker(this, opts, comment); + } catch (e) { + worker = new GenericWorker("error"); + worker.error(e); + } + return new StreamHelper(worker, opts.type || "string", opts.mimeType); + }, + /** + * Generate the complete zip file asynchronously. + * @see generateInternalStream + */ + generateAsync: function(options, onUpdate) { + return this.generateInternalStream(options).accumulate(onUpdate); + }, + /** + * Generate the complete zip file asynchronously. + * @see generateInternalStream + */ + generateNodeStream: function(options, onUpdate) { + options = options || {}; + if (!options.type) { + options.type = "nodebuffer"; + } + return this.generateInternalStream(options).toNodejsStream(onUpdate); + } +}; +module.exports = out; + +},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(require,module,exports){ +"use strict"; +/* + * This file is used by module bundlers (browserify/webpack/etc) when + * including a stream implementation. We use "readable-stream" to get a + * consistent behavior between nodejs versions but bundlers often have a shim + * for "stream". Using this shim greatly improve the compatibility and greatly + * reduce the final size of the bundle (only one stream implementation, not + * two). + */ +module.exports = require("stream"); + +},{"stream":undefined}],17:[function(require,module,exports){ +"use strict"; +var DataReader = require("./DataReader"); +var utils = require("../utils"); + +function ArrayReader(data) { + DataReader.call(this, data); + for(var i = 0; i < this.data.length; i++) { + data[i] = data[i] & 0xFF; + } +} +utils.inherits(ArrayReader, DataReader); +/** + * @see DataReader.byteAt + */ +ArrayReader.prototype.byteAt = function(i) { + return this.data[this.zero + i]; +}; +/** + * @see DataReader.lastIndexOfSignature + */ +ArrayReader.prototype.lastIndexOfSignature = function(sig) { + var sig0 = sig.charCodeAt(0), + sig1 = sig.charCodeAt(1), + sig2 = sig.charCodeAt(2), + sig3 = sig.charCodeAt(3); + for (var i = this.length - 4; i >= 0; --i) { + if (this.data[i] === sig0 && this.data[i + 1] === sig1 && this.data[i + 2] === sig2 && this.data[i + 3] === sig3) { + return i - this.zero; + } + } + + return -1; +}; +/** + * @see DataReader.readAndCheckSignature + */ +ArrayReader.prototype.readAndCheckSignature = function (sig) { + var sig0 = sig.charCodeAt(0), + sig1 = sig.charCodeAt(1), + sig2 = sig.charCodeAt(2), + sig3 = sig.charCodeAt(3), + data = this.readData(4); + return sig0 === data[0] && sig1 === data[1] && sig2 === data[2] && sig3 === data[3]; +}; +/** + * @see DataReader.readData + */ +ArrayReader.prototype.readData = function(size) { + this.checkOffset(size); + if(size === 0) { + return []; + } + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = ArrayReader; + +},{"../utils":32,"./DataReader":18}],18:[function(require,module,exports){ +"use strict"; +var utils = require("../utils"); + +function DataReader(data) { + this.data = data; // type : see implementation + this.length = data.length; + this.index = 0; + this.zero = 0; +} +DataReader.prototype = { + /** + * Check that the offset will not go too far. + * @param {string} offset the additional offset to check. + * @throws {Error} an Error if the offset is out of bounds. + */ + checkOffset: function(offset) { + this.checkIndex(this.index + offset); + }, + /** + * Check that the specified index will not be too far. + * @param {string} newIndex the index to check. + * @throws {Error} an Error if the index is out of bounds. + */ + checkIndex: function(newIndex) { + if (this.length < this.zero + newIndex || newIndex < 0) { + throw new Error("End of data reached (data length = " + this.length + ", asked index = " + (newIndex) + "). Corrupted zip ?"); + } + }, + /** + * Change the index. + * @param {number} newIndex The new index. + * @throws {Error} if the new index is out of the data. + */ + setIndex: function(newIndex) { + this.checkIndex(newIndex); + this.index = newIndex; + }, + /** + * Skip the next n bytes. + * @param {number} n the number of bytes to skip. + * @throws {Error} if the new index is out of the data. + */ + skip: function(n) { + this.setIndex(this.index + n); + }, + /** + * Get the byte at the specified index. + * @param {number} i the index to use. + * @return {number} a byte. + */ + byteAt: function() { + // see implementations + }, + /** + * Get the next number with a given byte size. + * @param {number} size the number of bytes to read. + * @return {number} the corresponding number. + */ + readInt: function(size) { + var result = 0, + i; + this.checkOffset(size); + for (i = this.index + size - 1; i >= this.index; i--) { + result = (result << 8) + this.byteAt(i); + } + this.index += size; + return result; + }, + /** + * Get the next string with a given byte size. + * @param {number} size the number of bytes to read. + * @return {string} the corresponding string. + */ + readString: function(size) { + return utils.transformTo("string", this.readData(size)); + }, + /** + * Get raw data without conversion, bytes. + * @param {number} size the number of bytes to read. + * @return {Object} the raw data, implementation specific. + */ + readData: function() { + // see implementations + }, + /** + * Find the last occurrence of a zip signature (4 bytes). + * @param {string} sig the signature to find. + * @return {number} the index of the last occurrence, -1 if not found. + */ + lastIndexOfSignature: function() { + // see implementations + }, + /** + * Read the signature (4 bytes) at the current position and compare it with sig. + * @param {string} sig the expected signature + * @return {boolean} true if the signature matches, false otherwise. + */ + readAndCheckSignature: function() { + // see implementations + }, + /** + * Get the next date. + * @return {Date} the date. + */ + readDate: function() { + var dostime = this.readInt(4); + return new Date(Date.UTC( + ((dostime >> 25) & 0x7f) + 1980, // year + ((dostime >> 21) & 0x0f) - 1, // month + (dostime >> 16) & 0x1f, // day + (dostime >> 11) & 0x1f, // hour + (dostime >> 5) & 0x3f, // minute + (dostime & 0x1f) << 1)); // second + } +}; +module.exports = DataReader; + +},{"../utils":32}],19:[function(require,module,exports){ +"use strict"; +var Uint8ArrayReader = require("./Uint8ArrayReader"); +var utils = require("../utils"); + +function NodeBufferReader(data) { + Uint8ArrayReader.call(this, data); +} +utils.inherits(NodeBufferReader, Uint8ArrayReader); + +/** + * @see DataReader.readData + */ +NodeBufferReader.prototype.readData = function(size) { + this.checkOffset(size); + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = NodeBufferReader; + +},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(require,module,exports){ +"use strict"; +var DataReader = require("./DataReader"); +var utils = require("../utils"); + +function StringReader(data) { + DataReader.call(this, data); +} +utils.inherits(StringReader, DataReader); +/** + * @see DataReader.byteAt + */ +StringReader.prototype.byteAt = function(i) { + return this.data.charCodeAt(this.zero + i); +}; +/** + * @see DataReader.lastIndexOfSignature + */ +StringReader.prototype.lastIndexOfSignature = function(sig) { + return this.data.lastIndexOf(sig) - this.zero; +}; +/** + * @see DataReader.readAndCheckSignature + */ +StringReader.prototype.readAndCheckSignature = function (sig) { + var data = this.readData(4); + return sig === data; +}; +/** + * @see DataReader.readData + */ +StringReader.prototype.readData = function(size) { + this.checkOffset(size); + // this will work because the constructor applied the "& 0xff" mask. + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = StringReader; + +},{"../utils":32,"./DataReader":18}],21:[function(require,module,exports){ +"use strict"; +var ArrayReader = require("./ArrayReader"); +var utils = require("../utils"); + +function Uint8ArrayReader(data) { + ArrayReader.call(this, data); +} +utils.inherits(Uint8ArrayReader, ArrayReader); +/** + * @see DataReader.readData + */ +Uint8ArrayReader.prototype.readData = function(size) { + this.checkOffset(size); + if(size === 0) { + // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. + return new Uint8Array(0); + } + var result = this.data.subarray(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = Uint8ArrayReader; + +},{"../utils":32,"./ArrayReader":17}],22:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var support = require("../support"); +var ArrayReader = require("./ArrayReader"); +var StringReader = require("./StringReader"); +var NodeBufferReader = require("./NodeBufferReader"); +var Uint8ArrayReader = require("./Uint8ArrayReader"); + +/** + * Create a reader adapted to the data. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data to read. + * @return {DataReader} the data reader. + */ +module.exports = function (data) { + var type = utils.getTypeOf(data); + utils.checkSupport(type); + if (type === "string" && !support.uint8array) { + return new StringReader(data); + } + if (type === "nodebuffer") { + return new NodeBufferReader(data); + } + if (support.uint8array) { + return new Uint8ArrayReader(utils.transformTo("uint8array", data)); + } + return new ArrayReader(utils.transformTo("array", data)); +}; + +},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(require,module,exports){ +"use strict"; +exports.LOCAL_FILE_HEADER = "PK\x03\x04"; +exports.CENTRAL_FILE_HEADER = "PK\x01\x02"; +exports.CENTRAL_DIRECTORY_END = "PK\x05\x06"; +exports.ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x06\x07"; +exports.ZIP64_CENTRAL_DIRECTORY_END = "PK\x06\x06"; +exports.DATA_DESCRIPTOR = "PK\x07\x08"; + +},{}],24:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./GenericWorker"); +var utils = require("../utils"); + +/** + * A worker which convert chunks to a specified type. + * @constructor + * @param {String} destType the destination type. + */ +function ConvertWorker(destType) { + GenericWorker.call(this, "ConvertWorker to " + destType); + this.destType = destType; +} +utils.inherits(ConvertWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +ConvertWorker.prototype.processChunk = function (chunk) { + this.push({ + data : utils.transformTo(this.destType, chunk.data), + meta : chunk.meta + }); +}; +module.exports = ConvertWorker; + +},{"../utils":32,"./GenericWorker":28}],25:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./GenericWorker"); +var crc32 = require("../crc32"); +var utils = require("../utils"); + +/** + * A worker which calculate the crc32 of the data flowing through. + * @constructor + */ +function Crc32Probe() { + GenericWorker.call(this, "Crc32Probe"); + this.withStreamInfo("crc32", 0); +} +utils.inherits(Crc32Probe, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Crc32Probe.prototype.processChunk = function (chunk) { + this.streamInfo.crc32 = crc32(chunk.data, this.streamInfo.crc32 || 0); + this.push(chunk); +}; +module.exports = Crc32Probe; + +},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("./GenericWorker"); + +/** + * A worker which calculate the total length of the data flowing through. + * @constructor + * @param {String} propName the name used to expose the length + */ +function DataLengthProbe(propName) { + GenericWorker.call(this, "DataLengthProbe for " + propName); + this.propName = propName; + this.withStreamInfo(propName, 0); +} +utils.inherits(DataLengthProbe, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +DataLengthProbe.prototype.processChunk = function (chunk) { + if(chunk) { + var length = this.streamInfo[this.propName] || 0; + this.streamInfo[this.propName] = length + chunk.data.length; + } + GenericWorker.prototype.processChunk.call(this, chunk); +}; +module.exports = DataLengthProbe; + + +},{"../utils":32,"./GenericWorker":28}],27:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("./GenericWorker"); + +// the size of the generated chunks +// TODO expose this as a public variable +var DEFAULT_BLOCK_SIZE = 16 * 1024; + +/** + * A worker that reads a content and emits chunks. + * @constructor + * @param {Promise} dataP the promise of the data to split + */ +function DataWorker(dataP) { + GenericWorker.call(this, "DataWorker"); + var self = this; + this.dataIsReady = false; + this.index = 0; + this.max = 0; + this.data = null; + this.type = ""; + + this._tickScheduled = false; + + dataP.then(function (data) { + self.dataIsReady = true; + self.data = data; + self.max = data && data.length || 0; + self.type = utils.getTypeOf(data); + if(!self.isPaused) { + self._tickAndRepeat(); + } + }, function (e) { + self.error(e); + }); +} + +utils.inherits(DataWorker, GenericWorker); + +/** + * @see GenericWorker.cleanUp + */ +DataWorker.prototype.cleanUp = function () { + GenericWorker.prototype.cleanUp.call(this); + this.data = null; +}; + +/** + * @see GenericWorker.resume + */ +DataWorker.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if (!this._tickScheduled && this.dataIsReady) { + this._tickScheduled = true; + utils.delay(this._tickAndRepeat, [], this); + } + return true; +}; + +/** + * Trigger a tick a schedule an other call to this function. + */ +DataWorker.prototype._tickAndRepeat = function() { + this._tickScheduled = false; + if(this.isPaused || this.isFinished) { + return; + } + this._tick(); + if(!this.isFinished) { + utils.delay(this._tickAndRepeat, [], this); + this._tickScheduled = true; + } +}; + +/** + * Read and push a chunk. + */ +DataWorker.prototype._tick = function() { + + if(this.isPaused || this.isFinished) { + return false; + } + + var size = DEFAULT_BLOCK_SIZE; + var data = null, nextIndex = Math.min(this.max, this.index + size); + if (this.index >= this.max) { + // EOF + return this.end(); + } else { + switch(this.type) { + case "string": + data = this.data.substring(this.index, nextIndex); + break; + case "uint8array": + data = this.data.subarray(this.index, nextIndex); + break; + case "array": + case "nodebuffer": + data = this.data.slice(this.index, nextIndex); + break; + } + this.index = nextIndex; + return this.push({ + data : data, + meta : { + percent : this.max ? this.index / this.max * 100 : 0 + } + }); + } +}; + +module.exports = DataWorker; + +},{"../utils":32,"./GenericWorker":28}],28:[function(require,module,exports){ +"use strict"; + +/** + * A worker that does nothing but passing chunks to the next one. This is like + * a nodejs stream but with some differences. On the good side : + * - it works on IE 6-9 without any issue / polyfill + * - it weights less than the full dependencies bundled with browserify + * - it forwards errors (no need to declare an error handler EVERYWHERE) + * + * A chunk is an object with 2 attributes : `meta` and `data`. The former is an + * object containing anything (`percent` for example), see each worker for more + * details. The latter is the real data (String, Uint8Array, etc). + * + * @constructor + * @param {String} name the name of the stream (mainly used for debugging purposes) + */ +function GenericWorker(name) { + // the name of the worker + this.name = name || "default"; + // an object containing metadata about the workers chain + this.streamInfo = {}; + // an error which happened when the worker was paused + this.generatedError = null; + // an object containing metadata to be merged by this worker into the general metadata + this.extraStreamInfo = {}; + // true if the stream is paused (and should not do anything), false otherwise + this.isPaused = true; + // true if the stream is finished (and should not do anything), false otherwise + this.isFinished = false; + // true if the stream is locked to prevent further structure updates (pipe), false otherwise + this.isLocked = false; + // the event listeners + this._listeners = { + "data":[], + "end":[], + "error":[] + }; + // the previous worker, if any + this.previous = null; +} + +GenericWorker.prototype = { + /** + * Push a chunk to the next workers. + * @param {Object} chunk the chunk to push + */ + push : function (chunk) { + this.emit("data", chunk); + }, + /** + * End the stream. + * @return {Boolean} true if this call ended the worker, false otherwise. + */ + end : function () { + if (this.isFinished) { + return false; + } + + this.flush(); + try { + this.emit("end"); + this.cleanUp(); + this.isFinished = true; + } catch (e) { + this.emit("error", e); + } + return true; + }, + /** + * End the stream with an error. + * @param {Error} e the error which caused the premature end. + * @return {Boolean} true if this call ended the worker with an error, false otherwise. + */ + error : function (e) { + if (this.isFinished) { + return false; + } + + if(this.isPaused) { + this.generatedError = e; + } else { + this.isFinished = true; + + this.emit("error", e); + + // in the workers chain exploded in the middle of the chain, + // the error event will go downward but we also need to notify + // workers upward that there has been an error. + if(this.previous) { + this.previous.error(e); + } + + this.cleanUp(); + } + return true; + }, + /** + * Add a callback on an event. + * @param {String} name the name of the event (data, end, error) + * @param {Function} listener the function to call when the event is triggered + * @return {GenericWorker} the current object for chainability + */ + on : function (name, listener) { + this._listeners[name].push(listener); + return this; + }, + /** + * Clean any references when a worker is ending. + */ + cleanUp : function () { + this.streamInfo = this.generatedError = this.extraStreamInfo = null; + this._listeners = []; + }, + /** + * Trigger an event. This will call registered callback with the provided arg. + * @param {String} name the name of the event (data, end, error) + * @param {Object} arg the argument to call the callback with. + */ + emit : function (name, arg) { + if (this._listeners[name]) { + for(var i = 0; i < this._listeners[name].length; i++) { + this._listeners[name][i].call(this, arg); + } + } + }, + /** + * Chain a worker with an other. + * @param {Worker} next the worker receiving events from the current one. + * @return {worker} the next worker for chainability + */ + pipe : function (next) { + return next.registerPrevious(this); + }, + /** + * Same as `pipe` in the other direction. + * Using an API with `pipe(next)` is very easy. + * Implementing the API with the point of view of the next one registering + * a source is easier, see the ZipFileWorker. + * @param {Worker} previous the previous worker, sending events to this one + * @return {Worker} the current worker for chainability + */ + registerPrevious : function (previous) { + if (this.isLocked) { + throw new Error("The stream '" + this + "' has already been used."); + } + + // sharing the streamInfo... + this.streamInfo = previous.streamInfo; + // ... and adding our own bits + this.mergeStreamInfo(); + this.previous = previous; + var self = this; + previous.on("data", function (chunk) { + self.processChunk(chunk); + }); + previous.on("end", function () { + self.end(); + }); + previous.on("error", function (e) { + self.error(e); + }); + return this; + }, + /** + * Pause the stream so it doesn't send events anymore. + * @return {Boolean} true if this call paused the worker, false otherwise. + */ + pause : function () { + if(this.isPaused || this.isFinished) { + return false; + } + this.isPaused = true; + + if(this.previous) { + this.previous.pause(); + } + return true; + }, + /** + * Resume a paused stream. + * @return {Boolean} true if this call resumed the worker, false otherwise. + */ + resume : function () { + if(!this.isPaused || this.isFinished) { + return false; + } + this.isPaused = false; + + // if true, the worker tried to resume but failed + var withError = false; + if(this.generatedError) { + this.error(this.generatedError); + withError = true; + } + if(this.previous) { + this.previous.resume(); + } + + return !withError; + }, + /** + * Flush any remaining bytes as the stream is ending. + */ + flush : function () {}, + /** + * Process a chunk. This is usually the method overridden. + * @param {Object} chunk the chunk to process. + */ + processChunk : function(chunk) { + this.push(chunk); + }, + /** + * Add a key/value to be added in the workers chain streamInfo once activated. + * @param {String} key the key to use + * @param {Object} value the associated value + * @return {Worker} the current worker for chainability + */ + withStreamInfo : function (key, value) { + this.extraStreamInfo[key] = value; + this.mergeStreamInfo(); + return this; + }, + /** + * Merge this worker's streamInfo into the chain's streamInfo. + */ + mergeStreamInfo : function () { + for(var key in this.extraStreamInfo) { + if (!Object.prototype.hasOwnProperty.call(this.extraStreamInfo, key)) { + continue; + } + this.streamInfo[key] = this.extraStreamInfo[key]; + } + }, + + /** + * Lock the stream to prevent further updates on the workers chain. + * After calling this method, all calls to pipe will fail. + */ + lock: function () { + if (this.isLocked) { + throw new Error("The stream '" + this + "' has already been used."); + } + this.isLocked = true; + if (this.previous) { + this.previous.lock(); + } + }, + + /** + * + * Pretty print the workers chain. + */ + toString : function () { + var me = "Worker " + this.name; + if (this.previous) { + return this.previous + " -> " + me; + } else { + return me; + } + } +}; + +module.exports = GenericWorker; + +},{}],29:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var ConvertWorker = require("./ConvertWorker"); +var GenericWorker = require("./GenericWorker"); +var base64 = require("../base64"); +var support = require("../support"); +var external = require("../external"); + +var NodejsStreamOutputAdapter = null; +if (support.nodestream) { + try { + NodejsStreamOutputAdapter = require("../nodejs/NodejsStreamOutputAdapter"); + } catch(e) { + // ignore + } +} + +/** + * Apply the final transformation of the data. If the user wants a Blob for + * example, it's easier to work with an U8intArray and finally do the + * ArrayBuffer/Blob conversion. + * @param {String} type the name of the final type + * @param {String|Uint8Array|Buffer} content the content to transform + * @param {String} mimeType the mime type of the content, if applicable. + * @return {String|Uint8Array|ArrayBuffer|Buffer|Blob} the content in the right format. + */ +function transformZipOutput(type, content, mimeType) { + switch(type) { + case "blob" : + return utils.newBlob(utils.transformTo("arraybuffer", content), mimeType); + case "base64" : + return base64.encode(content); + default : + return utils.transformTo(type, content); + } +} + +/** + * Concatenate an array of data of the given type. + * @param {String} type the type of the data in the given array. + * @param {Array} dataArray the array containing the data chunks to concatenate + * @return {String|Uint8Array|Buffer} the concatenated data + * @throws Error if the asked type is unsupported + */ +function concat (type, dataArray) { + var i, index = 0, res = null, totalLength = 0; + for(i = 0; i < dataArray.length; i++) { + totalLength += dataArray[i].length; + } + switch(type) { + case "string": + return dataArray.join(""); + case "array": + return Array.prototype.concat.apply([], dataArray); + case "uint8array": + res = new Uint8Array(totalLength); + for(i = 0; i < dataArray.length; i++) { + res.set(dataArray[i], index); + index += dataArray[i].length; + } + return res; + case "nodebuffer": + return Buffer.concat(dataArray); + default: + throw new Error("concat : unsupported type '" + type + "'"); + } +} + +/** + * Listen a StreamHelper, accumulate its content and concatenate it into a + * complete block. + * @param {StreamHelper} helper the helper to use. + * @param {Function} updateCallback a callback called on each update. Called + * with one arg : + * - the metadata linked to the update received. + * @return Promise the promise for the accumulation. + */ +function accumulate(helper, updateCallback) { + return new external.Promise(function (resolve, reject){ + var dataArray = []; + var chunkType = helper._internalType, + resultType = helper._outputType, + mimeType = helper._mimeType; + helper + .on("data", function (data, meta) { + dataArray.push(data); + if(updateCallback) { + updateCallback(meta); + } + }) + .on("error", function(err) { + dataArray = []; + reject(err); + }) + .on("end", function (){ + try { + var result = transformZipOutput(resultType, concat(chunkType, dataArray), mimeType); + resolve(result); + } catch (e) { + reject(e); + } + dataArray = []; + }) + .resume(); + }); +} + +/** + * An helper to easily use workers outside of JSZip. + * @constructor + * @param {Worker} worker the worker to wrap + * @param {String} outputType the type of data expected by the use + * @param {String} mimeType the mime type of the content, if applicable. + */ +function StreamHelper(worker, outputType, mimeType) { + var internalType = outputType; + switch(outputType) { + case "blob": + case "arraybuffer": + internalType = "uint8array"; + break; + case "base64": + internalType = "string"; + break; + } + + try { + // the type used internally + this._internalType = internalType; + // the type used to output results + this._outputType = outputType; + // the mime type + this._mimeType = mimeType; + utils.checkSupport(internalType); + this._worker = worker.pipe(new ConvertWorker(internalType)); + // the last workers can be rewired without issues but we need to + // prevent any updates on previous workers. + worker.lock(); + } catch(e) { + this._worker = new GenericWorker("error"); + this._worker.error(e); + } +} + +StreamHelper.prototype = { + /** + * Listen a StreamHelper, accumulate its content and concatenate it into a + * complete block. + * @param {Function} updateCb the update callback. + * @return Promise the promise for the accumulation. + */ + accumulate : function (updateCb) { + return accumulate(this, updateCb); + }, + /** + * Add a listener on an event triggered on a stream. + * @param {String} evt the name of the event + * @param {Function} fn the listener + * @return {StreamHelper} the current helper. + */ + on : function (evt, fn) { + var self = this; + + if(evt === "data") { + this._worker.on(evt, function (chunk) { + fn.call(self, chunk.data, chunk.meta); + }); + } else { + this._worker.on(evt, function () { + utils.delay(fn, arguments, self); + }); + } + return this; + }, + /** + * Resume the flow of chunks. + * @return {StreamHelper} the current helper. + */ + resume : function () { + utils.delay(this._worker.resume, [], this._worker); + return this; + }, + /** + * Pause the flow of chunks. + * @return {StreamHelper} the current helper. + */ + pause : function () { + this._worker.pause(); + return this; + }, + /** + * Return a nodejs stream for this helper. + * @param {Function} updateCb the update callback. + * @return {NodejsStreamOutputAdapter} the nodejs stream. + */ + toNodejsStream : function (updateCb) { + utils.checkSupport("nodestream"); + if (this._outputType !== "nodebuffer") { + // an object stream containing blob/arraybuffer/uint8array/string + // is strange and I don't know if it would be useful. + // I you find this comment and have a good usecase, please open a + // bug report ! + throw new Error(this._outputType + " is not supported by this method"); + } + + return new NodejsStreamOutputAdapter(this, { + objectMode : this._outputType !== "nodebuffer" + }, updateCb); + } +}; + + +module.exports = StreamHelper; + +},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],30:[function(require,module,exports){ +"use strict"; + +exports.base64 = true; +exports.array = true; +exports.string = true; +exports.arraybuffer = typeof ArrayBuffer !== "undefined" && typeof Uint8Array !== "undefined"; +exports.nodebuffer = typeof Buffer !== "undefined"; +// contains true if JSZip can read/generate Uint8Array, false otherwise. +exports.uint8array = typeof Uint8Array !== "undefined"; + +if (typeof ArrayBuffer === "undefined") { + exports.blob = false; +} +else { + var buffer = new ArrayBuffer(0); + try { + exports.blob = new Blob([buffer], { + type: "application/zip" + }).size === 0; + } + catch (e) { + try { + var Builder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder || self.MSBlobBuilder; + var builder = new Builder(); + builder.append(buffer); + exports.blob = builder.getBlob("application/zip").size === 0; + } + catch (e) { + exports.blob = false; + } + } +} + +try { + exports.nodestream = !!require("readable-stream").Readable; +} catch(e) { + exports.nodestream = false; +} + +},{"readable-stream":16}],31:[function(require,module,exports){ +"use strict"; + +var utils = require("./utils"); +var support = require("./support"); +var nodejsUtils = require("./nodejsUtils"); +var GenericWorker = require("./stream/GenericWorker"); + +/** + * The following functions come from pako, from pako/lib/utils/strings + * released under the MIT license, see pako https://github.com/nodeca/pako/ + */ + +// Table with utf8 lengths (calculated by first byte of sequence) +// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS, +// because max possible codepoint is 0x10ffff +var _utf8len = new Array(256); +for (var i=0; i<256; i++) { + _utf8len[i] = (i >= 252 ? 6 : i >= 248 ? 5 : i >= 240 ? 4 : i >= 224 ? 3 : i >= 192 ? 2 : 1); +} +_utf8len[254]=_utf8len[254]=1; // Invalid sequence start + +// convert string to array (typed, when possible) +var string2buf = function (str) { + var buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0; + + // count binary size + for (m_pos = 0; m_pos < str_len; m_pos++) { + c = str.charCodeAt(m_pos); + if ((c & 0xfc00) === 0xd800 && (m_pos+1 < str_len)) { + c2 = str.charCodeAt(m_pos+1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); + m_pos++; + } + } + buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; + } + + // allocate buffer + if (support.uint8array) { + buf = new Uint8Array(buf_len); + } else { + buf = new Array(buf_len); + } + + // convert + for (i=0, m_pos = 0; i < buf_len; m_pos++) { + c = str.charCodeAt(m_pos); + if ((c & 0xfc00) === 0xd800 && (m_pos+1 < str_len)) { + c2 = str.charCodeAt(m_pos+1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); + m_pos++; + } + } + if (c < 0x80) { + /* one byte */ + buf[i++] = c; + } else if (c < 0x800) { + /* two bytes */ + buf[i++] = 0xC0 | (c >>> 6); + buf[i++] = 0x80 | (c & 0x3f); + } else if (c < 0x10000) { + /* three bytes */ + buf[i++] = 0xE0 | (c >>> 12); + buf[i++] = 0x80 | (c >>> 6 & 0x3f); + buf[i++] = 0x80 | (c & 0x3f); + } else { + /* four bytes */ + buf[i++] = 0xf0 | (c >>> 18); + buf[i++] = 0x80 | (c >>> 12 & 0x3f); + buf[i++] = 0x80 | (c >>> 6 & 0x3f); + buf[i++] = 0x80 | (c & 0x3f); + } + } + + return buf; +}; + +// Calculate max possible position in utf8 buffer, +// that will not break sequence. If that's not possible +// - (very small limits) return max size as is. +// +// buf[] - utf8 bytes array +// max - length limit (mandatory); +var utf8border = function(buf, max) { + var pos; + + max = max || buf.length; + if (max > buf.length) { max = buf.length; } + + // go back from last position, until start of sequence found + pos = max-1; + while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; } + + // Fuckup - very small and broken sequence, + // return max, because we should return something anyway. + if (pos < 0) { return max; } + + // If we came to start of buffer - that means vuffer is too small, + // return max too. + if (pos === 0) { return max; } + + return (pos + _utf8len[buf[pos]] > max) ? pos : max; +}; + +// convert array to string +var buf2string = function (buf) { + var i, out, c, c_len; + var len = buf.length; + + // Reserve max possible length (2 words per char) + // NB: by unknown reasons, Array is significantly faster for + // String.fromCharCode.apply than Uint16Array. + var utf16buf = new Array(len*2); + + for (out=0, i=0; i 4) { utf16buf[out++] = 0xfffd; i += c_len-1; continue; } + + // apply mask on first byte + c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07; + // join the rest + while (c_len > 1 && i < len) { + c = (c << 6) | (buf[i++] & 0x3f); + c_len--; + } + + // terminated by end of string? + if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; } + + if (c < 0x10000) { + utf16buf[out++] = c; + } else { + c -= 0x10000; + utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff); + utf16buf[out++] = 0xdc00 | (c & 0x3ff); + } + } + + // shrinkBuf(utf16buf, out) + if (utf16buf.length !== out) { + if(utf16buf.subarray) { + utf16buf = utf16buf.subarray(0, out); + } else { + utf16buf.length = out; + } + } + + // return String.fromCharCode.apply(null, utf16buf); + return utils.applyFromCharCode(utf16buf); +}; + + +// That's all for the pako functions. + + +/** + * Transform a javascript string into an array (typed if possible) of bytes, + * UTF-8 encoded. + * @param {String} str the string to encode + * @return {Array|Uint8Array|Buffer} the UTF-8 encoded string. + */ +exports.utf8encode = function utf8encode(str) { + if (support.nodebuffer) { + return nodejsUtils.newBufferFrom(str, "utf-8"); + } + + return string2buf(str); +}; + + +/** + * Transform a bytes array (or a representation) representing an UTF-8 encoded + * string into a javascript string. + * @param {Array|Uint8Array|Buffer} buf the data de decode + * @return {String} the decoded string. + */ +exports.utf8decode = function utf8decode(buf) { + if (support.nodebuffer) { + return utils.transformTo("nodebuffer", buf).toString("utf-8"); + } + + buf = utils.transformTo(support.uint8array ? "uint8array" : "array", buf); + + return buf2string(buf); +}; + +/** + * A worker to decode utf8 encoded binary chunks into string chunks. + * @constructor + */ +function Utf8DecodeWorker() { + GenericWorker.call(this, "utf-8 decode"); + // the last bytes if a chunk didn't end with a complete codepoint. + this.leftOver = null; +} +utils.inherits(Utf8DecodeWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Utf8DecodeWorker.prototype.processChunk = function (chunk) { + + var data = utils.transformTo(support.uint8array ? "uint8array" : "array", chunk.data); + + // 1st step, re-use what's left of the previous chunk + if (this.leftOver && this.leftOver.length) { + if(support.uint8array) { + var previousData = data; + data = new Uint8Array(previousData.length + this.leftOver.length); + data.set(this.leftOver, 0); + data.set(previousData, this.leftOver.length); + } else { + data = this.leftOver.concat(data); + } + this.leftOver = null; + } + + var nextBoundary = utf8border(data); + var usableData = data; + if (nextBoundary !== data.length) { + if (support.uint8array) { + usableData = data.subarray(0, nextBoundary); + this.leftOver = data.subarray(nextBoundary, data.length); + } else { + usableData = data.slice(0, nextBoundary); + this.leftOver = data.slice(nextBoundary, data.length); + } + } + + this.push({ + data : exports.utf8decode(usableData), + meta : chunk.meta + }); +}; + +/** + * @see GenericWorker.flush + */ +Utf8DecodeWorker.prototype.flush = function () { + if(this.leftOver && this.leftOver.length) { + this.push({ + data : exports.utf8decode(this.leftOver), + meta : {} + }); + this.leftOver = null; + } +}; +exports.Utf8DecodeWorker = Utf8DecodeWorker; + +/** + * A worker to endcode string chunks into utf8 encoded binary chunks. + * @constructor + */ +function Utf8EncodeWorker() { + GenericWorker.call(this, "utf-8 encode"); +} +utils.inherits(Utf8EncodeWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Utf8EncodeWorker.prototype.processChunk = function (chunk) { + this.push({ + data : exports.utf8encode(chunk.data), + meta : chunk.meta + }); +}; +exports.Utf8EncodeWorker = Utf8EncodeWorker; + +},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(require,module,exports){ +"use strict"; + +var support = require("./support"); +var base64 = require("./base64"); +var nodejsUtils = require("./nodejsUtils"); +var external = require("./external"); +require("setimmediate"); + + +/** + * Convert a string that pass as a "binary string": it should represent a byte + * array but may have > 255 char codes. Be sure to take only the first byte + * and returns the byte array. + * @param {String} str the string to transform. + * @return {Array|Uint8Array} the string in a binary format. + */ +function string2binary(str) { + var result = null; + if (support.uint8array) { + result = new Uint8Array(str.length); + } else { + result = new Array(str.length); + } + return stringToArrayLike(str, result); +} + +/** + * Create a new blob with the given content and the given type. + * @param {String|ArrayBuffer} part the content to put in the blob. DO NOT use + * an Uint8Array because the stock browser of android 4 won't accept it (it + * will be silently converted to a string, "[object Uint8Array]"). + * + * Use only ONE part to build the blob to avoid a memory leak in IE11 / Edge: + * when a large amount of Array is used to create the Blob, the amount of + * memory consumed is nearly 100 times the original data amount. + * + * @param {String} type the mime type of the blob. + * @return {Blob} the created blob. + */ +exports.newBlob = function(part, type) { + exports.checkSupport("blob"); + + try { + // Blob constructor + return new Blob([part], { + type: type + }); + } + catch (e) { + + try { + // deprecated, browser only, old way + var Builder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder || self.MSBlobBuilder; + var builder = new Builder(); + builder.append(part); + return builder.getBlob(type); + } + catch (e) { + + // well, fuck ?! + throw new Error("Bug : can't construct the Blob."); + } + } + + +}; +/** + * The identity function. + * @param {Object} input the input. + * @return {Object} the same input. + */ +function identity(input) { + return input; +} + +/** + * Fill in an array with a string. + * @param {String} str the string to use. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to fill in (will be mutated). + * @return {Array|ArrayBuffer|Uint8Array|Buffer} the updated array. + */ +function stringToArrayLike(str, array) { + for (var i = 0; i < str.length; ++i) { + array[i] = str.charCodeAt(i) & 0xFF; + } + return array; +} + +/** + * An helper for the function arrayLikeToString. + * This contains static information and functions that + * can be optimized by the browser JIT compiler. + */ +var arrayToStringHelper = { + /** + * Transform an array of int into a string, chunk by chunk. + * See the performances notes on arrayLikeToString. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. + * @param {String} type the type of the array. + * @param {Integer} chunk the chunk size. + * @return {String} the resulting string. + * @throws Error if the chunk is too big for the stack. + */ + stringifyByChunk: function(array, type, chunk) { + var result = [], k = 0, len = array.length; + // shortcut + if (len <= chunk) { + return String.fromCharCode.apply(null, array); + } + while (k < len) { + if (type === "array" || type === "nodebuffer") { + result.push(String.fromCharCode.apply(null, array.slice(k, Math.min(k + chunk, len)))); + } + else { + result.push(String.fromCharCode.apply(null, array.subarray(k, Math.min(k + chunk, len)))); + } + k += chunk; + } + return result.join(""); + }, + /** + * Call String.fromCharCode on every item in the array. + * This is the naive implementation, which generate A LOT of intermediate string. + * This should be used when everything else fail. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. + * @return {String} the result. + */ + stringifyByChar: function(array){ + var resultStr = ""; + for(var i = 0; i < array.length; i++) { + resultStr += String.fromCharCode(array[i]); + } + return resultStr; + }, + applyCanBeUsed : { + /** + * true if the browser accepts to use String.fromCharCode on Uint8Array + */ + uint8array : (function () { + try { + return support.uint8array && String.fromCharCode.apply(null, new Uint8Array(1)).length === 1; + } catch (e) { + return false; + } + })(), + /** + * true if the browser accepts to use String.fromCharCode on nodejs Buffer. + */ + nodebuffer : (function () { + try { + return support.nodebuffer && String.fromCharCode.apply(null, nodejsUtils.allocBuffer(1)).length === 1; + } catch (e) { + return false; + } + })() + } +}; + +/** + * Transform an array-like object to a string. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. + * @return {String} the result. + */ +function arrayLikeToString(array) { + // Performances notes : + // -------------------- + // String.fromCharCode.apply(null, array) is the fastest, see + // see http://jsperf.com/converting-a-uint8array-to-a-string/2 + // but the stack is limited (and we can get huge arrays !). + // + // result += String.fromCharCode(array[i]); generate too many strings ! + // + // This code is inspired by http://jsperf.com/arraybuffer-to-string-apply-performance/2 + // TODO : we now have workers that split the work. Do we still need that ? + var chunk = 65536, + type = exports.getTypeOf(array), + canUseApply = true; + if (type === "uint8array") { + canUseApply = arrayToStringHelper.applyCanBeUsed.uint8array; + } else if (type === "nodebuffer") { + canUseApply = arrayToStringHelper.applyCanBeUsed.nodebuffer; + } + + if (canUseApply) { + while (chunk > 1) { + try { + return arrayToStringHelper.stringifyByChunk(array, type, chunk); + } catch (e) { + chunk = Math.floor(chunk / 2); + } + } + } + + // no apply or chunk error : slow and painful algorithm + // default browser on android 4.* + return arrayToStringHelper.stringifyByChar(array); +} + +exports.applyFromCharCode = arrayLikeToString; + + +/** + * Copy the data from an array-like to an other array-like. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} arrayFrom the origin array. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} arrayTo the destination array which will be mutated. + * @return {Array|ArrayBuffer|Uint8Array|Buffer} the updated destination array. + */ +function arrayLikeToArrayLike(arrayFrom, arrayTo) { + for (var i = 0; i < arrayFrom.length; i++) { + arrayTo[i] = arrayFrom[i]; + } + return arrayTo; +} + +// a matrix containing functions to transform everything into everything. +var transform = {}; + +// string to ? +transform["string"] = { + "string": identity, + "array": function(input) { + return stringToArrayLike(input, new Array(input.length)); + }, + "arraybuffer": function(input) { + return transform["string"]["uint8array"](input).buffer; + }, + "uint8array": function(input) { + return stringToArrayLike(input, new Uint8Array(input.length)); + }, + "nodebuffer": function(input) { + return stringToArrayLike(input, nodejsUtils.allocBuffer(input.length)); + } +}; + +// array to ? +transform["array"] = { + "string": arrayLikeToString, + "array": identity, + "arraybuffer": function(input) { + return (new Uint8Array(input)).buffer; + }, + "uint8array": function(input) { + return new Uint8Array(input); + }, + "nodebuffer": function(input) { + return nodejsUtils.newBufferFrom(input); + } +}; + +// arraybuffer to ? +transform["arraybuffer"] = { + "string": function(input) { + return arrayLikeToString(new Uint8Array(input)); + }, + "array": function(input) { + return arrayLikeToArrayLike(new Uint8Array(input), new Array(input.byteLength)); + }, + "arraybuffer": identity, + "uint8array": function(input) { + return new Uint8Array(input); + }, + "nodebuffer": function(input) { + return nodejsUtils.newBufferFrom(new Uint8Array(input)); + } +}; + +// uint8array to ? +transform["uint8array"] = { + "string": arrayLikeToString, + "array": function(input) { + return arrayLikeToArrayLike(input, new Array(input.length)); + }, + "arraybuffer": function(input) { + return input.buffer; + }, + "uint8array": identity, + "nodebuffer": function(input) { + return nodejsUtils.newBufferFrom(input); + } +}; + +// nodebuffer to ? +transform["nodebuffer"] = { + "string": arrayLikeToString, + "array": function(input) { + return arrayLikeToArrayLike(input, new Array(input.length)); + }, + "arraybuffer": function(input) { + return transform["nodebuffer"]["uint8array"](input).buffer; + }, + "uint8array": function(input) { + return arrayLikeToArrayLike(input, new Uint8Array(input.length)); + }, + "nodebuffer": identity +}; + +/** + * Transform an input into any type. + * The supported output type are : string, array, uint8array, arraybuffer, nodebuffer. + * If no output type is specified, the unmodified input will be returned. + * @param {String} outputType the output type. + * @param {String|Array|ArrayBuffer|Uint8Array|Buffer} input the input to convert. + * @throws {Error} an Error if the browser doesn't support the requested output type. + */ +exports.transformTo = function(outputType, input) { + if (!input) { + // undefined, null, etc + // an empty string won't harm. + input = ""; + } + if (!outputType) { + return input; + } + exports.checkSupport(outputType); + var inputType = exports.getTypeOf(input); + var result = transform[inputType][outputType](input); + return result; +}; + +/** + * Resolve all relative path components, "." and "..", in a path. If these relative components + * traverse above the root then the resulting path will only contain the final path component. + * + * All empty components, e.g. "//", are removed. + * @param {string} path A path with / or \ separators + * @returns {string} The path with all relative path components resolved. + */ +exports.resolve = function(path) { + var parts = path.split("/"); + var result = []; + for (var index = 0; index < parts.length; index++) { + var part = parts[index]; + // Allow the first and last component to be empty for trailing slashes. + if (part === "." || (part === "" && index !== 0 && index !== parts.length - 1)) { + continue; + } else if (part === "..") { + result.pop(); + } else { + result.push(part); + } + } + return result.join("/"); +}; + +/** + * Return the type of the input. + * The type will be in a format valid for JSZip.utils.transformTo : string, array, uint8array, arraybuffer. + * @param {Object} input the input to identify. + * @return {String} the (lowercase) type of the input. + */ +exports.getTypeOf = function(input) { + if (typeof input === "string") { + return "string"; + } + if (Object.prototype.toString.call(input) === "[object Array]") { + return "array"; + } + if (support.nodebuffer && nodejsUtils.isBuffer(input)) { + return "nodebuffer"; + } + if (support.uint8array && input instanceof Uint8Array) { + return "uint8array"; + } + if (support.arraybuffer && input instanceof ArrayBuffer) { + return "arraybuffer"; + } +}; + +/** + * Throw an exception if the type is not supported. + * @param {String} type the type to check. + * @throws {Error} an Error if the browser doesn't support the requested type. + */ +exports.checkSupport = function(type) { + var supported = support[type.toLowerCase()]; + if (!supported) { + throw new Error(type + " is not supported by this platform"); + } +}; + +exports.MAX_VALUE_16BITS = 65535; +exports.MAX_VALUE_32BITS = -1; // well, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" is parsed as -1 + +/** + * Prettify a string read as binary. + * @param {string} str the string to prettify. + * @return {string} a pretty string. + */ +exports.pretty = function(str) { + var res = "", + code, i; + for (i = 0; i < (str || "").length; i++) { + code = str.charCodeAt(i); + res += "\\x" + (code < 16 ? "0" : "") + code.toString(16).toUpperCase(); + } + return res; +}; + +/** + * Defer the call of a function. + * @param {Function} callback the function to call asynchronously. + * @param {Array} args the arguments to give to the callback. + */ +exports.delay = function(callback, args, self) { + setImmediate(function () { + callback.apply(self || null, args || []); + }); +}; + +/** + * Extends a prototype with an other, without calling a constructor with + * side effects. Inspired by nodejs' `utils.inherits` + * @param {Function} ctor the constructor to augment + * @param {Function} superCtor the parent constructor to use + */ +exports.inherits = function (ctor, superCtor) { + var Obj = function() {}; + Obj.prototype = superCtor.prototype; + ctor.prototype = new Obj(); +}; + +/** + * Merge the objects passed as parameters into a new one. + * @private + * @param {...Object} var_args All objects to merge. + * @return {Object} a new object with the data of the others. + */ +exports.extend = function() { + var result = {}, i, attr; + for (i = 0; i < arguments.length; i++) { // arguments is not enumerable in some browsers + for (attr in arguments[i]) { + if (Object.prototype.hasOwnProperty.call(arguments[i], attr) && typeof result[attr] === "undefined") { + result[attr] = arguments[i][attr]; + } + } + } + return result; +}; + +/** + * Transform arbitrary content into a Promise. + * @param {String} name a name for the content being processed. + * @param {Object} inputData the content to process. + * @param {Boolean} isBinary true if the content is not an unicode string + * @param {Boolean} isOptimizedBinaryString true if the string content only has one byte per character. + * @param {Boolean} isBase64 true if the string content is encoded with base64. + * @return {Promise} a promise in a format usable by JSZip. + */ +exports.prepareContent = function(name, inputData, isBinary, isOptimizedBinaryString, isBase64) { + + // if inputData is already a promise, this flatten it. + var promise = external.Promise.resolve(inputData).then(function(data) { + + + var isBlob = support.blob && (data instanceof Blob || ["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(data)) !== -1); + + if (isBlob && typeof FileReader !== "undefined") { + return new external.Promise(function (resolve, reject) { + var reader = new FileReader(); + + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e.target.error); + }; + reader.readAsArrayBuffer(data); + }); + } else { + return data; + } + }); + + return promise.then(function(data) { + var dataType = exports.getTypeOf(data); + + if (!dataType) { + return external.Promise.reject( + new Error("Can't read the data of '" + name + "'. Is it " + + "in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?") + ); + } + // special case : it's way easier to work with Uint8Array than with ArrayBuffer + if (dataType === "arraybuffer") { + data = exports.transformTo("uint8array", data); + } else if (dataType === "string") { + if (isBase64) { + data = base64.decode(data); + } + else if (isBinary) { + // optimizedBinaryString === true means that the file has already been filtered with a 0xFF mask + if (isOptimizedBinaryString !== true) { + // this is a string, not in a base64 format. + // Be sure that this is a correct "binary string" + data = string2binary(data); + } + } + } + return data; + }); +}; + +},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,"setimmediate":54}],33:[function(require,module,exports){ +"use strict"; +var readerFor = require("./reader/readerFor"); +var utils = require("./utils"); +var sig = require("./signature"); +var ZipEntry = require("./zipEntry"); +var support = require("./support"); +// class ZipEntries {{{ +/** + * All the entries in the zip file. + * @constructor + * @param {Object} loadOptions Options for loading the stream. + */ +function ZipEntries(loadOptions) { + this.files = []; + this.loadOptions = loadOptions; +} +ZipEntries.prototype = { + /** + * Check that the reader is on the specified signature. + * @param {string} expectedSignature the expected signature. + * @throws {Error} if it is an other signature. + */ + checkSignature: function(expectedSignature) { + if (!this.reader.readAndCheckSignature(expectedSignature)) { + this.reader.index -= 4; + var signature = this.reader.readString(4); + throw new Error("Corrupted zip or bug: unexpected signature " + "(" + utils.pretty(signature) + ", expected " + utils.pretty(expectedSignature) + ")"); + } + }, + /** + * Check if the given signature is at the given index. + * @param {number} askedIndex the index to check. + * @param {string} expectedSignature the signature to expect. + * @return {boolean} true if the signature is here, false otherwise. + */ + isSignature: function(askedIndex, expectedSignature) { + var currentIndex = this.reader.index; + this.reader.setIndex(askedIndex); + var signature = this.reader.readString(4); + var result = signature === expectedSignature; + this.reader.setIndex(currentIndex); + return result; + }, + /** + * Read the end of the central directory. + */ + readBlockEndOfCentral: function() { + this.diskNumber = this.reader.readInt(2); + this.diskWithCentralDirStart = this.reader.readInt(2); + this.centralDirRecordsOnThisDisk = this.reader.readInt(2); + this.centralDirRecords = this.reader.readInt(2); + this.centralDirSize = this.reader.readInt(4); + this.centralDirOffset = this.reader.readInt(4); + + this.zipCommentLength = this.reader.readInt(2); + // warning : the encoding depends of the system locale + // On a linux machine with LANG=en_US.utf8, this field is utf8 encoded. + // On a windows machine, this field is encoded with the localized windows code page. + var zipComment = this.reader.readData(this.zipCommentLength); + var decodeParamType = support.uint8array ? "uint8array" : "array"; + // To get consistent behavior with the generation part, we will assume that + // this is utf8 encoded unless specified otherwise. + var decodeContent = utils.transformTo(decodeParamType, zipComment); + this.zipComment = this.loadOptions.decodeFileName(decodeContent); + }, + /** + * Read the end of the Zip 64 central directory. + * Not merged with the method readEndOfCentral : + * The end of central can coexist with its Zip64 brother, + * I don't want to read the wrong number of bytes ! + */ + readBlockZip64EndOfCentral: function() { + this.zip64EndOfCentralSize = this.reader.readInt(8); + this.reader.skip(4); + // this.versionMadeBy = this.reader.readString(2); + // this.versionNeeded = this.reader.readInt(2); + this.diskNumber = this.reader.readInt(4); + this.diskWithCentralDirStart = this.reader.readInt(4); + this.centralDirRecordsOnThisDisk = this.reader.readInt(8); + this.centralDirRecords = this.reader.readInt(8); + this.centralDirSize = this.reader.readInt(8); + this.centralDirOffset = this.reader.readInt(8); + + this.zip64ExtensibleData = {}; + var extraDataSize = this.zip64EndOfCentralSize - 44, + index = 0, + extraFieldId, + extraFieldLength, + extraFieldValue; + while (index < extraDataSize) { + extraFieldId = this.reader.readInt(2); + extraFieldLength = this.reader.readInt(4); + extraFieldValue = this.reader.readData(extraFieldLength); + this.zip64ExtensibleData[extraFieldId] = { + id: extraFieldId, + length: extraFieldLength, + value: extraFieldValue + }; + } + }, + /** + * Read the end of the Zip 64 central directory locator. + */ + readBlockZip64EndOfCentralLocator: function() { + this.diskWithZip64CentralDirStart = this.reader.readInt(4); + this.relativeOffsetEndOfZip64CentralDir = this.reader.readInt(8); + this.disksCount = this.reader.readInt(4); + if (this.disksCount > 1) { + throw new Error("Multi-volumes zip are not supported"); + } + }, + /** + * Read the local files, based on the offset read in the central part. + */ + readLocalFiles: function() { + var i, file; + for (i = 0; i < this.files.length; i++) { + file = this.files[i]; + this.reader.setIndex(file.localHeaderOffset); + this.checkSignature(sig.LOCAL_FILE_HEADER); + file.readLocalPart(this.reader); + file.handleUTF8(); + file.processAttributes(); + } + }, + /** + * Read the central directory. + */ + readCentralDir: function() { + var file; + + this.reader.setIndex(this.centralDirOffset); + while (this.reader.readAndCheckSignature(sig.CENTRAL_FILE_HEADER)) { + file = new ZipEntry({ + zip64: this.zip64 + }, this.loadOptions); + file.readCentralPart(this.reader); + this.files.push(file); + } + + if (this.centralDirRecords !== this.files.length) { + if (this.centralDirRecords !== 0 && this.files.length === 0) { + // We expected some records but couldn't find ANY. + // This is really suspicious, as if something went wrong. + throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length); + } else { + // We found some records but not all. + // Something is wrong but we got something for the user: no error here. + // console.warn("expected", this.centralDirRecords, "records in central dir, got", this.files.length); + } + } + }, + /** + * Read the end of central directory. + */ + readEndOfCentral: function() { + var offset = this.reader.lastIndexOfSignature(sig.CENTRAL_DIRECTORY_END); + if (offset < 0) { + // Check if the content is a truncated zip or complete garbage. + // A "LOCAL_FILE_HEADER" is not required at the beginning (auto + // extractible zip for example) but it can give a good hint. + // If an ajax request was used without responseType, we will also + // get unreadable data. + var isGarbage = !this.isSignature(0, sig.LOCAL_FILE_HEADER); + + if (isGarbage) { + throw new Error("Can't find end of central directory : is this a zip file ? " + + "If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"); + } else { + throw new Error("Corrupted zip: can't find end of central directory"); + } + + } + this.reader.setIndex(offset); + var endOfCentralDirOffset = offset; + this.checkSignature(sig.CENTRAL_DIRECTORY_END); + this.readBlockEndOfCentral(); + + + /* extract from the zip spec : + 4) If one of the fields in the end of central directory + record is too small to hold required data, the field + should be set to -1 (0xFFFF or 0xFFFFFFFF) and the + ZIP64 format record should be created. + 5) The end of central directory record and the + Zip64 end of central directory locator record must + reside on the same disk when splitting or spanning + an archive. + */ + if (this.diskNumber === utils.MAX_VALUE_16BITS || this.diskWithCentralDirStart === utils.MAX_VALUE_16BITS || this.centralDirRecordsOnThisDisk === utils.MAX_VALUE_16BITS || this.centralDirRecords === utils.MAX_VALUE_16BITS || this.centralDirSize === utils.MAX_VALUE_32BITS || this.centralDirOffset === utils.MAX_VALUE_32BITS) { + this.zip64 = true; + + /* + Warning : the zip64 extension is supported, but ONLY if the 64bits integer read from + the zip file can fit into a 32bits integer. This cannot be solved : JavaScript represents + all numbers as 64-bit double precision IEEE 754 floating point numbers. + So, we have 53bits for integers and bitwise operations treat everything as 32bits. + see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators + and http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf section 8.5 + */ + + // should look for a zip64 EOCD locator + offset = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR); + if (offset < 0) { + throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator"); + } + this.reader.setIndex(offset); + this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR); + this.readBlockZip64EndOfCentralLocator(); + + // now the zip64 EOCD record + if (!this.isSignature(this.relativeOffsetEndOfZip64CentralDir, sig.ZIP64_CENTRAL_DIRECTORY_END)) { + // console.warn("ZIP64 end of central directory not where expected."); + this.relativeOffsetEndOfZip64CentralDir = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); + if (this.relativeOffsetEndOfZip64CentralDir < 0) { + throw new Error("Corrupted zip: can't find the ZIP64 end of central directory"); + } + } + this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir); + this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); + this.readBlockZip64EndOfCentral(); + } + + var expectedEndOfCentralDirOffset = this.centralDirOffset + this.centralDirSize; + if (this.zip64) { + expectedEndOfCentralDirOffset += 20; // end of central dir 64 locator + expectedEndOfCentralDirOffset += 12 /* should not include the leading 12 bytes */ + this.zip64EndOfCentralSize; + } + + var extraBytes = endOfCentralDirOffset - expectedEndOfCentralDirOffset; + + if (extraBytes > 0) { + // console.warn(extraBytes, "extra bytes at beginning or within zipfile"); + if (this.isSignature(endOfCentralDirOffset, sig.CENTRAL_FILE_HEADER)) { + // The offsets seem wrong, but we have something at the specified offset. + // So… we keep it. + } else { + // the offset is wrong, update the "zero" of the reader + // this happens if data has been prepended (crx files for example) + this.reader.zero = extraBytes; + } + } else if (extraBytes < 0) { + throw new Error("Corrupted zip: missing " + Math.abs(extraBytes) + " bytes."); + } + }, + prepareReader: function(data) { + this.reader = readerFor(data); + }, + /** + * Read a zip file and create ZipEntries. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the binary string representing a zip file. + */ + load: function(data) { + this.prepareReader(data); + this.readEndOfCentral(); + this.readCentralDir(); + this.readLocalFiles(); + } +}; +// }}} end of ZipEntries +module.exports = ZipEntries; + +},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(require,module,exports){ +"use strict"; +var readerFor = require("./reader/readerFor"); +var utils = require("./utils"); +var CompressedObject = require("./compressedObject"); +var crc32fn = require("./crc32"); +var utf8 = require("./utf8"); +var compressions = require("./compressions"); +var support = require("./support"); + +var MADE_BY_DOS = 0x00; +var MADE_BY_UNIX = 0x03; + +/** + * Find a compression registered in JSZip. + * @param {string} compressionMethod the method magic to find. + * @return {Object|null} the JSZip compression object, null if none found. + */ +var findCompression = function(compressionMethod) { + for (var method in compressions) { + if (!Object.prototype.hasOwnProperty.call(compressions, method)) { + continue; + } + if (compressions[method].magic === compressionMethod) { + return compressions[method]; + } + } + return null; +}; + +// class ZipEntry {{{ +/** + * An entry in the zip file. + * @constructor + * @param {Object} options Options of the current file. + * @param {Object} loadOptions Options for loading the stream. + */ +function ZipEntry(options, loadOptions) { + this.options = options; + this.loadOptions = loadOptions; +} +ZipEntry.prototype = { + /** + * say if the file is encrypted. + * @return {boolean} true if the file is encrypted, false otherwise. + */ + isEncrypted: function() { + // bit 1 is set + return (this.bitFlag & 0x0001) === 0x0001; + }, + /** + * say if the file has utf-8 filename/comment. + * @return {boolean} true if the filename/comment is in utf-8, false otherwise. + */ + useUTF8: function() { + // bit 11 is set + return (this.bitFlag & 0x0800) === 0x0800; + }, + /** + * Read the local part of a zip file and add the info in this object. + * @param {DataReader} reader the reader to use. + */ + readLocalPart: function(reader) { + var compression, localExtraFieldsLength; + + // we already know everything from the central dir ! + // If the central dir data are false, we are doomed. + // On the bright side, the local part is scary : zip64, data descriptors, both, etc. + // The less data we get here, the more reliable this should be. + // Let's skip the whole header and dash to the data ! + reader.skip(22); + // in some zip created on windows, the filename stored in the central dir contains \ instead of /. + // Strangely, the filename here is OK. + // I would love to treat these zip files as corrupted (see http://www.info-zip.org/FAQ.html#backslashes + // or APPNOTE#4.4.17.1, "All slashes MUST be forward slashes '/'") but there are a lot of bad zip generators... + // Search "unzip mismatching "local" filename continuing with "central" filename version" on + // the internet. + // + // I think I see the logic here : the central directory is used to display + // content and the local directory is used to extract the files. Mixing / and \ + // may be used to display \ to windows users and use / when extracting the files. + // Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394 + this.fileNameLength = reader.readInt(2); + localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir + // the fileName is stored as binary data, the handleUTF8 method will take care of the encoding. + this.fileName = reader.readData(this.fileNameLength); + reader.skip(localExtraFieldsLength); + + if (this.compressedSize === -1 || this.uncompressedSize === -1) { + throw new Error("Bug or corrupted zip : didn't get enough information from the central directory " + "(compressedSize === -1 || uncompressedSize === -1)"); + } + + compression = findCompression(this.compressionMethod); + if (compression === null) { // no compression found + throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); + } + this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); + }, + + /** + * Read the central part of a zip file and add the info in this object. + * @param {DataReader} reader the reader to use. + */ + readCentralPart: function(reader) { + this.versionMadeBy = reader.readInt(2); + reader.skip(2); + // this.versionNeeded = reader.readInt(2); + this.bitFlag = reader.readInt(2); + this.compressionMethod = reader.readString(2); + this.date = reader.readDate(); + this.crc32 = reader.readInt(4); + this.compressedSize = reader.readInt(4); + this.uncompressedSize = reader.readInt(4); + var fileNameLength = reader.readInt(2); + this.extraFieldsLength = reader.readInt(2); + this.fileCommentLength = reader.readInt(2); + this.diskNumberStart = reader.readInt(2); + this.internalFileAttributes = reader.readInt(2); + this.externalFileAttributes = reader.readInt(4); + this.localHeaderOffset = reader.readInt(4); + + if (this.isEncrypted()) { + throw new Error("Encrypted zip are not supported"); + } + + // will be read in the local part, see the comments there + reader.skip(fileNameLength); + this.readExtraFields(reader); + this.parseZIP64ExtraField(reader); + this.fileComment = reader.readData(this.fileCommentLength); + }, + + /** + * Parse the external file attributes and get the unix/dos permissions. + */ + processAttributes: function () { + this.unixPermissions = null; + this.dosPermissions = null; + var madeBy = this.versionMadeBy >> 8; + + // Check if we have the DOS directory flag set. + // We look for it in the DOS and UNIX permissions + // but some unknown platform could set it as a compatibility flag. + this.dir = this.externalFileAttributes & 0x0010 ? true : false; + + if(madeBy === MADE_BY_DOS) { + // first 6 bits (0 to 5) + this.dosPermissions = this.externalFileAttributes & 0x3F; + } + + if(madeBy === MADE_BY_UNIX) { + this.unixPermissions = (this.externalFileAttributes >> 16) & 0xFFFF; + // the octal permissions are in (this.unixPermissions & 0x01FF).toString(8); + } + + // fail safe : if the name ends with a / it probably means a folder + if (!this.dir && this.fileNameStr.slice(-1) === "/") { + this.dir = true; + } + }, + + /** + * Parse the ZIP64 extra field and merge the info in the current ZipEntry. + * @param {DataReader} reader the reader to use. + */ + parseZIP64ExtraField: function() { + if (!this.extraFields[0x0001]) { + return; + } + + // should be something, preparing the extra reader + var extraReader = readerFor(this.extraFields[0x0001].value); + + // I really hope that these 64bits integer can fit in 32 bits integer, because js + // won't let us have more. + if (this.uncompressedSize === utils.MAX_VALUE_32BITS) { + this.uncompressedSize = extraReader.readInt(8); + } + if (this.compressedSize === utils.MAX_VALUE_32BITS) { + this.compressedSize = extraReader.readInt(8); + } + if (this.localHeaderOffset === utils.MAX_VALUE_32BITS) { + this.localHeaderOffset = extraReader.readInt(8); + } + if (this.diskNumberStart === utils.MAX_VALUE_32BITS) { + this.diskNumberStart = extraReader.readInt(4); + } + }, + /** + * Read the central part of a zip file and add the info in this object. + * @param {DataReader} reader the reader to use. + */ + readExtraFields: function(reader) { + var end = reader.index + this.extraFieldsLength, + extraFieldId, + extraFieldLength, + extraFieldValue; + + if (!this.extraFields) { + this.extraFields = {}; + } + + while (reader.index + 4 < end) { + extraFieldId = reader.readInt(2); + extraFieldLength = reader.readInt(2); + extraFieldValue = reader.readData(extraFieldLength); + + this.extraFields[extraFieldId] = { + id: extraFieldId, + length: extraFieldLength, + value: extraFieldValue + }; + } + + reader.setIndex(end); + }, + /** + * Apply an UTF8 transformation if needed. + */ + handleUTF8: function() { + var decodeParamType = support.uint8array ? "uint8array" : "array"; + if (this.useUTF8()) { + this.fileNameStr = utf8.utf8decode(this.fileName); + this.fileCommentStr = utf8.utf8decode(this.fileComment); + } else { + var upath = this.findExtraFieldUnicodePath(); + if (upath !== null) { + this.fileNameStr = upath; + } else { + // ASCII text or unsupported code page + var fileNameByteArray = utils.transformTo(decodeParamType, this.fileName); + this.fileNameStr = this.loadOptions.decodeFileName(fileNameByteArray); + } + + var ucomment = this.findExtraFieldUnicodeComment(); + if (ucomment !== null) { + this.fileCommentStr = ucomment; + } else { + // ASCII text or unsupported code page + var commentByteArray = utils.transformTo(decodeParamType, this.fileComment); + this.fileCommentStr = this.loadOptions.decodeFileName(commentByteArray); + } + } + }, + + /** + * Find the unicode path declared in the extra field, if any. + * @return {String} the unicode path, null otherwise. + */ + findExtraFieldUnicodePath: function() { + var upathField = this.extraFields[0x7075]; + if (upathField) { + var extraReader = readerFor(upathField.value); + + // wrong version + if (extraReader.readInt(1) !== 1) { + return null; + } + + // the crc of the filename changed, this field is out of date. + if (crc32fn(this.fileName) !== extraReader.readInt(4)) { + return null; + } + + return utf8.utf8decode(extraReader.readData(upathField.length - 5)); + } + return null; + }, + + /** + * Find the unicode comment declared in the extra field, if any. + * @return {String} the unicode comment, null otherwise. + */ + findExtraFieldUnicodeComment: function() { + var ucommentField = this.extraFields[0x6375]; + if (ucommentField) { + var extraReader = readerFor(ucommentField.value); + + // wrong version + if (extraReader.readInt(1) !== 1) { + return null; + } + + // the crc of the comment changed, this field is out of date. + if (crc32fn(this.fileComment) !== extraReader.readInt(4)) { + return null; + } + + return utf8.utf8decode(extraReader.readData(ucommentField.length - 5)); + } + return null; + } +}; +module.exports = ZipEntry; + +},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(require,module,exports){ +"use strict"; + +var StreamHelper = require("./stream/StreamHelper"); +var DataWorker = require("./stream/DataWorker"); +var utf8 = require("./utf8"); +var CompressedObject = require("./compressedObject"); +var GenericWorker = require("./stream/GenericWorker"); + +/** + * A simple object representing a file in the zip file. + * @constructor + * @param {string} name the name of the file + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data + * @param {Object} options the options of the file + */ +var ZipObject = function(name, data, options) { + this.name = name; + this.dir = options.dir; + this.date = options.date; + this.comment = options.comment; + this.unixPermissions = options.unixPermissions; + this.dosPermissions = options.dosPermissions; + + this._data = data; + this._dataBinary = options.binary; + // keep only the compression + this.options = { + compression : options.compression, + compressionOptions : options.compressionOptions + }; +}; + +ZipObject.prototype = { + /** + * Create an internal stream for the content of this object. + * @param {String} type the type of each chunk. + * @return StreamHelper the stream. + */ + internalStream: function (type) { + var result = null, outputType = "string"; + try { + if (!type) { + throw new Error("No output type specified."); + } + outputType = type.toLowerCase(); + var askUnicodeString = outputType === "string" || outputType === "text"; + if (outputType === "binarystring" || outputType === "text") { + outputType = "string"; + } + result = this._decompressWorker(); + + var isUnicodeString = !this._dataBinary; + + if (isUnicodeString && !askUnicodeString) { + result = result.pipe(new utf8.Utf8EncodeWorker()); + } + if (!isUnicodeString && askUnicodeString) { + result = result.pipe(new utf8.Utf8DecodeWorker()); + } + } catch (e) { + result = new GenericWorker("error"); + result.error(e); + } + + return new StreamHelper(result, outputType, ""); + }, + + /** + * Prepare the content in the asked type. + * @param {String} type the type of the result. + * @param {Function} onUpdate a function to call on each internal update. + * @return Promise the promise of the result. + */ + async: function (type, onUpdate) { + return this.internalStream(type).accumulate(onUpdate); + }, + + /** + * Prepare the content as a nodejs stream. + * @param {String} type the type of each chunk. + * @param {Function} onUpdate a function to call on each internal update. + * @return Stream the stream. + */ + nodeStream: function (type, onUpdate) { + return this.internalStream(type || "nodebuffer").toNodejsStream(onUpdate); + }, + + /** + * Return a worker for the compressed content. + * @private + * @param {Object} compression the compression object to use. + * @param {Object} compressionOptions the options to use when compressing. + * @return Worker the worker. + */ + _compressWorker: function (compression, compressionOptions) { + if ( + this._data instanceof CompressedObject && + this._data.compression.magic === compression.magic + ) { + return this._data.getCompressedWorker(); + } else { + var result = this._decompressWorker(); + if(!this._dataBinary) { + result = result.pipe(new utf8.Utf8EncodeWorker()); + } + return CompressedObject.createWorkerFrom(result, compression, compressionOptions); + } + }, + /** + * Return a worker for the decompressed content. + * @private + * @return Worker the worker. + */ + _decompressWorker : function () { + if (this._data instanceof CompressedObject) { + return this._data.getContentWorker(); + } else if (this._data instanceof GenericWorker) { + return this._data; + } else { + return new DataWorker(this._data); + } + } +}; + +var removedMethods = ["asText", "asBinary", "asNodeBuffer", "asUint8Array", "asArrayBuffer"]; +var removedFn = function () { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); +}; + +for(var i = 0; i < removedMethods.length; i++) { + ZipObject.prototype[removedMethods[i]] = removedFn; +} +module.exports = ZipObject; + +},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(require,module,exports){ +(function (global){ +'use strict'; +var Mutation = global.MutationObserver || global.WebKitMutationObserver; + +var scheduleDrain; + +{ + if (Mutation) { + var called = 0; + var observer = new Mutation(nextTick); + var element = global.document.createTextNode(''); + observer.observe(element, { + characterData: true + }); + scheduleDrain = function () { + element.data = (called = ++called % 2); + }; + } else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') { + var channel = new global.MessageChannel(); + channel.port1.onmessage = nextTick; + scheduleDrain = function () { + channel.port2.postMessage(0); + }; + } else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) { + scheduleDrain = function () { + + // Create a + + @@ -30,6 +31,15 @@ +
+
    +
  • + +
  • +
+