From 7dc0469b30a0c3c896c35025c556fa3346739070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E9=9B=84?= Date: Mon, 19 Jan 2026 17:46:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E9=A1=B5=E7=A0=81?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=20=E7=BE=8E=E5=8C=96=E5=89=8D=E7=AB=AF=20?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E5=8A=9F=E8=83=BD=E8=B0=83=E6=95=B4=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20cccd42-e1fc-4cb4-b9c6-8371f160cebc.dmp | Bin 71802 -> 0 bytes .../cn/keking/config/ConfigConstants.java | 125 +- .../keking/config/ConfigRefreshComponent.java | 45 +- .../cn/keking/service/CadToPdfService.java | 81 +- .../cn/keking/service/FileHandlerService.java | 10 +- .../cn/keking/service/Mediatomp4Service.java | 50 +- .../cn/keking/service/OfficeToPdfService.java | 17 +- .../cn/keking/service/PdfToJpgService.java | 923 ++- .../cn/keking/service/TifToPdfService.java | 35 +- .../service/impl/CadFilePreviewImpl.java | 24 +- .../service/impl/CodeFilePreviewImpl.java | 5 + .../service/impl/MediaFilePreviewImpl.java | 214 +- .../service/impl/OfficeFilePreviewImpl.java | 265 +- .../service/impl/PdfFilePreviewImpl.java | 262 +- .../service/impl/SimTextFilePreviewImpl.java | 2 +- .../service/impl/TiffFilePreviewImpl.java | 182 +- .../java/cn/keking/utils/DownloadUtils.java | 2 +- .../java/cn/keking/utils/KkFileUtils.java | 13 +- .../java/cn/keking/utils/UrlEncoderUtils.java | 14 +- .../main/java/cn/keking/utils/WebUtils.java | 8 +- .../keking/web/controller/FileController.java | 326 +- .../controller/OnlinePreviewController.java | 16 +- .../keking/web/filter/AttributeSetFilter.java | 8 + .../src/main/resources/static/css/loading.gif | Bin 0 -> 69010 bytes .../resources/static/css/officePicture.css | 115 + .../resources/static/highlight/highlight.css | 157 + server/src/main/resources/static/js/aes.js | 234 + .../src/main/resources/static/js/crypto-js.js | 6191 +++++++++++++++++ server/src/main/resources/static/js/fenye.js | 299 + .../src/main/resources/static/js/jsformat.js | 72 + .../main/resources/static/js/viewer.min.js | 6 +- .../main/resources/static/pdfjs/build/pdf.mjs | 22 +- server/src/main/resources/web/bpmn.ftl | 5 +- server/src/main/resources/web/code.ftl | 55 +- server/src/main/resources/web/dcm.ftl | 5 +- server/src/main/resources/web/drawio.ftl | 5 +- server/src/main/resources/web/eml.ftl | 7 +- server/src/main/resources/web/epub.ftl | 7 +- server/src/main/resources/web/main/index.ftl | 724 +- server/src/main/resources/web/ofd.ftl | 10 +- .../src/main/resources/web/officePicture.ftl | 351 +- server/src/main/resources/web/officeweb.ftl | 332 +- server/src/main/resources/web/pdf.ftl | 5 +- server/src/main/resources/web/picture.ftl | 24 +- server/src/main/resources/web/svg.ftl | 423 +- server/src/main/resources/web/tiff.ftl | 114 +- server/src/main/resources/web/txt.ftl | 366 +- server/src/main/resources/web/xmind.ftl | 7 +- 48 files changed, 10922 insertions(+), 1241 deletions(-) delete mode 100644 server/LibreOfficePortable/App/libreoffice/program/20cccd42-e1fc-4cb4-b9c6-8371f160cebc.dmp create mode 100644 server/src/main/resources/static/css/loading.gif create mode 100644 server/src/main/resources/static/css/officePicture.css create mode 100644 server/src/main/resources/static/highlight/highlight.css create mode 100644 server/src/main/resources/static/js/aes.js create mode 100644 server/src/main/resources/static/js/crypto-js.js create mode 100644 server/src/main/resources/static/js/fenye.js create mode 100644 server/src/main/resources/static/js/jsformat.js diff --git a/server/LibreOfficePortable/App/libreoffice/program/20cccd42-e1fc-4cb4-b9c6-8371f160cebc.dmp b/server/LibreOfficePortable/App/libreoffice/program/20cccd42-e1fc-4cb4-b9c6-8371f160cebc.dmp deleted file mode 100644 index 55c2f9768d3522eb31f0c498720cd6181e61af21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71802 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 diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java index 9a83f0f6..8d640b4a 100644 --- a/server/src/main/java/cn/keking/config/ConfigConstants.java +++ b/server/src/main/java/cn/keking/config/ConfigConstants.java @@ -17,6 +17,7 @@ import java.util.concurrent.CopyOnWriteArraySet; public class ConfigConstants { public static final String BEAN_NAME = "configConstants"; + static { // PDFBox兼容低版本JDK System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider"); @@ -90,7 +91,7 @@ public class ConfigConstants { public static final String DEFAULT_PICTURES_PREVIEW = "true"; public static final String DEFAULT_GET_CORS_FILE = "true"; public static final String DEFAULT_ADD_TASK = "true"; - public static final String DEFAULT_AES_KEY = "1234567890123456"; + public static final String DEFAULT_AES_KEY = "false"; // 12. UserAgent配置常量 public static final String DEFAULT_USER_AGENT = "false"; @@ -134,6 +135,17 @@ public class ConfigConstants { // 20. 重定向启用配置常量 public static final String DEFAULT_ENABLE_REDIRECT = "true"; + // 22. 异步定时 + public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5"; + + // 23. 其他配置常量 + public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456"; + public static final String DEFAULT_IS_JAVASCRIPT = "false"; + public static final String DEFAULT_XLSX_ALLOW_EDIT = "false"; + public static final String DEFAULT_XLSX_SHOW_TOOLBAR = "false"; + public static final String DEFAULT_IS_SHOW_KEY= "false"; + public static final String DEFAULT_SCRIPT_JS ="false" ; + // ================================================== // 配置变量定义区(按功能分类,均为静态变量) // ================================================== @@ -254,6 +266,18 @@ public class ConfigConstants { // 21. 重定向启用配置 private static Boolean enableRedirect; + // 22. 异步定时 + private static int refreshSchedule; + + // 23. 其他配置变量 + private static boolean isShowaesKey; + private static boolean isJavaScript; + private static boolean xlsxAllowEdit; + private static boolean xlsxShowtoolbar; + private static boolean isShowKey; + private static boolean scriptJs; + + // ================================================== // 获取方法(按功能分类) @@ -579,6 +603,36 @@ public class ConfigConstants { return enableRedirect; } + // 22. 异步定时刷新时间 + public static int getTime() { + return 0; + } + + // 23. 其他配置获取方法 + public static boolean getisShowaesKey() { + return isShowaesKey; + } + + public static boolean getisJavaScript() { + return isJavaScript; + } + + public static boolean getxlsxAllowEdit() { + return xlsxAllowEdit; + } + + public static boolean getxlsxShowtoolbar() { + return xlsxShowtoolbar; + } + + public static boolean getisShowKey() { + return isShowKey; + } + + public static boolean getscriptJs() { + return scriptJs; + } + // ================================================== // Setter方法(按功能分类) // ================================================== @@ -1036,7 +1090,7 @@ public class ConfigConstants { } // 12. 权限配置Setter方法 - @Value("${kk.Key:}") + @Value("${kk.key:false}") public void setKey(String key) { setKeyValue(key); } @@ -1072,7 +1126,7 @@ public class ConfigConstants { ConfigConstants.addTask = addTask; } - @Value("${ase.key:1234567890123456}") + @Value("${aes.key:1234567890123456}") public void setaesKey(String aesKey) { setaesKeyValue(aesKey); } @@ -1305,4 +1359,69 @@ public class ConfigConstants { public static void setEnableRedirectValue(Boolean enableRedirect) { ConfigConstants.enableRedirect = enableRedirect; } + + // 22 异步定时刷新时间 + @Value("${kk.refreshSchedule:5}") + public void setRefreshSchedule(int refreshSchedule) { + setRefreshScheduleValue(refreshSchedule); + } + + public static void setRefreshScheduleValue(int refreshSchedule) { + ConfigConstants.refreshSchedule = refreshSchedule; + } + + // 23. 其他配置Setter方法 + @Value("${kk.isshowaeskey:false}") + public void setIsShowaesKey(String isShowaesKey) { + setIsShowaesKeyValue(Boolean.parseBoolean(isShowaesKey)); + } + + public static void setIsShowaesKeyValue(boolean isShowaesKey) { + ConfigConstants.isShowaesKey = isShowaesKey; + } + + @Value("${kk.isjavascript:false}") + public void setIsJavaScript(String isJavaScript) { + setIsJavaScriptValue(Boolean.parseBoolean(isJavaScript)); + } + + public static void setIsJavaScriptValue(boolean isJavaScript) { + ConfigConstants.isJavaScript = isJavaScript; + } + + @Value("${kk.xlsxallowedit:false}") + public void setXlsxAllowEdit(String xlsxAllowEdit) { + setXlsxAllowEditValue(Boolean.parseBoolean(xlsxAllowEdit)); + } + + public static void setXlsxAllowEditValue(boolean xlsxAllowEdit) { + ConfigConstants.xlsxAllowEdit = xlsxAllowEdit; + } + + @Value("${kk.xlsxshowtoolbar:false}") + public void setXlsxShowtoolbar(String xlsxShowtoolbar) { + setXlsxShowtoolbarValue(Boolean.parseBoolean(xlsxShowtoolbar)); + } + + public static void setXlsxShowtoolbarValue(boolean xlsxShowtoolbar) { + ConfigConstants.xlsxShowtoolbar = xlsxShowtoolbar; + } + + @Value("${kk.isshowkey:false}") + public void setisShowKey(String isShowKey) { + setisShowKeyValue(Boolean.parseBoolean(isShowKey)); + } + + public static void setisShowKeyValue(boolean isShowKey) { + ConfigConstants.isShowKey = isShowKey; + } + + @Value("${kk.scriptjs:false}") + public void setscriptJs(String scriptJs) { + setscriptJsValue(Boolean.parseBoolean(scriptJs)); + } + + public static void setscriptJsValue(boolean scriptJs) { + ConfigConstants.scriptJs = scriptJs; + } } \ No newline at end of file diff --git a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java index c34e3dfe..8531c069 100644 --- a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java +++ b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java @@ -251,11 +251,11 @@ public class ConfigRefreshComponent { String homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH); // 12. 权限配置 - String key = properties.getProperty("kk.Key", ConfigConstants.DEFAULT_KEY); + String key = properties.getProperty("kk.key=", ConfigConstants.DEFAULT_KEY); boolean picturesPreview = Boolean.parseBoolean(properties.getProperty("kk.Picturespreview", ConfigConstants.DEFAULT_PICTURES_PREVIEW)); boolean getCorsFile = Boolean.parseBoolean(properties.getProperty("kk.Getcorsfile", ConfigConstants.DEFAULT_GET_CORS_FILE)); boolean addTask = Boolean.parseBoolean(properties.getProperty("kk.addTask", ConfigConstants.DEFAULT_ADD_TASK)); - String aesKey = properties.getProperty("ase.key", ConfigConstants.DEFAULT_AES_KEY); + String aesKey = properties.getProperty("aes.key", ConfigConstants.DEFAULT_AES_KEY); // 13. UserAgent配置 String userAgent = properties.getProperty("useragent", ConfigConstants.DEFAULT_USER_AGENT); @@ -299,6 +299,17 @@ public class ConfigRefreshComponent { // 21. 重定向启用配置 boolean enableRedirect = Boolean.parseBoolean(properties.getProperty("kk.enable.redirect", ConfigConstants.DEFAULT_ENABLE_REDIRECT)); + // 22. 重定向启用配置 + int refreshSchedule = Integer.parseInt(properties.getProperty("kk.refreshSchedule ", ConfigConstants.DEFAULT_ENABLE_REFRECSHSCHEDULE).trim()); + + // 23. 其他配置 + boolean isShowaesKey = Boolean.parseBoolean(properties.getProperty("kk.isshowaeskey", ConfigConstants.DEFAULT_SHOW_AES_KEY)); + boolean isJavaScript = Boolean.parseBoolean(properties.getProperty("kk.isjavascript", ConfigConstants.DEFAULT_IS_JAVASCRIPT)); + boolean xlsxAllowEdit = Boolean.parseBoolean(properties.getProperty("kk.xlsxallowedit", ConfigConstants.DEFAULT_XLSX_ALLOW_EDIT)); + boolean xlsxShowtoolbar = Boolean.parseBoolean(properties.getProperty("kk.xlsxshowtoolbar", ConfigConstants.DEFAULT_XLSX_SHOW_TOOLBAR)); + boolean isShowKey = Boolean.parseBoolean(properties.getProperty("kk.isshowkey", ConfigConstants.DEFAULT_IS_SHOW_KEY)); + boolean scriptJs = Boolean.parseBoolean(properties.getProperty("kk.scriptjs", ConfigConstants.DEFAULT_SCRIPT_JS)); + // 设置配置值 // 1. 缓存配置 ConfigConstants.setCacheEnabledValueValue(cacheEnabled); @@ -385,16 +396,6 @@ public class ConfigRefreshComponent { ConfigConstants.setMediaXLFileTimeoutValue(mediaXLFileTimeout); ConfigConstants.setMediaXXLFileTimeoutValue(mediaXXLFileTimeout); ConfigConstants.setMediaXXXLFileTimeoutValue(mediaXXXLFileTimeout); - - // 19. CAD水印配置 - ConfigConstants.setCadwatermarkValue(cadwatermark); - - // 20. SSL忽略配置 - ConfigConstants.setIgnoreSSLValue(ignoreSSL); - - // 21. 重定向启用配置 - ConfigConstants.setEnableRedirectValue(enableRedirect); - // 16. PDF DPI配置 ConfigConstants.setPdfDpiEnabledValue(pdfDpiEnabled); ConfigConstants.setPdfSmallDpiValue(pdfSmallDpi); @@ -411,6 +412,26 @@ public class ConfigRefreshComponent { // 18. PDF线程配置 ConfigConstants.setPdfMaxThreadsValue(pdfMaxThreads); + + // 19. CAD水印配置 + ConfigConstants.setCadwatermarkValue(cadwatermark); + + // 20. SSL忽略配置 + ConfigConstants.setIgnoreSSLValue(ignoreSSL); + + // 21. 重定向启用配置 + ConfigConstants.setEnableRedirectValue(enableRedirect); + + // 22. 重定向启用配置 + ConfigConstants.setRefreshScheduleValue(refreshSchedule); + + // 23. 其他配置 + ConfigConstants.setIsShowaesKeyValue(isShowaesKey); + ConfigConstants.setIsJavaScriptValue(isJavaScript); + ConfigConstants.setXlsxAllowEditValue(xlsxAllowEdit); + ConfigConstants.setXlsxShowtoolbarValue(xlsxShowtoolbar); + ConfigConstants.setisShowKeyValue(isShowKey); + ConfigConstants.setscriptJsValue(scriptJs); } /** diff --git a/server/src/main/java/cn/keking/service/CadToPdfService.java b/server/src/main/java/cn/keking/service/CadToPdfService.java index 9e9a0c31..1fbdfd4e 100644 --- a/server/src/main/java/cn/keking/service/CadToPdfService.java +++ b/server/src/main/java/cn/keking/service/CadToPdfService.java @@ -56,44 +56,44 @@ public class CadToPdfService { * @param fileAttribute 文件属性 * @return 转换结果的CompletableFuture */ - public CompletableFuture cadToPdfAsync(String inputFilePath, String outputFilePath, + public CompletableFuture cadToPdfAsync(String inputFilePath, String outputFilePath,String cacheName, String cadPreviewType, FileAttribute fileAttribute) { - String fileName = new File(inputFilePath).getName(); + // 立即创建初始状态,防止重复执行 - FileConvertStatusManager.startConvert(fileName); + FileConvertStatusManager.startConvert(cacheName); // 创建可取消的任务 CompletableFuture taskFuture = new CompletableFuture<>(); - taskCompletionStatus.put(fileName, new AtomicBoolean(false)); + taskCompletionStatus.put(cacheName, new AtomicBoolean(false)); // 提交任务到线程池 Future future = virtualThreadExecutor.submit(() -> { try { // 添加初始状态更新 - FileConvertStatusManager.updateProgress(fileName, "正在启动转换任务", 5); - boolean result = convertCadWithConcurrencyControl(inputFilePath, outputFilePath, + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 5); + boolean result = convertCadWithConcurrencyControl(inputFilePath, outputFilePath,cacheName, cadPreviewType, fileAttribute); if (result) { taskFuture.complete(true); - taskCompletionStatus.get(fileName).set(true); + taskCompletionStatus.get(cacheName).set(true); } else { taskFuture.complete(false); } } catch (Exception e) { - logger.error("CAD转换任务执行失败: {}", fileName, e); - FileConvertStatusManager.markError(fileName, "转换过程异常: " + e.getMessage()); + logger.error("CAD转换任务执行失败: {}", cacheName, e); + FileConvertStatusManager.markError(cacheName, "转换过程异常: " + e.getMessage()); taskFuture.completeExceptionally(e); } finally { // 移除任务记录 - runningTasks.remove(fileName); - taskCompletionStatus.remove(fileName); + runningTasks.remove(cacheName); + taskCompletionStatus.remove(cacheName); } }); // 记录正在运行的任务 - runningTasks.put(fileName, future); + runningTasks.put(cacheName, future); // 设置超时取消 - scheduleTimeoutCheck(fileName, taskFuture, future, outputFilePath); + scheduleTimeoutCheck(cacheName, taskFuture, future, outputFilePath); return taskFuture; } @@ -160,34 +160,34 @@ public class CadToPdfService { /** * 带并发控制的CAD转换 */ - private boolean convertCadWithConcurrencyControl(String inputFilePath, String outputFilePath, + private boolean convertCadWithConcurrencyControl(String inputFilePath, String outputFilePath,String cacheName, String cadPreviewType, FileAttribute fileAttribute) throws Exception { - String fileName = new File(inputFilePath).getName(); + long acquireStartTime = System.currentTimeMillis(); // 获取并发许可 if (!concurrentLimit.tryAcquire(30, TimeUnit.SECONDS)) { long acquireTime = System.currentTimeMillis() - acquireStartTime; - logger.warn("获取并发许可超时,文件: {}, 等待时间: {}ms", fileName, acquireTime); - FileConvertStatusManager.updateProgress(fileName, "系统繁忙,等待资源中...", 15); + logger.warn("获取并发许可超时,文件: {}, 等待时间: {}ms", cacheName, acquireTime); + FileConvertStatusManager.updateProgress(cacheName, "系统繁忙,等待资源中...", 15); throw new TimeoutException("系统繁忙,请稍后重试"); } long acquireTime = System.currentTimeMillis() - acquireStartTime; - logger.debug("获取并发许可成功: {}, 等待时间: {}ms", fileName, acquireTime); + logger.debug("获取并发许可成功: {}, 等待时间: {}ms", cacheName, acquireTime); // 更新状态 - FileConvertStatusManager.updateProgress(fileName, "已获取转换资源,开始转换", 20); + FileConvertStatusManager.updateProgress(cacheName, "已获取转换资源,开始转换", 20); long conversionStartTime = System.currentTimeMillis(); try { - boolean result = performCadConversion(inputFilePath, outputFilePath, cadPreviewType, fileAttribute); + boolean result = performCadConversion(inputFilePath, outputFilePath,cacheName, cadPreviewType, fileAttribute); long conversionTime = System.currentTimeMillis() - conversionStartTime; logger.debug("CAD转换核心完成: {}, 转换耗时: {}ms, 总耗时(含等待): {}ms", - fileName, conversionTime, conversionTime + acquireTime); + cacheName, conversionTime, conversionTime + acquireTime); return result; @@ -199,49 +199,48 @@ public class CadToPdfService { /** * 执行实际的CAD转换逻辑 */ - private boolean performCadConversion(String inputFilePath, String outputFilePath, + private boolean performCadConversion(String inputFilePath, String outputFilePath,String cacheName, String cadPreviewType, FileAttribute fileAttribute) { final InterruptionTokenSource source = new InterruptionTokenSource(); - String fileName = new File(inputFilePath).getName(); long totalStartTime = System.currentTimeMillis(); try { // 1. 验证输入参数 long validationStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在验证文件参数", 25); + FileConvertStatusManager.updateProgress(cacheName, "正在验证文件参数", 25); if (!validateInputParameters(inputFilePath, outputFilePath, cadPreviewType)) { long validationTime = System.currentTimeMillis() - validationStartTime; - logger.error("CAD转换参数验证失败: {}, 验证耗时: {}ms", fileName, validationTime); - FileConvertStatusManager.markError(fileName, "文件参数验证失败"); + logger.error("CAD转换参数验证失败: {}, 验证耗时: {}ms", cacheName, validationTime); + FileConvertStatusManager.markError(cacheName, "文件参数验证失败"); return false; } long validationTime = System.currentTimeMillis() - validationStartTime; // 2. 创建输出目录 long directoryStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在准备输出目录", 30); + FileConvertStatusManager.updateProgress(cacheName, "正在准备输出目录", 30); createOutputDirectoryIfNeeded(outputFilePath, fileAttribute.isCompressFile()); long directoryTime = System.currentTimeMillis() - directoryStartTime; // 3. 加载并转换CAD文件 long loadStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在加载CAD文件", 40); + FileConvertStatusManager.updateProgress(cacheName, "正在加载CAD文件", 40); LoadOptions loadOptions = createLoadOptions(); try (Image cadImage = Image.load(inputFilePath, loadOptions)) { long loadTime = System.currentTimeMillis() - loadStartTime; - logger.debug("CAD文件加载完成: {}, 加载耗时: {}ms", fileName, loadTime); + logger.debug("CAD文件加载完成: {}, 加载耗时: {}ms", cacheName, loadTime); - FileConvertStatusManager.updateProgress(fileName, "CAD文件加载完成,开始渲染", 50); + FileConvertStatusManager.updateProgress(cacheName, "CAD文件加载完成,开始渲染", 50); // 4. 创建光栅化选项 long rasterizationStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在设置渲染参数", 60); + FileConvertStatusManager.updateProgress(cacheName, "正在设置渲染参数", 60); CadRasterizationOptions rasterizationOptions = createRasterizationOptions(cadImage); long rasterizationTime = System.currentTimeMillis() - rasterizationStartTime; // 5. 根据预览类型创建选项 long optionsStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在配置输出格式", 70); + FileConvertStatusManager.updateProgress(cacheName, "正在配置输出格式", 70); var options = switch (cadPreviewType.toLowerCase()) { case "svg" -> createSvgOptions(rasterizationOptions, source); case "pdf" -> createPdfOptions(rasterizationOptions, source); @@ -251,15 +250,15 @@ public class CadToPdfService { long optionsTime = System.currentTimeMillis() - optionsStartTime; // 6. 保存转换结果 long saveStartTime = System.currentTimeMillis(); - FileConvertStatusManager.updateProgress(fileName, "正在生成输出文件", 80); + FileConvertStatusManager.updateProgress(cacheName, "正在生成输出文件", 80); saveConvertedFile(outputFilePath, cadImage, options); long saveTime = System.currentTimeMillis() - saveStartTime; - FileConvertStatusManager.updateProgress(fileName, "文件转换完成", 90); + FileConvertStatusManager.updateProgress(cacheName, "文件转换完成", 90); // 计算总时间 long totalTime = System.currentTimeMillis() - totalStartTime; // 记录详细的性能信息 logger.debug("CAD转换详细耗时 - 文件: {}, 验证={}ms, 目录={}ms, 加载={}ms, 光栅化={}ms, 选项={}ms, 保存={}ms, 总耗时={}ms", - fileName, validationTime, directoryTime, loadTime, + cacheName, validationTime, directoryTime, loadTime, rasterizationTime, optionsTime, saveTime, totalTime); logger.info("CAD转换完成: 总耗时: {}ms", totalTime); @@ -272,9 +271,9 @@ public class CadToPdfService { } // 转换成功,标记为完成 - FileConvertStatusManager.updateProgress(fileName, "转换成功", 100); + FileConvertStatusManager.updateProgress(cacheName, "转换成功", 100); // 短暂延迟后清理状态,给前端一个显示100%的机会 - FileConvertStatusManager.convertSuccess(fileName); + FileConvertStatusManager.convertSuccess(cacheName); return true; @@ -282,12 +281,12 @@ public class CadToPdfService { } catch (Exception e) { long totalTime = System.currentTimeMillis() - totalStartTime; - logger.error("CAD转换执行失败: {}, 耗时: {}ms", fileName, totalTime, e); + logger.error("CAD转换执行失败: {}, 耗时: {}ms", cacheName, totalTime, e); // 检查是否已经标记为超时 - FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(fileName); + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) { - FileConvertStatusManager.markError(fileName, "转换失败: " + e.getMessage()); + FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage()); } // 删除可能已创建的不完整文件 @@ -302,7 +301,7 @@ public class CadToPdfService { long cleanupTime = System.currentTimeMillis() - cleanupStartTime; long totalTime = System.currentTimeMillis() - totalStartTime; logger.debug("CAD转换资源清理完成: {}, 清理耗时: {}ms, 总耗时: {}ms", - fileName, cleanupTime, totalTime); + cacheName, cleanupTime, totalTime); } } } diff --git a/server/src/main/java/cn/keking/service/FileHandlerService.java b/server/src/main/java/cn/keking/service/FileHandlerService.java index 54e60e65..fc65b5fc 100644 --- a/server/src/main/java/cn/keking/service/FileHandlerService.java +++ b/server/src/main/java/cn/keking/service/FileHandlerService.java @@ -254,10 +254,16 @@ public class FileHandlerService { try { originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名 } catch (UnsupportedEncodingException e) { - logger.error("Failed to decode file name: {}", originFileName, e); + e.printStackTrace(); } }else { - url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义 + url = Objects.requireNonNull(WebUtils.encodeUrlFileName(url)) + .replaceAll("\\+", "%20") + .replaceAll("%3A", ":") + .replaceAll("%2F", "/") + .replaceAll("%3F", "?") + .replaceAll("%26", "&") + .replaceAll("%3D", "="); } originFileName = KkFileUtils.htmlEscape(originFileName); //文件名处理 boolean isHtmlView = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx") || suffix.equalsIgnoreCase("csv") || suffix.equalsIgnoreCase("xlsm") || suffix.equalsIgnoreCase("xlt") || suffix.equalsIgnoreCase("xltm") || suffix.equalsIgnoreCase("et") || suffix.equalsIgnoreCase("ett") || suffix.equalsIgnoreCase("xlam"); diff --git a/server/src/main/java/cn/keking/service/Mediatomp4Service.java b/server/src/main/java/cn/keking/service/Mediatomp4Service.java index 72e577aa..6b151841 100644 --- a/server/src/main/java/cn/keking/service/Mediatomp4Service.java +++ b/server/src/main/java/cn/keking/service/Mediatomp4Service.java @@ -2,6 +2,7 @@ package cn.keking.service; import cn.keking.config.ConfigConstants; import cn.keking.model.FileAttribute; +import cn.keking.utils.FileConvertStatusManager; import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.FFmpegFrameRecorder; @@ -12,7 +13,6 @@ import org.springframework.stereotype.Component; import java.io.File; import java.io.FileNotFoundException; -import java.io.IOException; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -89,15 +89,17 @@ public class Mediatomp4Service { * 异步转换方法(带任务ID和超时控制) */ public static CompletableFuture convertToMp4Async( - String filePath, String outFilePath, FileAttribute fileAttribute) { + String filePath, String outFilePath,String cacheName, FileAttribute fileAttribute) { String taskId = generateTaskId(filePath); + // 立即创建初始状态,防止重复执行 + FileConvertStatusManager.startConvert(cacheName); CompletableFuture resultFuture = new CompletableFuture<>(); // 创建转换线程 Thread conversionThread = new Thread(() -> { try { - boolean result = convertToMp4WithCancellation(filePath, outFilePath, + boolean result = convertToMp4WithCancellation(filePath, outFilePath,cacheName, fileAttribute, taskId, resultFuture); resultFuture.complete(result); } catch (Exception e) { @@ -120,10 +122,11 @@ public class Mediatomp4Service { // 设置超时监控 File inputFile = new File(filePath); long fileSizeMB = inputFile.length() / (1024 * 1024); - scheduleTimeoutMonitor(taskId, calculateTimeout(fileSizeMB)); + scheduleTimeoutMonitor(taskId, calculateTimeout(fileSizeMB),cacheName); return resultFuture; } catch (Exception e) { + FileConvertStatusManager.markError(cacheName, "转换过程异常: " + e.getMessage()); resultFuture.completeExceptionally(e); cleanupFailedFile(outFilePath); return resultFuture; @@ -134,25 +137,18 @@ public class Mediatomp4Service { * 带取消支持的同步转换方法(核心改进) */ private static boolean convertToMp4WithCancellation( - String filePath, String outFilePath, FileAttribute fileAttribute, + String filePath, String outFilePath,String cacheName, FileAttribute fileAttribute, String taskId, CompletableFuture resultFuture) throws Exception { - FFmpegFrameGrabber frameGrabber = null; FFmpegFrameRecorder recorder = null; ConversionContext context = null; - try { File sourceFile = new File(filePath); if (!sourceFile.exists()) { throw new FileNotFoundException("源文件不存在: " + filePath); } - File desFile = new File(outFilePath); - if (desFile.exists() && !fileAttribute.forceUpdatedCache()) { - logger.info("目标文件已存在,跳过转换: {}", outFilePath); - return true; - } - + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 10); // 初始化抓取器 frameGrabber = new FFmpegFrameGrabber(sourceFile); frameGrabber.setOption("stimeout", "10000000"); // 10秒超时 @@ -168,7 +164,7 @@ public class Mediatomp4Service { configureRecorder(recorder, frameGrabber); recorder.start(); - + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 40); // 创建任务上下文 context = new ConversionContext(frameGrabber, recorder); @@ -180,7 +176,7 @@ public class Mediatomp4Service { logger.info("开始转换任务 {}: {} -> {}", taskId, filePath, outFilePath); // 核心:使用非阻塞方式读取帧 - return processFramesWithTimeout(frameGrabber, recorder, context, taskId); + return processFramesWithTimeout(frameGrabber, recorder, context, taskId,cacheName); } catch (Exception e) { // 检查是否是取消操作 @@ -208,7 +204,7 @@ public class Mediatomp4Service { */ private static boolean processFramesWithTimeout( FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder, - ConversionContext context, String taskId) throws Exception { + ConversionContext context, String taskId, String cacheName) throws Exception { long frameCount = 0; long startTime = System.currentTimeMillis(); @@ -227,10 +223,9 @@ public class Mediatomp4Service { logger.warn("任务 {} 帧读取超时,可能文件损坏", taskId); throw new TimeoutException("帧读取超时"); } - // 尝试抓取帧 frame = grabber.grabFrame(); - + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 60); if (frame == null) { consecutiveNullFrames++; @@ -260,7 +255,7 @@ public class Mediatomp4Service { } continue; } - + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 80); // 成功获取到帧,重置计数器 consecutiveNullFrames = 0; lastFrameTime = System.currentTimeMillis(); @@ -295,12 +290,12 @@ public class Mediatomp4Service { // 完成录制 recorder.stop(); recorder.close(); - + FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 100); long totalTime = System.currentTimeMillis() - startTime; double fps = totalTime > 0 ? (frameCount * 1000.0) / totalTime : 0; logger.info("任务 {} 转换完成: {} 帧, 耗时: {}ms, 平均速度: {} fps", taskId, frameCount, totalTime, String.format("%.2f", fps)); - + FileConvertStatusManager.convertSuccess(cacheName); return true; } catch (Exception e) { @@ -404,11 +399,12 @@ public class Mediatomp4Service { /** * 配置超时监控 */ - private static void scheduleTimeoutMonitor(String taskId, long timeoutSeconds) { + private static void scheduleTimeoutMonitor(String taskId, long timeoutSeconds,String cacheName) { ScheduledFuture timeoutFuture = monitorExecutor.schedule(() -> { ConversionTask task = activeTasks.get(taskId); if (task != null && !task.context.completed) { logger.warn("任务 {} 超时 ({}秒),开始强制终止", taskId, timeoutSeconds); + FileConvertStatusManager.markTimeout(cacheName); cancelConversion(taskId); task.future.completeExceptionally( new TimeoutException("转换超时: " + timeoutSeconds + "秒") @@ -471,16 +467,6 @@ public class Mediatomp4Service { throw new FileNotFoundException("源文件不存在: " + filePath); } - File desFile = new File(outFilePath); - if (desFile.exists()) { - if (fileAttribute.forceUpdatedCache()) { - if (!desFile.delete()) { - throw new IOException("无法删除已存在的文件: " + outFilePath); - } - } else { - throw new IllegalStateException("目标文件已存在,跳过转换"); - } - } } /** diff --git a/server/src/main/java/cn/keking/service/OfficeToPdfService.java b/server/src/main/java/cn/keking/service/OfficeToPdfService.java index 50df239c..cd219a3d 100644 --- a/server/src/main/java/cn/keking/service/OfficeToPdfService.java +++ b/server/src/main/java/cn/keking/service/OfficeToPdfService.java @@ -79,29 +79,20 @@ public class OfficeToPdfService { // 格式化显示耗时(支持不同时间单位) String durationFormatted; if (duration.toMinutes() > 0) { - durationFormatted = String.format("%d分%d秒", - duration.toMinutes(), - duration.toSecondsPart()); + durationFormatted = String.format("%d分%d秒", duration.toMinutes(), duration.toSecondsPart()); } else if (duration.toSeconds() > 0) { - durationFormatted = String.format("%d.%03d秒", - duration.toSeconds(), - duration.toMillisPart()); + durationFormatted = String.format("%d.%03d秒",duration.toSeconds(), duration.toMillisPart()); } else { durationFormatted = String.format("%d毫秒", duration.toMillis()); } logger.info("文件转换成功:{} -> {},耗时:{}", - inputFile.getName(), - outputFile.getName(), - durationFormatted); + inputFile.getName(),outputFile.getName(), durationFormatted); } catch (OfficeException e) { Instant endTime = Instant.now(); Duration duration = Duration.between(startTime, endTime); - logger.error("文件转换失败:{},已耗时:{}毫秒,错误信息:{}", - inputFile.getName(), - duration.toMillis(), - e.getMessage()); + logger.error("文件转换失败:{},已耗时:{}毫秒,错误信息:{}", inputFile.getName(), duration.toMillis(), e.getMessage()); throw e; } } diff --git a/server/src/main/java/cn/keking/service/PdfToJpgService.java b/server/src/main/java/cn/keking/service/PdfToJpgService.java index 79a57327..49864a3d 100644 --- a/server/src/main/java/cn/keking/service/PdfToJpgService.java +++ b/server/src/main/java/cn/keking/service/PdfToJpgService.java @@ -21,65 +21,585 @@ import org.springframework.util.ObjectUtils; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * PDF转JPG服务 - 高性能优化版本 + * PDF转JPG服务 - JDK 21 高性能优化版本(使用虚拟线程和结构化并发) */ @Component public class PdfToJpgService { private final FileHandlerService fileHandlerService; - // 使用线程池替代虚拟线程,便于控制并发数 - private ExecutorService threadPoolExecutor; + // JDK 21: 使用虚拟线程池 + private ExecutorService virtualThreadExecutor; private static final Logger logger = LoggerFactory.getLogger(PdfToJpgService.class); private static final String PDF_PASSWORD_MSG = "password"; private static final String PDF2JPG_IMAGE_FORMAT = ".jpg"; private static final int BATCH_SIZE = 20; private static final int PARALLEL_BATCH_THRESHOLD = 100; - + private final Semaphore concurrentTaskSemaphore; + private final ConcurrentHashMap fileLocks = new ConcurrentHashMap<>(); // 性能监控 private final AtomicInteger activeTaskCount = new AtomicInteger(0); private final AtomicInteger totalCompletedTasks = new AtomicInteger(0); + // 存储正在运行的任务 + private final ConcurrentHashMap> runningTasks = new ConcurrentHashMap<>(); + + // 加密PDF缓存管理(内存缓存,10分钟过期) + private final ConcurrentHashMap encryptedPdfCacheMap = new ConcurrentHashMap<>(); + + // JDK 21: 使用虚拟线程调度器 + private final ScheduledExecutorService virtualCacheCleanupScheduler; + + // 使用读写锁保护缓存操作 + private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); + + // 加密PDF缓存记录类 + private static class EncryptedPdfCache { + private final long cacheTime; + private final int pageCount; + private final String outputFolder; + + EncryptedPdfCache(int pageCount, String outputFolder) { + this.cacheTime = System.currentTimeMillis(); + this.pageCount = pageCount; + this.outputFolder = outputFolder; + } + + int pageCount() { return pageCount; } + String outputFolder() { return outputFolder; } + + boolean isExpired(long expireTimeMillis) { + return System.currentTimeMillis() - cacheTime > expireTimeMillis; + } + } + public PdfToJpgService(FileHandlerService fileHandlerService) { + // JDK 21: 创建使用虚拟线程的调度器 this.fileHandlerService = fileHandlerService; + this.virtualCacheCleanupScheduler = Executors.newSingleThreadScheduledExecutor( + Thread.ofVirtual().name("pdf-cache-cleaner-", 0).factory() + ); + // 设置最大并发任务数为50(可根据配置调整) + int maxConcurrentTasks = ConfigConstants.getPdfMaxThreads(); + this.concurrentTaskSemaphore = new Semaphore(maxConcurrentTasks); } @PostConstruct public void init() { - // 使用固定大小的线程池,便于控制并发数 int maxThreads = ConfigConstants.getPdfMaxThreads(); - this.threadPoolExecutor = new ThreadPoolExecutor( - maxThreads, // 核心线程数 - maxThreads, // 最大线程数 - 60L, TimeUnit.SECONDS, // 空闲线程存活时间 - new LinkedBlockingQueue<>(100), // 任务队列 - Executors.defaultThreadFactory(), - new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行 - ); + // 使用固定大小的虚拟线程池 + this.virtualThreadExecutor = Executors.newFixedThreadPool(maxThreads, + Thread.ofVirtual().name("pdf-converter-", 0).factory()); - logger.info("PDF转换线程池初始化完成,最大线程数: {}", maxThreads); + logger.info("PDF转换虚拟线程池初始化完成,最大线程数: {}", maxThreads); + + // 启动缓存清理任务 + scheduleCacheCleanup(); } @PreDestroy public void shutdown() { - if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) { - threadPoolExecutor.shutdown(); + logger.info("开始关闭PDF转换服务..."); + + try { + // 1. 取消所有运行中的任务 + cancelAllRunningTasks(); + + // 2. 先清理内存缓存(这时执行器还可用) + clearAllMemoryCaches(); + + // 3. 并行关闭调度器和执行器 + List> shutdownFutures = new ArrayList<>(); + shutdownFutures.add(CompletableFuture.runAsync(this::shutdownCacheCleanupScheduler)); + shutdownFutures.add(CompletableFuture.runAsync(this::shutdownVirtualThreadExecutor)); + + // 等待所有关闭操作完成(最多30秒) + CompletableFuture.allOf(shutdownFutures.toArray(new CompletableFuture[0])) + .get(30, TimeUnit.SECONDS); + + } catch (TimeoutException e) { + logger.warn("PDF转换服务关闭超时,强制关闭剩余资源"); + forceShutdown(); + } catch (Exception e) { + logger.error("关闭PDF转换服务时发生异常", e); + forceShutdown(); + } + + logger.info("PDF转换服务已完全关闭"); + } + + /** + * 取消所有运行中的任务 + */ + private void cancelAllRunningTasks() { + int cancelledCount = 0; + // 使用keySet的快照,避免并发修改 + Set taskNames = new HashSet<>(runningTasks.keySet()); + for (String taskName : taskNames) { + Future future = runningTasks.get(taskName); + if (future != null) { + try { + if (future.cancel(true)) { + cancelledCount++; + logger.debug("已取消任务: {}", taskName); + } + } catch (Exception e) { + logger.warn("取消任务失败: {}", taskName, e); + } + } + } + runningTasks.clear(); + logger.info("已取消 {} 个运行中的PDF转换任务", cancelledCount); + } + + /** + * 关闭缓存清理调度器 + */ + private void shutdownCacheCleanupScheduler() { + virtualCacheCleanupScheduler.shutdown(); + try { + if (!virtualCacheCleanupScheduler.awaitTermination(10, TimeUnit.SECONDS)) { + virtualCacheCleanupScheduler.shutdownNow(); + if (!virtualCacheCleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("缓存清理调度器未完全关闭"); + } + } + } catch (InterruptedException e) { + virtualCacheCleanupScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + /** + * 关闭虚拟线程池 + */ + private void shutdownVirtualThreadExecutor() { + if (virtualThreadExecutor != null && !virtualThreadExecutor.isShutdown()) { + virtualThreadExecutor.shutdown(); try { - if (!threadPoolExecutor.awaitTermination(60, TimeUnit.SECONDS)) { - threadPoolExecutor.shutdownNow(); + if (!virtualThreadExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + virtualThreadExecutor.shutdownNow(); + if (!virtualThreadExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + logger.warn("虚拟线程池未完全关闭"); + } } } catch (InterruptedException e) { - threadPoolExecutor.shutdownNow(); + virtualThreadExecutor.shutdownNow(); Thread.currentThread().interrupt(); } - logger.info("PDF转换服务已关闭"); } } + /** + * 强制关闭(当优雅关闭失败时使用) + */ + private void forceShutdown() { + // 先清理缓存(同步方式) + try { + if (!encryptedPdfCacheMap.isEmpty()) { + logger.info("强制关闭时同步清理 {} 个缓存", encryptedPdfCacheMap.size()); + for (EncryptedPdfCache cache : encryptedPdfCacheMap.values()) { + try { + deleteCacheFolder(cache.outputFolder()); + } catch (Exception e) { + logger.warn("清理缓存目录失败: {}", cache.outputFolder(), e); + } + } + encryptedPdfCacheMap.clear(); + } + } catch (Exception e) { + logger.error("强制关闭时清理缓存失败", e); + } + + // 关闭执行器 + if (virtualCacheCleanupScheduler != null && !virtualCacheCleanupScheduler.isShutdown()) { + virtualCacheCleanupScheduler.shutdownNow(); + } + + if (virtualThreadExecutor != null && !virtualThreadExecutor.isShutdown()) { + virtualThreadExecutor.shutdownNow(); + } + + runningTasks.clear(); + } + + /** + * 清理所有内存缓存 + */ + private void clearAllMemoryCaches() { + try { + int cacheCount = encryptedPdfCacheMap.size(); + if (cacheCount > 0) { + // 检查执行器是否可用 + if (virtualThreadExecutor == null || virtualThreadExecutor.isShutdown() || virtualThreadExecutor.isTerminated()) { + logger.warn("执行器已关闭,同步清理缓存目录"); + for (EncryptedPdfCache cache : encryptedPdfCacheMap.values()) { + deleteCacheFolder(cache.outputFolder()); + } + encryptedPdfCacheMap.clear(); + logger.info("同步清理了 {} 个内存缓存", cacheCount); + } else { + // 并行清理所有缓存目录 + List> cleanupFutures = new ArrayList<>(); + for (EncryptedPdfCache cache : encryptedPdfCacheMap.values()) { + cleanupFutures.add(CompletableFuture.runAsync(() -> + deleteCacheFolder(cache.outputFolder()), virtualThreadExecutor)); + } + + CompletableFuture.allOf(cleanupFutures.toArray(new CompletableFuture[0])) + .get(60, TimeUnit.SECONDS); + + encryptedPdfCacheMap.clear(); + logger.info("清理了 {} 个内存缓存", cacheCount); + } + } + } catch (Exception e) { + logger.error("清理内存缓存时发生异常", e); + // 即使出错,也要确保清理缓存条目 + encryptedPdfCacheMap.clear(); + } + } + + /** + * 调度缓存清理 + */ + private void scheduleCacheCleanup() { + // 每5分钟执行一次缓存清理 + virtualCacheCleanupScheduler.scheduleAtFixedRate(() -> { + try { + cleanupExpiredEncryptedCache(); + monitorCacheStatistics(); + } catch (Exception e) { + logger.error("缓存清理任务执行失败", e); + } + }, 1, 5, TimeUnit.MINUTES); // 首次延迟1分钟,然后每5分钟执行一次 + + logger.info("缓存清理任务已启动(每5分钟执行一次)"); + } + + /** + * 启动性能监控 + */ + private void startPerformanceMonitoring() { + // 每10分钟记录一次性能统计 + virtualCacheCleanupScheduler.scheduleAtFixedRate(() -> { + try { + logPerformanceStatistics(); + } catch (Exception e) { + logger.error("性能监控任务执行失败", e); + } + }, 5, 10, TimeUnit.MINUTES); + } + + /** + * 记录性能统计信息 + */ + private void logPerformanceStatistics() { + try { + // 收集性能指标 + int runningTasksCount = runningTasks.size(); + int activeTasks = activeTaskCount.get(); + int totalCompleted = totalCompletedTasks.get(); + int cacheSize = encryptedPdfCacheMap.size(); + + // 计算内存使用 + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + + logger.info( + "PDF转换服务性能统计 - 运行中任务数: {}, 活跃任务数: {}, 累计完成任务数: {}, " + + "内存缓存数量: {}, 内存使用: {} MB / {} MB ({}%), 虚拟线程: {}", + runningTasksCount, activeTasks, totalCompleted, cacheSize, + usedMemory / 1024 / 1024, totalMemory / 1024 / 1024, + (usedMemory * 100 / totalMemory), + Thread.currentThread().isVirtual() ? "是" : "否" + ); + + } catch (Exception e) { + logger.error("记录性能统计时发生异常", e); + } + } + + /** + * 监控缓存统计信息 + */ + private void monitorCacheStatistics() { + try { + int totalCaches = encryptedPdfCacheMap.size(); + if (totalCaches > 0) { + // 统计过期缓存 + long expireTime = 10 * 60 * 1000L; // 10分钟 + int expiredCount = 0; + + for (EncryptedPdfCache cache : encryptedPdfCacheMap.values()) { + if (cache.isExpired(expireTime)) { + expiredCount++; + } + } + + if (expiredCount > 0) { + logger.debug("缓存监控: 总数={}, 已过期={}, 过期比例={}%", + totalCaches, expiredCount, (expiredCount * 100 / totalCaches)); + } + } + } catch (Exception e) { + logger.error("监控缓存统计时发生异常", e); + } + } + + /** + * 优化的缓存清理方法(使用虚拟线程并行处理) + */ + private void cleanupExpiredEncryptedCache() { + long startTime = System.currentTimeMillis(); + + try { + long expireTimeMillis = 10 * 60 * 1000L; // 10分钟过期 + + // 收集过期的键 + List expiredKeys = new ArrayList<>(); + + for (Map.Entry entry : encryptedPdfCacheMap.entrySet()) { + if (entry.getValue().isExpired(expireTimeMillis)) { + expiredKeys.add(entry.getKey()); + } + } + + int cleanedCount = expiredKeys.size(); + + if (cleanedCount > 0) { + logger.info("开始清理 {} 个过期的加密PDF缓存...", cleanedCount); + + // 并行删除文件和清理缓存 + List> deletionFutures = new ArrayList<>(); + + for (String cacheKey : expiredKeys) { + EncryptedPdfCache cache = encryptedPdfCacheMap.get(cacheKey); + if (cache == null) { + continue; + } + + CompletableFuture future = CompletableFuture.runAsync(() -> { + // 原子移除,如果已经被其他线程移除,则返回null + EncryptedPdfCache removed = encryptedPdfCacheMap.remove(cacheKey); + if (removed != null) { + // 删除文件目录 + deleteCacheFolderConcurrent(removed.outputFolder()); + } + }, virtualThreadExecutor); + + deletionFutures.add(future); + } + + // 等待所有删除任务完成 + try { + CompletableFuture.allOf(deletionFutures.toArray(new CompletableFuture[0])) + .get(5, TimeUnit.MINUTES); + } catch (TimeoutException e) { + logger.warn("缓存清理任务执行超时"); + } catch (Exception e) { + logger.error("缓存清理任务失败", e); + } + + long elapsedTime = System.currentTimeMillis() - startTime; + logger.info("清理了 {} 个过期的加密PDF缓存,耗时 {}ms", cleanedCount, elapsedTime); + } + + } catch (Exception e) { + logger.error("清理加密PDF缓存时发生异常", e); + } + } + + /** + * 并发安全的目录删除 + */ + private void deleteCacheFolderConcurrent(String folderPath) { + try { + Path path = Paths.get(folderPath); + if (Files.exists(path)) { + // JDK 21: 使用 Files.walk 流式删除 + try (var paths = Files.walk(path)) { + paths.sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + logger.debug("删除文件失败: {}", p, e); + } + }); + } + logger.debug("已删除缓存目录: {}", folderPath); + } + } catch (Exception e) { + logger.error("删除缓存目录失败: {}", folderPath, e); + throw new RuntimeException("删除目录失败: " + folderPath, e); + } + } + + /** + * 目录删除方法 + */ + private void deleteCacheFolder(String folderPath) { + deleteCacheFolderConcurrent(folderPath); + } + + /** + * 添加加密PDF缓存记录 + */ + private void addEncryptedPdfCache(String pdfFilePath, int pageCount, String outputFolder) { + EncryptedPdfCache cache = new EncryptedPdfCache(pageCount, outputFolder); + encryptedPdfCacheMap.put(pdfFilePath, cache); + + if (logger.isDebugEnabled()) { + logger.debug("加密PDF缓存已添加: {}, 页数: {}", pdfFilePath, pageCount); + } + } + + /** + * 获取加密PDF的缓存(如果存在且未过期) + * @param pdfFilePath PDF文件路径(缓存键) + * @return 图片URL列表,如果缓存不存在或过期则返回null + */ + public List getEncryptedPdfCache(String pdfFilePath) { + Integer cachedPageCount = loadEncryptedPdfCache(pdfFilePath); + if (cachedPageCount != null) { + return generateImageUrlsFromCache(pdfFilePath, cachedPageCount); + } + return null; + } + + + /** + * 检查加密PDF缓存是否存在且有效(简版) + * @param outFilePath PDF输出文件路径 + * @return 如果缓存存在返回true,否则返回false + */ + public boolean hasEncryptedPdfCacheSimple(String outFilePath) { + try { + List cache = getEncryptedPdfCache(outFilePath); + return cache != null && !cache.isEmpty(); + } catch (Exception e) { + logger.warn("检查加密PDF缓存失败: {}", outFilePath, e); + return false; + } + } + + /** + * 加载加密PDF缓存 + */ + private Integer loadEncryptedPdfCache(String pdfFilePath) { + EncryptedPdfCache cache = encryptedPdfCacheMap.get(pdfFilePath); + if (cache != null) { + // 检查是否过期(10分钟) + if (cache.isExpired(10 * 60 * 1000L)) { + // 立即移除过期缓存 + encryptedPdfCacheMap.remove(pdfFilePath); + logger.debug("移除过期缓存: {}", pdfFilePath); + return null; + } + + logger.debug("从缓存加载加密PDF: {}, 页数: {}", pdfFilePath, cache.pageCount()); + return cache.pageCount(); + } + return null; + } + + /** + * 从缓存目录生成图片URL列表 + */ + private List generateImageUrlsFromCache(String pdfFilePath, int pageCount) { + List imageUrls = new ArrayList<>(pageCount); + for (int i = 0; i < pageCount; i++) { + String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, i); + imageUrls.add(imageUrl); + } + logger.debug("从加密PDF缓存生成 {} 个图片URL: {}", pageCount, pdfFilePath); + return imageUrls; + } + + /** + * PDF转JPG - 异步版本(虚拟线程优化) + + public CompletableFuture> pdf2jpgAsync(String fileNameFilePath, String pdfFilePath, + String pdfName, FileAttribute fileAttribute) { + CompletableFuture> future = new CompletableFuture<>(); + + // 尝试获取信号量,如果获取不到,则立即拒绝任务 + if (!concurrentTaskSemaphore.tryAcquire()) { + future.completeExceptionally(new RejectedExecutionException("系统繁忙,请稍后再试")); + return future; + } + + // JDK 21: 使用虚拟线程提交任务 + Future taskFuture = virtualThreadExecutor.submit(() -> { + try { + List result = pdf2jpg(fileNameFilePath, pdfFilePath, pdfName, fileAttribute); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } finally { + // 必须在finally中释放信号量 + concurrentTaskSemaphore.release(); + runningTasks.remove(pdfName); + } + }); + + // 记录正在运行的任务 + runningTasks.put(pdfName, taskFuture); + + // 设置超时检查(使用虚拟线程) + scheduleVirtualTimeoutCheck(pdfName, future, taskFuture); + + return future; + } + */ + /** + * 虚拟线程超时检查 + */ + private void scheduleVirtualTimeoutCheck(String fileName, CompletableFuture> taskFuture, + Future future) { + CompletableFuture.runAsync(() -> { + try { + int timeout = calculateTimeoutByFileName(); + taskFuture.get(timeout, TimeUnit.SECONDS); + } catch (TimeoutException e) { + handleConversionTimeout(fileName, taskFuture, future); + } catch (Exception e) { + // 正常完成或异常 + } + }, virtualThreadExecutor); + } + + /** + * 处理转换超时 + */ + private void handleConversionTimeout(String fileName, CompletableFuture> taskFuture, + Future future) { + logger.error("PDF转换超时: {}", fileName); + + // 取消正在运行的任务 + if (future != null && !future.isDone()) { + boolean cancelled = future.cancel(true); + logger.info("尝试取消PDF转换任务 {}: {}", fileName, cancelled ? "成功" : "失败"); + } + + // 从运行任务列表中移除 + runningTasks.remove(fileName); + + // 完成Future + taskFuture.completeExceptionally(new TimeoutException("PDF转换超时: " + fileName)); + } + /** * PDF转JPG - 高性能主方法 */ @@ -88,15 +608,34 @@ public class PdfToJpgService { boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); boolean usePasswordCache = fileAttribute.getUsePasswordCache(); String filePassword = fileAttribute.getFilePassword(); - - // 检查缓存 + boolean semaphoreAcquired = false; + // 添加信号量控制 + try { + if (!concurrentTaskSemaphore.tryAcquire()) { + throw new RejectedExecutionException("系统繁忙,请稍后再试"); + } + semaphoreAcquired = true; + // 检查缓存 - 区分加密和非加密文件 if (!forceUpdatedCache) { - List cacheResult = fileHandlerService.loadPdf2jpgCache(pdfFilePath); - if (!CollectionUtils.isEmpty(cacheResult)) { - return cacheResult; + if (ObjectUtils.isEmpty(filePassword) || usePasswordCache) { + // 非加密文件:使用原有的缓存机制 + List cacheResult = fileHandlerService.loadPdf2jpgCache(pdfFilePath); + if (!CollectionUtils.isEmpty(cacheResult)) { + return cacheResult; + } + } else { + // 加密文件:使用内存缓存(10分钟有效期) + Integer cachedPageCount = loadEncryptedPdfCache(pdfFilePath); + if (cachedPageCount != null) { + // 从缓存目录加载图片URL + return generateImageUrlsFromCache(pdfFilePath, cachedPageCount); + } } } + ReentrantLock fileLock = fileLocks.computeIfAbsent(pdfFilePath, k -> new ReentrantLock()); + fileLock.lock(); + try { // 验证文件存在 File pdfFile = new File(fileNameFilePath); if (!pdfFile.exists()) { @@ -122,24 +661,26 @@ public class PdfToJpgService { throw new Exception("PDF文件加载失败", e); } - // 检查线程池负载 - checkThreadPoolLoad(); - // 根据页数选择最佳转换策略 List imageUrls; long startTime = System.currentTimeMillis(); + // 根据页数选择不同的转换策略 if (pageCount <= PARALLEL_BATCH_THRESHOLD) { - imageUrls = convertOptimizedParallel(pdfFile, filePassword, pdfFilePath, folder, pageCount); + imageUrls = convertOptimizedParallelVirtual(pdfFile, filePassword, pdfFilePath, folder, pageCount); } else { - imageUrls = convertHighPerformance(pdfFile, filePassword, pdfFilePath, folder, pageCount); + imageUrls = convertHighPerformanceVirtual(pdfFile, filePassword, pdfFilePath, folder, pageCount); } long elapsedTime = System.currentTimeMillis() - startTime; - // 缓存结果 - if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) { + // 缓存结果 - 区分加密和非加密文件 + if (ObjectUtils.isEmpty(filePassword) ||usePasswordCache) { + // 非加密文件:使用原有的缓存机制 fileHandlerService.addPdf2jpgCache(pdfFilePath, pageCount); + } else{ + // 加密文件:使用内存缓存(10分钟有效期) + addEncryptedPdfCache(pdfFilePath, pageCount, folder); } // 性能统计 @@ -148,62 +689,57 @@ public class PdfToJpgService { pdfFilePath, activeTaskCount.get()); return imageUrls; - } - - /** - * 检查线程池负载 - */ - private void checkThreadPoolLoad() { - if (threadPoolExecutor instanceof ThreadPoolExecutor pool) { - int activeCount = pool.getActiveCount(); - long taskCount = pool.getTaskCount(); - long completedTaskCount = pool.getCompletedTaskCount(); - int queueSize = pool.getQueue().size(); - - logger.debug("线程池状态: 活动线程={}, 队列大小={}, 总任务={}, 已完成={}", - activeCount, queueSize, taskCount, completedTaskCount); - - if (queueSize > 50) { - logger.warn("PDF转换任务队列堆积,当前队列大小: {}", queueSize); + } finally { + fileLock.unlock(); + // 可选:清理长时间不用的锁 + cleanupStaleFileLock(pdfFilePath, fileLock); + } + } finally { + if (semaphoreAcquired) { + concurrentTaskSemaphore.release(); } } } + private void cleanupStaleFileLock(String pdfFilePath, ReentrantLock lock) { + // 如果锁没有被持有且没有等待线程,则清理 + if (!lock.isLocked() && lock.getQueueLength() == 0) { + fileLocks.remove(pdfFilePath, lock); + } + } + /** - * 高性能并行转换 - 独立加载每个批次(针对100页以上的大文件) + * 高性能并行转换 - 使用虚拟线程 */ - private List convertHighPerformance(File pdfFile, String filePassword, - String pdfFilePath, String folder, int pageCount) { + private List convertHighPerformanceVirtual(File pdfFile, String filePassword, + String pdfFilePath, String folder, int pageCount) { List imageUrls = Collections.synchronizedList(new ArrayList<>(pageCount)); AtomicInteger successCount = new AtomicInteger(0); - AtomicInteger errorCount = new AtomicInteger(0); int batchCount = (pageCount + BATCH_SIZE - 1) / BATCH_SIZE; - long[] totalBatchTime = new long[]{0}; + int dpi = ConfigConstants.getOptimizedDpi(pageCount); - logger.info("使用高性能独立加载并行转换,总页数: {}, 批次数: {}, DPI: {}, 超时: {}秒", - pageCount, batchCount, ConfigConstants.getOptimizedDpi(pageCount), - calculateTimeout(pageCount)); + logger.info("使用虚拟线程高性能并行转换,总页数: {}, 批次数: {}, DPI: {}, 超时: {}秒", + pageCount, batchCount, dpi, calculateTimeoutByPageCount(pageCount)); - List> batchFutures = new ArrayList<>(); + // 使用虚拟线程执行批次任务 + List>> batchFutures = new ArrayList<>(); for (int batchIndex = 0; batchIndex < batchCount; batchIndex++) { - final int currentBatch = batchIndex; final int batchStart = batchIndex * BATCH_SIZE; final int batchEnd = Math.min(batchStart + BATCH_SIZE, pageCount); + final int currentBatch = batchIndex; - CompletableFuture batchFuture = CompletableFuture.runAsync(() -> { + CompletableFuture> batchFuture = CompletableFuture.supplyAsync(() -> { activeTaskCount.incrementAndGet(); - long batchStartTime = System.currentTimeMillis(); + List batchUrls = new ArrayList<>(); + try { + // 每个批次独立加载PDF文档 try (PDDocument batchDoc = Loader.loadPDF(pdfFile, filePassword)) { batchDoc.setResourceCache(new NotResourceCache()); PDFRenderer renderer = new PDFRenderer(batchDoc); renderer.setSubsamplingAllowed(true); - // 直接使用配置的DPI值 - int dpi = ConfigConstants.getOptimizedDpi(pageCount); - - int pagesInBatch = 0; for (int pageIndex = batchStart; pageIndex < batchEnd; pageIndex++) { try { String imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT; @@ -217,97 +753,98 @@ public class PdfToJpgService { image.flush(); String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex); - synchronized (imageUrls) { - imageUrls.add(imageUrl); - } - + batchUrls.add(imageUrl); successCount.incrementAndGet(); - pagesInBatch++; } catch (Exception e) { - errorCount.incrementAndGet(); logger.error("转换页 {} 失败: {}", pageIndex, e.getMessage()); + // 添加占位符URL + String placeholderUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex); + batchUrls.add(placeholderUrl); } } - long batchTime = System.currentTimeMillis() - batchStartTime; - synchronized (this) { - totalBatchTime[0] += batchTime; - } - if (logger.isDebugEnabled()) { - logger.debug("批次{}完成: 转换{}页, 耗时: {}ms", - currentBatch, pagesInBatch, batchTime); + logger.debug("批次{}完成: 转换{}页", currentBatch, batchUrls.size()); } } } catch (Exception e) { logger.error("批次{}处理失败: {}", currentBatch, e.getMessage()); - errorCount.addAndGet(batchEnd - batchStart); + // 为整个批次添加占位符URL + for (int pageIndex = batchStart; pageIndex < batchEnd; pageIndex++) { + batchUrls.add(fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex)); + } } finally { activeTaskCount.decrementAndGet(); totalCompletedTasks.incrementAndGet(); } - }, threadPoolExecutor); + + return batchUrls; + }, virtualThreadExecutor); batchFutures.add(batchFuture); } - // 等待所有批次完成 - int timeout = calculateTimeout(pageCount); - long waitStartTime = System.currentTimeMillis(); - + // 等待所有任务完成 + int timeout = calculateTimeoutByPageCount(pageCount); try { CompletableFuture allBatches = CompletableFuture.allOf( batchFutures.toArray(new CompletableFuture[0]) ); allBatches.get(timeout, TimeUnit.SECONDS); + + // 收集结果 + for (CompletableFuture> future : batchFutures) { + try { + List batchUrls = future.getNow(null); + if (batchUrls != null) { + imageUrls.addAll(batchUrls); + } + } catch (Exception e) { + logger.warn("获取批次结果失败", e); + } + } } catch (TimeoutException e) { - logger.warn("PDF转换超时,已转换页数: {},超时时间: {}秒", successCount.get(), timeout); + logger.warn("PDF转换超时,已转换页数: {},超时时间: {}秒", successCount.get(), + calculateTimeoutByPageCount(pageCount)); } catch (Exception e) { logger.error("批量转换失败", e); } - long waitTime = System.currentTimeMillis() - waitStartTime; - - logger.info("批次转换统计: 总批次={}, 成功={}, 失败={}, DPI={}, 等待耗时={}ms", - batchCount, successCount.get(), errorCount.get(), - ConfigConstants.getOptimizedDpi(pageCount), waitTime); - - // 按页码排序 + logger.info("虚拟线程转换完成: 成功转换 {} 页", successCount.get()); return sortImageUrls(imageUrls); } /** - * 优化并行转换 - 线程安全的批处理模式(针对100页以内的文件) + * 优化并行转换 - 使用虚拟线程(针对100页以内的文件) */ - private List convertOptimizedParallel(File pdfFile, String filePassword, - String pdfFilePath, String folder, int pageCount) { + private List convertOptimizedParallelVirtual(File pdfFile, String filePassword, + String pdfFilePath, String folder, int pageCount) { int dpi = ConfigConstants.getOptimizedDpi(pageCount); - logger.info("使用高性能批处理并行转换,总页数: {}, DPI: {}, 超时: {}秒", - pageCount, dpi, calculateTimeout(pageCount)); + logger.info("使用虚拟线程批处理并行转换,总页数: {}, DPI: {}, 超时: {}秒", + pageCount, dpi, calculateTimeoutByPageCount(pageCount)); - // 按CPU核心数划分批次,优化并行度 - int availableProcessors = Runtime.getRuntime().availableProcessors(); - int optimalBatchSize = Math.max(1, pageCount / availableProcessors); - optimalBatchSize = Math.min(optimalBatchSize, 10); // 每批最多10页 + // 按CPU核心数划分批次 + int optimalBatchSize = Math.max(1, Math.min(pageCount / 4, 10)); // 每批最多10页 - logger.debug("可用处理器: {}, 推荐批次大小: {}", availableProcessors, optimalBatchSize); + logger.debug("推荐批次大小: {}", optimalBatchSize); - List>> batchFutures = new ArrayList<>(); List allImageUrls = Collections.synchronizedList(new ArrayList<>(pageCount)); + AtomicInteger successCount = new AtomicInteger(0); + + // 创建并提交所有批次任务 + List> batchFutures = new ArrayList<>(); - // 分批次并行处理 for (int batchStart = 0; batchStart < pageCount; batchStart += optimalBatchSize) { final int startPage = batchStart; final int endPage = Math.min(batchStart + optimalBatchSize, pageCount); - CompletableFuture> batchFuture = CompletableFuture.supplyAsync(() -> { - List batchImageUrls = new ArrayList<>(endPage - startPage); + CompletableFuture batchFuture = CompletableFuture.runAsync(() -> { activeTaskCount.incrementAndGet(); try { - // 每个批次独立加载PDF,处理一批页面(而不是一页) + // 每个批次独立加载PDF try (PDDocument batchDoc = Loader.loadPDF(pdfFile, filePassword)) { batchDoc.setResourceCache(new NotResourceCache()); PDFRenderer renderer = new PDFRenderer(batchDoc); @@ -326,66 +863,59 @@ public class PdfToJpgService { image.flush(); String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex); - batchImageUrls.add(imageUrl); + synchronized (allImageUrls) { + allImageUrls.add(imageUrl); + } + + successCount.incrementAndGet(); } catch (Exception e) { - logger.error("批次内转换页 {} 失败: {}", pageIndex, e.getMessage()); - // 添加占位符URL + logger.error("转换页 {} 失败: {}", pageIndex, e.getMessage()); + // 添加占位符 String placeholderUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex); - batchImageUrls.add(placeholderUrl); + synchronized (allImageUrls) { + allImageUrls.add(placeholderUrl); + } } } if (logger.isDebugEnabled()) { logger.debug("批次 {}-{} 完成,转换 {} 页", - startPage, endPage - 1, batchImageUrls.size()); + startPage, endPage - 1, (endPage - startPage)); } } } catch (Exception e) { logger.error("批次 {}-{} 加载失败: {}", startPage, endPage - 1, e.getMessage()); - // 为整个批次添加占位符URL + // 为整个批次添加占位符 for (int pageIndex = startPage; pageIndex < endPage; pageIndex++) { - batchImageUrls.add(fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex)); + synchronized (allImageUrls) { + allImageUrls.add(fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex)); + } } } finally { activeTaskCount.decrementAndGet(); totalCompletedTasks.incrementAndGet(); } - - return batchImageUrls; - }, threadPoolExecutor); + }, virtualThreadExecutor); batchFutures.add(batchFuture); } - // 等待所有批次完成并收集结果 - CompletableFuture allBatches = CompletableFuture.allOf( - batchFutures.toArray(new CompletableFuture[0]) - ); - - int timeout = calculateTimeout(pageCount); + // 等待所有批次完成 + int timeout = calculateTimeoutByPageCount(pageCount); try { + CompletableFuture allBatches = CompletableFuture.allOf( + batchFutures.toArray(new CompletableFuture[0]) + ); allBatches.get(timeout, TimeUnit.SECONDS); - - // 收集所有批次的结果 - for (CompletableFuture> future : batchFutures) { - try { - List batchUrls = future.getNow(null); - if (batchUrls != null) { - allImageUrls.addAll(batchUrls); - } - } catch (Exception e) { - // 忽略已完成的任务 - } - } - } catch (TimeoutException e) { - logger.warn("PDF转换超时,已转换页数: {},超时时间: {}秒", allImageUrls.size(), timeout); + logger.warn("优化转换超时,已转换页数: {},超时时间: {}秒", successCount.get(), + calculateTimeoutByPageCount(pageCount)); } catch (Exception e) { - logger.error("批次并行转换失败", e); + logger.error("优化并行转换异常", e); } - // 确保返回正确数量的URL + logger.debug("优化并行转换完成: 成功转换 {} 页", successCount.get()); return sortImageUrls(allImageUrls); } @@ -407,10 +937,9 @@ public class PdfToJpgService { } /** - * 计算超时时间 - 标准化配置,不使用计算 + * 计算超时时间 */ - private int calculateTimeout(int pageCount) { - // 根据页数范围直接返回对应的超时时间配置 + private int calculateTimeoutByPageCount(int pageCount) { if (pageCount <= 50) { return ConfigConstants.getPdfTimeoutSmall(); // 小文件:90秒 } else if (pageCount <= 200) { @@ -422,6 +951,13 @@ public class PdfToJpgService { } } + /** + * 根据文件名计算超时时间 + */ + private int calculateTimeoutByFileName() { + return ConfigConstants.getPdfTimeoutMedium(); + } + /** * 按页码排序 */ @@ -439,4 +975,107 @@ public class PdfToJpgService { return sortedImageUrls; } + /** + * 强制取消指定文件的转换任务 + */ + public boolean cancelConversion(String fileName) { + Future future = runningTasks.get(fileName); + if (future != null) { + boolean cancelled = future.cancel(true); + if (cancelled) { + logger.info("成功取消PDF转换任务: {}", fileName); + runningTasks.remove(fileName); + } + return cancelled; + } + return false; + } + + /** + * 获取正在运行的任务数量 + */ + public int getRunningTaskCount() { + return runningTasks.size(); + } + + /** + * 获取所有正在运行的文件名 + */ + public Set getRunningTasks() { + return new HashSet<>(runningTasks.keySet()); + } + + /** + * 获取缓存统计信息(用于监控) + */ + public Map getCacheStatistics() { + Map stats = new HashMap<>(); + cacheLock.readLock().lock(); + try { + stats.put("cacheSize", encryptedPdfCacheMap.size()); + stats.put("runningTasks", runningTasks.size()); + stats.put("activeTasks", activeTaskCount.get()); + stats.put("totalCompleted", totalCompletedTasks.get()); + + // 计算缓存过期情况 + long expireTime = 10 * 60 * 1000L; + int expiredCount = 0; + for (EncryptedPdfCache cache : encryptedPdfCacheMap.values()) { + if (cache.isExpired(expireTime)) { + expiredCount++; + } + } + stats.put("expiredCaches", expiredCount); + + } finally { + cacheLock.readLock().unlock(); + } + return stats; + } + + /** + * 手动清理所有过期缓存(供管理接口调用) + */ + public int cleanupAllExpiredCaches() { + try { + // 使用虚拟线程执行清理 + Future future = virtualThreadExecutor.submit(() -> { + long expireTime = 10 * 60 * 1000L; + List expiredKeys = new ArrayList<>(); + + cacheLock.readLock().lock(); + try { + for (Map.Entry entry : encryptedPdfCacheMap.entrySet()) { + if (entry.getValue().isExpired(expireTime)) { + expiredKeys.add(entry.getKey()); + } + } + } finally { + cacheLock.readLock().unlock(); + } + + // 清理 + int cleaned = 0; + for (String key : expiredKeys) { + EncryptedPdfCache cache = encryptedPdfCacheMap.remove(key); + if (cache != null) { + try { + deleteCacheFolder(cache.outputFolder()); + cleaned++; + } catch (Exception e) { + logger.warn("清理缓存文件失败: {}", cache.outputFolder(), e); + } + } + } + + return cleaned; + }); + + return future.get(30, TimeUnit.SECONDS); + + } catch (Exception e) { + logger.error("手动清理缓存失败", e); + return 0; + } + } } \ No newline at end of file diff --git a/server/src/main/java/cn/keking/service/TifToPdfService.java b/server/src/main/java/cn/keking/service/TifToPdfService.java index 6ee47a96..7dc15d6c 100644 --- a/server/src/main/java/cn/keking/service/TifToPdfService.java +++ b/server/src/main/java/cn/keking/service/TifToPdfService.java @@ -1,6 +1,7 @@ package cn.keking.service; import cn.keking.config.ConfigConstants; +import cn.keking.utils.FileConvertStatusManager; import cn.keking.utils.WebUtils; import cn.keking.web.filter.BaseUrlFilter; import jakarta.annotation.PostConstruct; @@ -60,14 +61,13 @@ public class TifToPdfService { /** * TIF转JPG - 虚拟线程版本 */ - public List convertTif2Jpg(String strInputFile, String strOutputFile, + public List convertTif2Jpg(String strInputFile, String strOutputFile,String fileName, + String cacheName, boolean forceUpdatedCache) throws Exception { - String fileName = new File(strInputFile).getName(); Instant startTime = Instant.now(); - try { List result = performTifToJpgConversionVirtual( - strInputFile, strOutputFile, forceUpdatedCache + strInputFile, strOutputFile, forceUpdatedCache,fileName,cacheName ); Duration elapsedTime = Duration.between(startTime, Instant.now()); @@ -90,7 +90,8 @@ public class TifToPdfService { * 虚拟线程执行TIF转JPG转换 */ private List performTifToJpgConversionVirtual(String strInputFile, String strOutputFile, - boolean forceUpdatedCache) throws Exception { + boolean forceUpdatedCache,String fileName, + String cacheName) throws Exception { Instant totalStart = Instant.now(); String baseUrl = BaseUrlFilter.getBaseUrl(); @@ -105,7 +106,7 @@ public class TifToPdfService { if (!outputDir.exists() && !outputDir.mkdirs()) { throw new IOException("创建目录失败: " + outputDirPath); } - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30); // 加载所有图片 List images; try { @@ -121,12 +122,12 @@ public class TifToPdfService { logger.warn("TIF文件没有可转换的页面: {}", strInputFile); return Collections.emptyList(); } - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 50); List result = convertPagesVirtualThreads(images, outputDirPath, baseUrl, forceUpdatedCache); Duration totalTime = Duration.between(totalStart, Instant.now()); logger.info("TIF转换PNG完成,总页数: {}, 总耗时: {}ms", pageCount, totalTime.toMillis()); - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 100); return result; } @@ -211,9 +212,9 @@ public class TifToPdfService { /** * TIF转PDF - 虚拟线程版本 */ - public void convertTif2Pdf(String strJpgFile, String strPdfFile, + public void convertTif2Pdf(String strJpgFile, String strPdfFile,String fileName, + String cacheName, boolean forceUpdatedCache) throws Exception { - String fileName = new File(strJpgFile).getName(); Instant startTime = Instant.now(); try { @@ -224,8 +225,8 @@ public class TifToPdfService { logger.info("PDF文件已存在,跳过转换: {}", strPdfFile); return; } - - boolean result = performTifToPdfConversionVirtual(strJpgFile, strPdfFile); + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30); + boolean result = performTifToPdfConversionVirtual(strJpgFile, strPdfFile,fileName,cacheName); Duration elapsedTime = Duration.between(startTime, Instant.now()); logger.info("TIF转PDF{} - 文件: {}, 耗时: {}ms", @@ -244,7 +245,7 @@ public class TifToPdfService { /** * 虚拟线程执行TIF转PDF转换(保持顺序) */ - private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile) throws Exception { + private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile,String fileName,String cacheName) throws Exception { Instant totalStart = Instant.now(); File tiffFile = new File(strJpgFile); @@ -257,14 +258,14 @@ public class TifToPdfService { logger.warn("TIFF文件没有可转换的页面: {}", strJpgFile); return false; } - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30); int pageCount = images.size(); AtomicInteger processedCount = new AtomicInteger(0); AtomicInteger errorCount = new AtomicInteger(0); // 创建页面处理结果的列表 List> futures = new ArrayList<>(pageCount); - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 50); // 为每个页面创建处理任务 for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) { final int currentPageIndex = pageIndex; @@ -286,7 +287,7 @@ public class TifToPdfService { futures.add(future); } - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 70); // 等待所有任务完成 CompletableFuture allFutures = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) @@ -332,7 +333,7 @@ public class TifToPdfService { errorCount.incrementAndGet(); } } - + FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 100); // 保存PDF document.save(strPdfFile); diff --git a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java index 86d4d917..46d9d5fd 100644 --- a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java @@ -55,7 +55,6 @@ public class CadFilePreviewImpl implements FilePreview { // 预览Type,参数传了就取参数的,没传取系统默认 String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType(); - String baseUrl = BaseUrlFilter.getBaseUrl(); boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); String fileName = fileAttribute.getName(); String cadPreviewType = ConfigConstants.getCadPreviewType(); @@ -63,20 +62,9 @@ public class CadFilePreviewImpl implements FilePreview { String outFilePath = fileAttribute.getOutFilePath(); // 查询转换状态 - FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(fileName); - if (status != null) { - if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) { - // 正在转换中,返回等待页面 - model.addAttribute("fileName", fileName); - model.addAttribute("message", status.getRealTimeMessage()); - return WAITING_FILE_PREVIEW_PAGE; - } else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) { - // 超时状态,不允许重新转换 - return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换"); - } else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) { - // 失败状态,不允许重新转换 - return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换"); - } + String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute); + if (statusResult != null) { + return statusResult; } // 判断之前是否已转换过,如果转换过,直接返回,否则执行转换 @@ -117,6 +105,7 @@ public class CadFilePreviewImpl implements FilePreview { CompletableFuture conversionFuture = cadtopdfservice.cadToPdfAsync( filePath, outFilePath, + cacheName, ConfigConstants.getCadPreviewType(), fileAttribute ); @@ -166,11 +155,8 @@ public class CadFilePreviewImpl implements FilePreview { if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) { - return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, - cacheName, outFilePath, fileHandlerService, - OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview); + return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath); } - model.addAttribute("pdfUrl", cacheName); return PDF_FILE_PREVIEW_PAGE; } diff --git a/server/src/main/java/cn/keking/service/impl/CodeFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/CodeFilePreviewImpl.java index 7a9f9eac..92a4b979 100644 --- a/server/src/main/java/cn/keking/service/impl/CodeFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/CodeFilePreviewImpl.java @@ -21,6 +21,11 @@ public class CodeFilePreviewImpl implements FilePreview { @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { filePreviewHandle.filePreviewHandle(url, model, fileAttribute); + String suffix = fileAttribute.getSuffix(); + if(suffix.equalsIgnoreCase("htm") || suffix.equalsIgnoreCase("html") || suffix.equalsIgnoreCase("shtml") ){ + model.addAttribute("pdfUrl", url); + return TXT_FILE_PREVIEW_PAGE; //直接输出html + } return CODE_FILE_PREVIEW_PAGE; } } diff --git a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java index 2591e649..44bb9dc0 100644 --- a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java @@ -8,23 +8,23 @@ import cn.keking.service.FileHandlerService; import cn.keking.service.FilePreview; import cn.keking.service.Mediatomp4Service; import cn.keking.utils.DownloadUtils; +import cn.keking.utils.FileConvertStatusManager; +import cn.keking.utils.KkFileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import java.io.File; -import java.io.IOException; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * @author : kl * @authorboke : kailing.pub * @create : 2018-03-25 上午11:58 - * @description: + * @description: 异步视频文件预览实现 **/ @Service public class MediaFilePreviewImpl implements FilePreview { @@ -32,10 +32,20 @@ public class MediaFilePreviewImpl implements FilePreview { private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class); private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; + private final Mediatomp4Service mediatomp4Service; + private final OfficeFilePreviewImpl officefilepreviewimpl; - public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { + // 用于处理回调的线程池 + private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3); + + public MediaFilePreviewImpl(FileHandlerService fileHandlerService, + OtherFilePreviewImpl otherFilePreview, + Mediatomp4Service mediatomp4Service, + OfficeFilePreviewImpl officefilepreviewimpl) { this.fileHandlerService = fileHandlerService; this.otherFilePreview = otherFilePreview; + this.mediatomp4Service = mediatomp4Service; + this.officefilepreviewimpl = officefilepreviewimpl; } @Override @@ -46,69 +56,61 @@ public class MediaFilePreviewImpl implements FilePreview { String outFilePath = fileAttribute.getOutFilePath(); boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); FileType type = fileAttribute.getType(); - String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式 + + // 检查是否是需要转换的视频格式 boolean mediaTypes = false; + String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; for (String temp : mediaTypesConvert) { if (suffix.equalsIgnoreCase(temp)) { mediaTypes = true; break; } } - + // 查询转换状态 + String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute); + if (statusResult != null) { + return statusResult; + } // 非HTTP协议或需要转换的文件 if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { // 检查缓存 File outputFile = new File(outFilePath); if (outputFile.exists() && !forceUpdatedCache && ConfigConstants.isCacheEnabled()) { String relativePath = fileHandlerService.getRelativePath(outFilePath); - if (fileHandlerService.listConvertedFiles().containsKey(cacheName)) { + if (fileHandlerService.listConvertedMedias().containsKey(cacheName)) { model.addAttribute("mediaUrl", relativePath); - logger.info("使用已缓存的视频文件: {}", cacheName); return MEDIA_FILE_PREVIEW_PAGE; } } - // 下载文件 ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); if (response.isFailure()) { return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); } - String filePath = response.getContent(); - - try { - if (mediaTypes) { - // 检查文件大小限制 - if (isFileSizeExceeded(filePath)) { - return otherFilePreview.notSupportedFile(model, fileAttribute, - "视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制,禁止转换"); - } - - // 使用改进的转换方法 - String convertedPath = convertVideoWithImprovedTimeout(filePath, outFilePath, fileAttribute); - if (convertedPath != null) { - model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(convertedPath)); - - // 缓存转换结果 - if (ConfigConstants.isCacheEnabled()) { - fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(convertedPath)); - } - return MEDIA_FILE_PREVIEW_PAGE; - } else { - return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换失败,请联系管理员"); - } - } else { - // 不需要转换的文件 - model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath)); - return MEDIA_FILE_PREVIEW_PAGE; + if (mediaTypes) { + // 检查文件大小限制 + if (isFileSizeExceeded(filePath)) { + return otherFilePreview.notSupportedFile(model, fileAttribute, + "视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制,禁止转换"); } - } catch (Exception e) { - logger.error("处理媒体文件失败: {}", filePath, e); - return otherFilePreview.notSupportedFile(model, fileAttribute, - "视频处理异常: " + getErrorMessage(e)); + try { + // 启动异步转换,并添加回调处理 + startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute); + // 返回等待页面 + model.addAttribute("fileName", fileName); + model.addAttribute("message", "视频文件正在转换中,请稍候..."); + return WAITING_FILE_PREVIEW_PAGE; + } catch (Exception e) { + logger.error("Failed to start video conversion: {}", filePath, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员"); + } + } else { + // 不需要转换的文件,直接返回 + model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath)); + return MEDIA_FILE_PREVIEW_PAGE; } } - // HTTP协议的媒体文件,直接播放 if (type.equals(FileType.MEDIA)) { model.addAttribute("mediaUrl", url); @@ -118,6 +120,45 @@ public class MediaFilePreviewImpl implements FilePreview { return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览"); } + /** + * 启动异步转换,并在转换完成后处理后续操作 + */ + private void startAsyncConversion(String filePath, String outFilePath, + String cacheName, FileAttribute fileAttribute) { + // 启动异步转换 + CompletableFuture conversionFuture = mediatomp4Service.convertToMp4Async( + filePath, + outFilePath, + cacheName, + fileAttribute + ); + + // 添加转换完成后的回调 + conversionFuture.whenCompleteAsync((success, throwable) -> { + if (success != null && success) { + try { + // 1. 是否保留源文件(只在转换成功后才删除) + if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { + KkFileUtils.deleteFileByPath(filePath); + } + // 2. 加入视频缓存(只在转换成功后才添加) + if (ConfigConstants.isCacheEnabled()) { + fileHandlerService.addConvertedMedias(cacheName, + fileHandlerService.getRelativePath(outFilePath)); + } + } catch (Exception e) { + logger.error("视频转换后续处理失败: {}", filePath, e); + } + } else { + // 转换失败,保留源文件供排查问题 + logger.error("视频转换失败,保留源文件: {}", filePath); + if (throwable != null) { + logger.error("视频转换失败原因: ", throwable); + } + } + }, callbackExecutor); + } + /** * 检查文件大小是否超过限制 */ @@ -139,99 +180,14 @@ public class MediaFilePreviewImpl implements FilePreview { return false; } - /** - * 改进的转换方法 - */ - private String convertVideoWithImprovedTimeout(String filePath, String outFilePath, - FileAttribute fileAttribute) { - try { - // 检查文件是否存在 - File outputFile = new File(outFilePath); - if (outputFile.exists() && !fileAttribute.forceUpdatedCache()) { - logger.info("输出文件已存在且非强制更新模式,跳过转换!"); - return outFilePath; - } - - // 使用改进的异步转换方法 - CompletableFuture future = - Mediatomp4Service.convertToMp4Async(filePath, outFilePath, fileAttribute); - - // 计算超时时间 - File inputFile = new File(filePath); - long fileSizeMB = inputFile.length() / (1024 * 1024); - int timeoutSeconds = calculateTimeout(fileSizeMB); - - try { - boolean result = future.get(timeoutSeconds, TimeUnit.SECONDS); - if (result) { - // 验证输出文件 - File convertedFile = new File(outFilePath); - if (!convertedFile.exists() || convertedFile.length() == 0) { - throw new IOException("转换完成但输出文件无效"); - } - return outFilePath; - } else { - throw new Exception("转换返回失败状态"); - } - } catch (TimeoutException e) { - // 超时后尝试获取任务ID并取消 - logger.error("视频转换超时: {}, 文件大小: {}MB, 超时: {}秒", - filePath, fileSizeMB, timeoutSeconds); - - throw new RuntimeException("视频转换超时,文件可能过大"); - } - - } catch (Exception e) { - logger.error("视频转换异常: {}", filePath, e); - throw new RuntimeException("视频转换失败: " + getErrorMessage(e), e); - } - } - - /** - * 计算超时时间 - 从配置文件读取 - */ - public int calculateTimeout(long fileSizeMB) { - // 如果超时功能被禁用,返回一个非常大的值 - if (!ConfigConstants.isMediaTimeoutEnabled()) { - return Integer.MAX_VALUE; - } - - // 根据文件大小从配置文件读取超时时间 - if (fileSizeMB < 10) return ConfigConstants.getMediaSmallFileTimeout(); // 小文件 - if (fileSizeMB < 50) return ConfigConstants.getMediaMediumFileTimeout(); // 中等文件 - if (fileSizeMB < 200) return ConfigConstants.getMediaLargeFileTimeout(); // 较大文件 - if (fileSizeMB < 500) return ConfigConstants.getMediaXLFileTimeout(); // 大文件 - if (fileSizeMB < 1024) return ConfigConstants.getMediaXXLFileTimeout(); // 超大文件 - return ConfigConstants.getMediaXXXLFileTimeout(); // 极大文件 - } - /** * 检查是否需要转换 */ private boolean checkNeedConvert(boolean mediaTypes) { - // 1.检查开关是否开启 + // 检查转换开关是否开启 if ("true".equals(ConfigConstants.getMediaConvertDisable())) { return mediaTypes; } return false; } - - /** - * 获取友好的错误信息 - */ - private String getErrorMessage(Exception e) { - if (e instanceof CancellationException) { - return "转换被取消"; - } else if (e instanceof TimeoutException) { - return "转换超时"; - } else if (e.getMessage() != null) { - // 截取主要错误信息 - String msg = e.getMessage(); - if (msg.length() > 100) { - msg = msg.substring(0, 100) + "..."; - } - return msg; - } - return "未知错误"; - } } \ No newline at end of file diff --git a/server/src/main/java/cn/keking/service/impl/OfficeFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/OfficeFilePreviewImpl.java index 1f78b10d..77c6ed7d 100644 --- a/server/src/main/java/cn/keking/service/impl/OfficeFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/OfficeFilePreviewImpl.java @@ -8,19 +8,23 @@ import cn.keking.service.FilePreview; import cn.keking.service.OfficeToPdfService; import cn.keking.service.PdfToJpgService; import cn.keking.utils.DownloadUtils; +import cn.keking.utils.FileConvertStatusManager; import cn.keking.utils.KkFileUtils; import cn.keking.utils.OfficeUtils; import cn.keking.utils.WebUtils; import cn.keking.web.filter.BaseUrlFilter; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.poi.EncryptedDocumentException; import org.jodconverter.core.office.OfficeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import java.io.IOException; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Created by kl on 2018/1/17. @@ -29,9 +33,12 @@ import java.util.List; @Service public class OfficeFilePreviewImpl implements FilePreview { + private static final Logger logger = LoggerFactory.getLogger(OfficeFilePreviewImpl.class); public static final String OFFICE_PREVIEW_TYPE_IMAGE = "image"; public static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages"; - private static final String OFFICE_PASSWORD_MSG = "password"; + + // 用于处理回调的线程池 + private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3); private final FileHandlerService fileHandlerService; private final OfficeToPdfService officeToPdfService; @@ -54,12 +61,16 @@ public class OfficeFilePreviewImpl implements FilePreview { String suffix = fileAttribute.getSuffix(); //获取文件后缀 String fileName = fileAttribute.getName(); //获取文件原始名称 String filePassword = fileAttribute.getFilePassword(); //获取密码 - boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 + boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html String cacheName = fileAttribute.getCacheName(); //转换后的文件名 String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径 + + // 查询转换状态 + checkAndHandleConvertStatus(model, fileName, cacheName,fileAttribute); + if (!officePreviewType.equalsIgnoreCase("html")) { - if (ConfigConstants.getOfficeTypeWeb() .equalsIgnoreCase("web")) { + if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) { if (suffix.equalsIgnoreCase("xlsx")) { model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理 return XLSX_FILE_PREVIEW_PAGE; @@ -70,14 +81,190 @@ public class OfficeFilePreviewImpl implements FilePreview { } } } - if (forceUpdatedCache|| !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { - // 下载远程文件到本地,如果文件在本地已存在不会重复下载 - ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); - if (response.isFailure()) { - return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); + + // 图片预览模式(异步转换) + if (!isHtmlView && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) { + boolean jiami = false; + if (!ObjectUtils.isEmpty(filePassword)) { + jiami = pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath); + } + if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { + if (jiami) { + return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath); + } + // 下载文件 + ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); + if (response.isFailure()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); + } + String filePath = response.getContent(); + + // 检查是否加密文件 + boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); + if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) { + // 加密文件需要密码 + model.addAttribute("needFilePassword", true); + model.addAttribute("fileName", fileName); + model.addAttribute("cacheName", cacheName); + return EXEL_FILE_PREVIEW_PAGE; + } + + try { + // 启动异步转换 + startAsyncOfficeConversion(filePath, outFilePath, cacheName, fileAttribute, officePreviewType); + // 返回等待页面 + model.addAttribute("fileName", fileName); + model.addAttribute("message", "文件正在转换中,请稍候..."); + return WAITING_FILE_PREVIEW_PAGE; + } catch (Exception e) { + logger.error("Failed to start Office conversion: {}", filePath, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换异常,请联系管理员"); + } + } else { + // 如果已有缓存,直接渲染预览 + return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath); + } } + + // 处理普通Office转PDF预览 + return handleRegularOfficePreview(model, fileAttribute, fileName, forceUpdatedCache, cacheName, outFilePath, + isHtmlView, userToken, filePassword); + } + + /** + * 启动异步Office转换 + */ + private void startAsyncOfficeConversion(String filePath, String outFilePath, String cacheName, + FileAttribute fileAttribute, + String officePreviewType) { + // 启动异步转换 + CompletableFuture> conversionFuture = CompletableFuture.supplyAsync(() -> { + try { + // 更新状态 + FileConvertStatusManager.startConvert(cacheName); + FileConvertStatusManager.updateProgress(cacheName, "正在启动Office转换", 20); + + // 转换Office到PDF + FileConvertStatusManager.updateProgress(cacheName, "正在转换Office到jpg", 60); + officeToPdfService.openOfficeToPDF(filePath, outFilePath, fileAttribute); + + + if (fileAttribute.isHtmlView()) { + // 对转换后的文件进行操作(改变编码方式) + FileConvertStatusManager.updateProgress(cacheName, "处理HTML编码", 95); + fileHandlerService.doActionConvertedFile(outFilePath); + } + + // 是否需要转换为图片 + List imageUrls = null; + if (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || + OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) { + FileConvertStatusManager.updateProgress(cacheName, "正在转换PDF为图片", 90); + imageUrls = pdftojpgservice.pdf2jpg(outFilePath, outFilePath, cacheName, fileAttribute); + } + + // 缓存处理 + boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); + boolean userToken = fileAttribute.getUsePasswordCache(); + String filePassword = fileAttribute.getFilePassword(); + + if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || userToken || !isPwdProtectedOffice)) { + fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); + } + + FileConvertStatusManager.updateProgress(cacheName, "转换完成", 100); + FileConvertStatusManager.convertSuccess(cacheName); + + return imageUrls; + } catch (OfficeException e) { + boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); + String filePassword = fileAttribute.getFilePassword(); + if (isPwdProtectedOffice && !OfficeUtils.isCompatible(filePath, filePassword)) { + FileConvertStatusManager.markError(cacheName, "文件密码错误,请重新输入"); + } else { + logger.error("Office转换执行失败: {}", cacheName, e); + FileConvertStatusManager.markError(cacheName, "Office转换失败: " + e.getMessage()); + } + return null; + } catch (Exception e) { + logger.error("Office转换执行失败: {}", cacheName, e); + + // 检查是否已经标记为超时 + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); + if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) { + FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage()); + } + return null; + } + }); + // 添加转换完成后的回调 + conversionFuture.thenAcceptAsync(imageUrls -> { + try { + // 这里假设imageUrls不为null且不为空表示转换成功 + if (imageUrls != null && !imageUrls.isEmpty()) { + // 是否保留源文件(只在转换成功后才删除) + if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { + KkFileUtils.deleteFileByPath(filePath); + } + } + } catch (Exception e) { + logger.error("Office转换后续处理失败: {}", filePath, e); + } + }, callbackExecutor); + } + + /** + * 获取预览类型(图片预览) + */ + String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, + String cacheName, String outFilePath) { + String suffix = fileAttribute.getSuffix(); + boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx"); + List imageUrls; + + try { + if (pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath)) { + imageUrls = pdftojpgservice.getEncryptedPdfCache(outFilePath); + } else { + imageUrls = fileHandlerService.loadPdf2jpgCache(outFilePath); + } + + if (imageUrls == null || imageUrls.isEmpty()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, "Office转换缓存异常,请联系管理员"); + } + + model.addAttribute("imgUrls", imageUrls); + model.addAttribute("currentUrl", imageUrls.getFirst()); + + if (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) { + // PPT 图片模式使用专用预览页面 + return (isPPT ? PPT_FILE_PREVIEW_PAGE : OFFICE_PICTURE_FILE_PREVIEW_PAGE); + } else { + return PICTURE_FILE_PREVIEW_PAGE; + } + } catch (Exception e) { + logger.error("渲染Office预览页面失败: {}", cacheName, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "渲染预览页面异常,请联系管理员"); + } + } + + /** + * 处理普通Office预览(转PDF) + */ + private String handleRegularOfficePreview(Model model, FileAttribute fileAttribute, + String fileName, boolean forceUpdatedCache, String cacheName, + String outFilePath, boolean isHtmlView, boolean userToken, + String filePassword) { + + if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { + // 下载远程文件到本地,如果文件在本地已存在不会重复下载 + ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); + if (response.isFailure()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); + } String filePath = response.getContent(); - boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件 + + boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件 if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) { // 加密文件需要密码 model.addAttribute("needFilePassword", true); @@ -109,43 +296,33 @@ public class OfficeFilePreviewImpl implements FilePreview { } } } + } - } - if (!isHtmlView && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) { - return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview); - } model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); //输出转义文件名 方便url识别 return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE; } - String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, String pdfName, String outFilePath, FileHandlerService fileHandlerService, String officePreviewTypeImage, OtherFilePreviewImpl otherFilePreview) { - String suffix = fileAttribute.getSuffix(); - boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx"); - List imageUrls = null; - try { - imageUrls = pdftojpgservice.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute); - } catch (Exception e) { - Throwable[] throwableArray = ExceptionUtils.getThrowables(e); - for (Throwable throwable : throwableArray) { - if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) { - if (e.getMessage().toLowerCase().contains(OFFICE_PASSWORD_MSG)) { - model.addAttribute("needFilePassword", true); - return EXEL_FILE_PREVIEW_PAGE; - } - } + /** + * 异步方法 + */ + public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute){ + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); + int refreshSchedule = ConfigConstants.getTime(); + if (status != null) { + if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) { + // 正在转换中,返回等待页面 + model.addAttribute("fileName", fileName); + model.addAttribute("time", refreshSchedule); + model.addAttribute("message", status.getRealTimeMessage()); + return WAITING_FILE_PREVIEW_PAGE; + } else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) { + // 超时状态,不允许重新转换 + return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换"); + } else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) { + // 失败状态,不允许重新转换 + return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换"); } } - if (imageUrls == null || imageUrls.size() < 1) { - return otherFilePreview.notSupportedFile(model, fileAttribute, "office转图片异常,请联系管理员"); - } - model.addAttribute("imgUrls", imageUrls); - model.addAttribute("currentUrl", imageUrls.get(0)); - if (officePreviewTypeImage.equals(officePreviewType)) { - // PPT 图片模式使用专用预览页面 - return (isPPT ? PPT_FILE_PREVIEW_PAGE : OFFICE_PICTURE_FILE_PREVIEW_PAGE); - } else { - return PICTURE_FILE_PREVIEW_PAGE; - } + return null; } - -} +} \ No newline at end of file diff --git a/server/src/main/java/cn/keking/service/impl/PdfFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/PdfFilePreviewImpl.java index ca8dd05e..5798da23 100644 --- a/server/src/main/java/cn/keking/service/impl/PdfFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/PdfFilePreviewImpl.java @@ -7,14 +7,23 @@ import cn.keking.service.FileHandlerService; import cn.keking.service.FilePreview; import cn.keking.service.PdfToJpgService; import cn.keking.utils.DownloadUtils; +import cn.keking.utils.FileConvertStatusManager; +import cn.keking.utils.KkFileUtils; import cn.keking.utils.WebUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.poi.EncryptedDocumentException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; +import org.springframework.util.ObjectUtils; +import java.io.File; import java.io.IOException; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Created by kl on 2018/1/17. @@ -23,80 +32,263 @@ import java.util.List; @Service public class PdfFilePreviewImpl implements FilePreview { + private static final Logger logger = LoggerFactory.getLogger(PdfFilePreviewImpl.class); + private static final String PDF_PASSWORD_MSG = "password"; private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; private final PdfToJpgService pdftojpgservice; - private static final String PDF_PASSWORD_MSG = "password"; - public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) { + + // 用于处理回调的线程池 + private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3); + + public PdfFilePreviewImpl(FileHandlerService fileHandlerService, + OtherFilePreviewImpl otherFilePreview, + PdfToJpgService pdftojpgservice) { this.fileHandlerService = fileHandlerService; this.otherFilePreview = otherFilePreview; this.pdftojpgservice = pdftojpgservice; } + @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { String pdfName = fileAttribute.getName(); //获取原始文件名 String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型 - boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 + boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径 - String originFilePath = fileAttribute.getOriginFilePath(); //原始文件路径 - if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) { - //当文件不存在时,就去下载 - if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { + String originFilePath; //原始文件路径 + String cacheName = fileAttribute.getCacheName(); + String filePassword = fileAttribute.getFilePassword(); // 获取密码 + int refreshSchedule = ConfigConstants.getTime(); + // 查询转换状态 + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); + if (status != null) { + if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) { + // 正在转换中,返回等待页面 + model.addAttribute("fileName", pdfName); + model.addAttribute("time", refreshSchedule); + model.addAttribute("message", status.getRealTimeMessage()); + return WAITING_FILE_PREVIEW_PAGE; + } else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) { + // 超时状态,不允许重新转换 + return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换"); + } + } + boolean jiami=false; + if(!ObjectUtils.isEmpty(filePassword)){ + jiami=pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath); + } + if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || + OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) { + + // 判断之前是否已转换过,如果转换过,直接返回,否则执行转换 + if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { + if(jiami){ + return renderPreview(model, cacheName, outFilePath, + officePreviewType, pdfName, fileAttribute); + } + // 当文件不存在时,就去下载 ReturnResponse response = DownloadUtils.downLoad(fileAttribute, pdfName); if (response.isFailure()) { return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); } originFilePath = response.getContent(); - if (ConfigConstants.isCacheEnabled()) { - // 加入缓存 - fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(originFilePath)); + // 检查文件是否需要密码,但不启动转换 + if (filePassword == null || filePassword.trim().isEmpty()) { + // 没有提供密码,先检查文件是否需要密码 + if (checkIfPdfNeedsPassword(originFilePath, cacheName, pdfName)) { + model.addAttribute("needFilePassword", true); + model.addAttribute("fileName", pdfName); + model.addAttribute("cacheName", cacheName); + return EXEL_FILE_PREVIEW_PAGE; + } } + try { + // 启动异步转换 + startAsyncPdfConversion(originFilePath, outFilePath, cacheName, pdfName, fileAttribute); + // 返回等待页面 + model.addAttribute("fileName", pdfName); + model.addAttribute("message", "文件正在转换中,请稍候..."); + return WAITING_FILE_PREVIEW_PAGE; + } catch (Exception e) { + logger.error("Failed to start PDF conversion: {}", originFilePath, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "PDF转换异常,请联系管理员"); + } + } else { + // 如果已有缓存,直接渲染预览 + return renderPreview(model, cacheName, outFilePath, + officePreviewType, pdfName, fileAttribute); } - List imageUrls; - try { - imageUrls = pdftojpgservice.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute); + } else { + // 处理普通PDF预览(非图片转换) + return handleRegularPdfPreview(url, model, fileAttribute, pdfName, forceUpdatedCache, outFilePath); + } + } + + /** + * 检查PDF文件是否需要密码(不进行实际转换) + */ + private boolean checkIfPdfNeedsPassword(String originFilePath, String cacheName, String pdfName) { + try { + // 尝试用空密码加载PDF,检查是否需要密码 + File pdfFile = new File(originFilePath); + if (!pdfFile.exists()) { + return false; + } + + // 使用try-with-resources确保资源释放 + try (org.apache.pdfbox.pdmodel.PDDocument tempDoc = org.apache.pdfbox.Loader.loadPDF(pdfFile, "")) { + // 如果能加载成功,说明不需要密码 + int pageCount = tempDoc.getNumberOfPages(); + logger.info("PDF文件不需要密码,总页数: {},文件: {}", pageCount, originFilePath); + return false; } catch (Exception e) { Throwable[] throwableArray = ExceptionUtils.getThrowables(e); for (Throwable throwable : throwableArray) { if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) { if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) { - model.addAttribute("needFilePassword", true); - return EXEL_FILE_PREVIEW_PAGE; + FileConvertStatusManager.convertSuccess(cacheName); + logger.info("PDF文件需要密码: {}", originFilePath); + return true; } } } - return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常,请联系管理员"); + logger.warn("PDF文件检查异常: {}", e.getMessage()); + return false; } - if (imageUrls == null || imageUrls.size() < 1) { - return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常,请联系管理员"); + } catch (Exception e) { + logger.error("检查PDF密码状态失败: {}", originFilePath, e); + return false; + } + } + + /** + * 启动异步PDF转换 + */ + private void startAsyncPdfConversion(String originFilePath, String outFilePath, + String cacheName, String pdfName, + FileAttribute fileAttribute) { + // 启动异步转换 + CompletableFuture> conversionFuture = CompletableFuture.supplyAsync(() -> { + try { + // 更新状态 + FileConvertStatusManager.startConvert(cacheName); + FileConvertStatusManager.updateProgress(cacheName, "正在启动PDF转换", 10); + + List imageUrls = pdftojpgservice.pdf2jpg(originFilePath, outFilePath, + pdfName, fileAttribute); + + if (imageUrls != null && !imageUrls.isEmpty()) { + boolean usePasswordCache = fileAttribute.getUsePasswordCache(); + String filePassword = fileAttribute.getFilePassword(); + if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || usePasswordCache)) { + fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath)); + } + FileConvertStatusManager.updateProgress(cacheName, "转换完成", 100); + // 短暂延迟后清理状态 + FileConvertStatusManager.convertSuccess(cacheName); + return imageUrls; + } else { + FileConvertStatusManager.markError(cacheName, "PDF转换失败,未生成图片"); + return null; + } + } catch (Exception e) { + Throwable[] throwableArray = ExceptionUtils.getThrowables(e); + for (Throwable throwable : throwableArray) { + if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) { + if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) { + // 标记为需要密码的状态 + return null; + } + } + } + logger.error("PDF转换执行失败: {}", cacheName, e); + + // 检查是否已经标记为超时 + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); + if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) { + FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage()); + } + return null; } + }); + + // 添加转换完成后的回调 + conversionFuture.whenCompleteAsync((imageUrls, throwable) -> { + if (imageUrls != null && !imageUrls.isEmpty()) { + try { + // 是否保留PDF源文件(只在转换成功后才删除) + if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { + KkFileUtils.deleteFileByPath(originFilePath); + } + } catch (Exception e) { + logger.error("PDF转换后续处理失败: {}", originFilePath, e); + } + } else { + // 转换失败,保留源文件供排查问题 + logger.error("PDF转换失败,保留源文件: {}", originFilePath); + if (throwable != null) { + logger.error("转换失败原因: ", throwable); + } + } + }, callbackExecutor); + } + + /** + * 渲染预览页面 + */ + private String renderPreview(Model model, String cacheName, + String outFilePath, String officePreviewType, + String pdfName, FileAttribute fileAttribute) { + try { + List imageUrls; + if(pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath)){ + imageUrls = pdftojpgservice.getEncryptedPdfCache(outFilePath); + }else { + imageUrls = fileHandlerService.loadPdf2jpgCache(outFilePath); + } + if (imageUrls == null || imageUrls.isEmpty()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, "PDF转换缓存异常,请联系管理员"); + } + model.addAttribute("imgUrls", imageUrls); model.addAttribute("currentUrl", imageUrls.get(0)); + if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) { return OFFICE_PICTURE_FILE_PREVIEW_PAGE; } else { return PICTURE_FILE_PREVIEW_PAGE; } - } else { - // 不是http开头,浏览器不能直接访问,需下载到本地 - if (url != null && !url.toLowerCase().startsWith("http")) { - if (!fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { - ReturnResponse response = DownloadUtils.downLoad(fileAttribute, pdfName); - if (response.isFailure()) { - return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); - } - model.addAttribute("pdfUrl", fileHandlerService.getRelativePath(response.getContent())); - if (ConfigConstants.isCacheEnabled()) { - // 加入缓存 - fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath)); - } - } else { - model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName)); + } catch (Exception e) { + logger.error("渲染PDF预览页面失败: {}", cacheName, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "渲染预览页面异常,请联系管理员"); + } + } + + /** + * 处理普通PDF预览(非图片转换) + */ + private String handleRegularPdfPreview(String url, Model model, FileAttribute fileAttribute, + String pdfName, boolean forceUpdatedCache, + String outFilePath) { + // 不是http开头,浏览器不能直接访问,需下载到本地 + if (url != null && !url.toLowerCase().startsWith("http")) { + if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { + ReturnResponse response = DownloadUtils.downLoad(fileAttribute, pdfName); + if (response.isFailure()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); + } + model.addAttribute("pdfUrl", fileHandlerService.getRelativePath(response.getContent())); + if (ConfigConstants.isCacheEnabled()) { + // 加入缓存 + fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath)); } } else { - model.addAttribute("pdfUrl", url); + model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName)); } + } else { + model.addAttribute("pdfUrl", url); } return PDF_FILE_PREVIEW_PAGE; } -} + +} \ No newline at end of file diff --git a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java index 1a55849d..03ced710 100644 --- a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java @@ -77,7 +77,7 @@ public class SimTextFilePreviewImpl implements FilePreview { return null; } if (!file.exists() || file.length() == 0) { - return ""; + return "KK提醒您:文件不存在或者已经被删除了!"; } else { String charset = EncodingDetects.getJavaEncode(filePath); if ("ASCII".equals(charset)) { diff --git a/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java index 66aa5e84..f1fd83a3 100644 --- a/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java @@ -7,12 +7,18 @@ import cn.keking.service.FileHandlerService; import cn.keking.service.FilePreview; import cn.keking.service.TifToPdfService; import cn.keking.utils.DownloadUtils; +import cn.keking.utils.FileConvertStatusManager; import cn.keking.utils.KkFileUtils; import cn.keking.utils.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * tiff 图片文件处理 @@ -22,21 +28,37 @@ import java.util.List; @Service public class TiffFilePreviewImpl implements FilePreview { + private static final Logger logger = LoggerFactory.getLogger(TiffFilePreviewImpl.class); + + // 用于处理回调的线程池 + private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3); + private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; private final TifToPdfService tiftoservice; - public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToPdfService tiftoservice) { + private final OfficeFilePreviewImpl officefilepreviewimpl; + + public TiffFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, TifToPdfService tiftoservice, OfficeFilePreviewImpl officefilepreviewimpl) { this.fileHandlerService = fileHandlerService; this.otherFilePreview = otherFilePreview; this.tiftoservice = tiftoservice; + this.officefilepreviewimpl = officefilepreviewimpl; } + @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { String fileName = fileAttribute.getName(); String tifPreviewType = ConfigConstants.getTifPreviewType(); - String cacheName = fileAttribute.getCacheName(); + String cacheName = fileAttribute.getCacheName(); String outFilePath = fileAttribute.getOutFilePath(); - boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); + boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); + + // 查询转换状态 + String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute); + if (statusResult != null) { + return statusResult; + } + if ("jpg".equalsIgnoreCase(tifPreviewType) || "pdf".equalsIgnoreCase(tifPreviewType)) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); @@ -44,66 +66,111 @@ public class TiffFilePreviewImpl implements FilePreview { return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); } String filePath = response.getContent(); + + try { + // 启动异步转换 + startAsyncTiffConversion(filePath, outFilePath, cacheName, fileName, fileAttribute, tifPreviewType, forceUpdatedCache); + // 返回等待页面 + model.addAttribute("fileName", fileName); + model.addAttribute("message", "文件正在转换中,请稍候..."); + return WAITING_FILE_PREVIEW_PAGE; + } catch (Exception e) { + logger.error("Failed to start TIF conversion: {}", filePath, e); + return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换异常,请联系系统管理员!"); + } + } else { + // 如果已有缓存,直接渲染预览 if ("pdf".equalsIgnoreCase(tifPreviewType)) { - try { - tiftoservice.convertTif2Pdf(filePath, outFilePath,forceUpdatedCache); - } catch (Exception e) { - if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) { - model.addAttribute("imgUrls", url); - model.addAttribute("currentUrl", url); - return PICTURE_FILE_PREVIEW_PAGE; - }else { - return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转pdf异常,请联系系统管理员!" ); - } - } - //是否保留TIFF源文件 - if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { - KkFileUtils.deleteFileByPath(filePath); - } - if (ConfigConstants.isCacheEnabled()) { - // 加入缓存 - fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); - } model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); return PDF_FILE_PREVIEW_PAGE; - }else { - // 将tif转换为jpg,返回转换后的文件路径、文件名的list - List listPic2Jpg; - try { - listPic2Jpg = tiftoservice.convertTif2Jpg(filePath, outFilePath,forceUpdatedCache); - } catch (Exception e) { - if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) { - model.addAttribute("imgUrls", url); - model.addAttribute("currentUrl", url); - return PICTURE_FILE_PREVIEW_PAGE; - }else { - return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转JPG异常,请联系系统管理员!" ); - } + } else if ("jpg".equalsIgnoreCase(tifPreviewType)) { + List imgCache = fileHandlerService.getImgCache(cacheName); + if (imgCache == null || imgCache.isEmpty()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换缓存异常,请联系系统管理员!"); } - //是否保留源文件,转换失败保留源文件,转换成功删除源文件 - if(!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { - KkFileUtils.deleteFileByPath(filePath); - } - if (ConfigConstants.isCacheEnabled()) { - // 加入缓存 - fileHandlerService.putImgCache(cacheName, listPic2Jpg); - fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); - } - model.addAttribute("imgUrls", listPic2Jpg); - model.addAttribute("currentUrl", listPic2Jpg.get(0)); + model.addAttribute("imgUrls", imgCache); + model.addAttribute("currentUrl", imgCache.getFirst()); return PICTURE_FILE_PREVIEW_PAGE; } } - if ("pdf".equalsIgnoreCase(tifPreviewType)) { - model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); - return PDF_FILE_PREVIEW_PAGE; - } - else if ("jpg".equalsIgnoreCase(tifPreviewType)) { - model.addAttribute("imgUrls", fileHandlerService.getImgCache(cacheName)); - model.addAttribute("currentUrl", fileHandlerService.getImgCache(cacheName).get(0)); - return PICTURE_FILE_PREVIEW_PAGE; - } } + + // 处理普通TIF预览(不进行转换) + return handleRegularTiffPreview(url, model, fileAttribute, fileName, forceUpdatedCache, outFilePath); + } + + /** + * 启动异步TIF转换 + */ + private void startAsyncTiffConversion(String filePath, String outFilePath, String cacheName, + String fileName, FileAttribute fileAttribute, + String tifPreviewType, boolean forceUpdatedCache) { + // 启动异步转换 + CompletableFuture conversionFuture = CompletableFuture.supplyAsync(() -> { + try { + // 更新状态 + FileConvertStatusManager.startConvert(cacheName); + FileConvertStatusManager.updateProgress(cacheName, "正在启动TIF转换", 10); + + if ("pdf".equalsIgnoreCase(tifPreviewType)) { + tiftoservice.convertTif2Pdf(filePath, outFilePath,fileName,cacheName, forceUpdatedCache); + + // 转换成功,更新缓存 + if (ConfigConstants.isCacheEnabled()) { + fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); + } + } else { + List listPic2Jpg = tiftoservice.convertTif2Jpg(filePath, outFilePath,fileName,cacheName, forceUpdatedCache); + // 转换成功,更新缓存 + if (ConfigConstants.isCacheEnabled()) { + fileHandlerService.putImgCache(cacheName, listPic2Jpg); + fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); + } + } + FileConvertStatusManager.convertSuccess(cacheName); + return null; + } catch (Exception e) { + // 检查是否为Bad endianness tag异常 + if (e.getMessage() != null && e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)")) { + // 特殊处理:对于这种异常,我们不标记为转换失败,而是记录日志 + logger.warn("TIF文件格式异常(Bad endianness tag),将尝试直接预览: {}", filePath); + FileConvertStatusManager.convertSuccess(cacheName); + return null; + } else { + logger.error("TIF转换执行失败: {}", cacheName, e); + + // 检查是否已经标记为超时 + FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName); + if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) { + FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage()); + } + throw new RuntimeException(e); + } + } + }); + + // 添加转换完成后的回调 + conversionFuture.thenRunAsync(() -> { + try { + // 是否保留源文件(只在转换成功后才删除) + if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { + KkFileUtils.deleteFileByPath(filePath); + } + } catch (Exception e) { + logger.error("TIF转换后续处理失败: {}", filePath, e); + } + }, callbackExecutor).exceptionally(throwable -> { + // 转换失败,记录日志但不删除源文件 + logger.error("TIF转换失败,保留源文件供排查: {}", filePath, throwable); + return null; + }); + } + + /** + * 处理普通TIF预览(不进行转换) + */ + private String handleRegularTiffPreview(String url, Model model, FileAttribute fileAttribute, + String fileName, boolean forceUpdatedCache, String outFilePath) { // 不是http开头,浏览器不能直接访问,需下载到本地 if (url != null && !url.toLowerCase().startsWith("http")) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) { @@ -117,12 +184,11 @@ public class TiffFilePreviewImpl implements FilePreview { fileHandlerService.addConvertedFile(fileName, fileHandlerService.getRelativePath(outFilePath)); } } else { - model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName)); + model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName)); } return TIFF_FILE_PREVIEW_PAGE; } model.addAttribute("currentUrl", url); return TIFF_FILE_PREVIEW_PAGE; } -} - +} \ No newline at end of file diff --git a/server/src/main/java/cn/keking/utils/DownloadUtils.java b/server/src/main/java/cn/keking/utils/DownloadUtils.java index 98ab8723..42055270 100644 --- a/server/src/main/java/cn/keking/utils/DownloadUtils.java +++ b/server/src/main/java/cn/keking/utils/DownloadUtils.java @@ -55,7 +55,7 @@ public class DownloadUtils { String urlStr = null; try { - urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20"); + urlStr = fileAttribute.getUrl(); } catch (Exception e) { logger.error("处理URL异常:", e); } diff --git a/server/src/main/java/cn/keking/utils/KkFileUtils.java b/server/src/main/java/cn/keking/utils/KkFileUtils.java index 3a5ae259..d171ccf5 100644 --- a/server/src/main/java/cn/keking/utils/KkFileUtils.java +++ b/server/src/main/java/cn/keking/utils/KkFileUtils.java @@ -10,6 +10,7 @@ import org.springframework.web.util.HtmlUtils; import java.io.File; import java.net.URL; import java.util.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class KkFileUtils { @@ -236,5 +237,15 @@ public class KkFileUtils { File file = new File(filePath); return file.exists(); } - + /** + * 判断是否是数字 + */ + public static boolean isNumeric(String str){ + Pattern pattern = Pattern.compile("[0-9]*"); + if (ObjectUtils.isEmpty(str)){ + return false; + } + Matcher isNum = pattern.matcher(str); + return isNum.matches(); + } } diff --git a/server/src/main/java/cn/keking/utils/UrlEncoderUtils.java b/server/src/main/java/cn/keking/utils/UrlEncoderUtils.java index 1cfb2bd3..84bfc474 100644 --- a/server/src/main/java/cn/keking/utils/UrlEncoderUtils.java +++ b/server/src/main/java/cn/keking/utils/UrlEncoderUtils.java @@ -4,7 +4,7 @@ import java.util.BitSet; public class UrlEncoderUtils { - private static BitSet dontNeedEncoding; + private static final BitSet dontNeedEncoding; static { dontNeedEncoding = new BitSet(256); @@ -19,7 +19,7 @@ public class UrlEncoderUtils { dontNeedEncoding.set(i); } dontNeedEncoding.set('+'); - /** + /* * 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢?
* 其实问题是一样的,比如遇到123%2B456,它到底是原文即使如此,还是123+456 urlEncode后的呢?
* 在这里,我认为只要符合urlEncode规范的,就当作已经urlEncode过了
@@ -36,13 +36,10 @@ public class UrlEncoderUtils { * 判断str是否urlEncoder.encode过
* 经常遇到这样的情况,拿到一个URL,但是搞不清楚到底要不要encode.
* 不做encode吧,担心出错,做encode吧,又怕重复了
- * - * @param str - * @return */ public static boolean hasUrlEncoded(String str) { - /** + /* * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+'
* 0-9a-zA-Z保留
* '-','_','.','*'保留
@@ -51,7 +48,7 @@ public class UrlEncoderUtils { boolean needEncode = false; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); - if (dontNeedEncoding.get((int) c)) { + if (dontNeedEncoding.get(c)) { continue; } if (c == '%' && (i + 2) < str.length()) { @@ -72,9 +69,6 @@ public class UrlEncoderUtils { /** * 判断c是否是16进制的字符 - * - * @param c - * @return */ private static boolean isDigit16Char(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); diff --git a/server/src/main/java/cn/keking/utils/WebUtils.java b/server/src/main/java/cn/keking/utils/WebUtils.java index e9978d5f..d40a5940 100644 --- a/server/src/main/java/cn/keking/utils/WebUtils.java +++ b/server/src/main/java/cn/keking/utils/WebUtils.java @@ -229,11 +229,7 @@ public class WebUtils { if (fileNameEndIndex < fileNameStartIndex) { return url; } - try { - encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), "UTF-8"); - } catch (UnsupportedEncodingException e) { - return null; - } + encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), StandardCharsets.UTF_8); return url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex); } @@ -471,6 +467,8 @@ public class WebUtils { */ public static void applyBasicAuthHeaders(HttpHeaders headers, FileAttribute fileAttribute) { String url = fileAttribute.getUrl(); + System.out.println(" T555."); + System.out.println(url); // 从配置文件读取User-Agent,如果没有配置则使用默认值 String customUserAgent=ConfigConstants.getUserAgent(); String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 3833032c..3c611a78 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -1,6 +1,7 @@ package cn.keking.web.controller; import cn.keking.config.ConfigConstants; +import cn.keking.model.FileType; import cn.keking.model.ReturnResponse; import cn.keking.utils.CaptchaUtil; import cn.keking.utils.DateUtils; @@ -27,13 +28,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE; import static cn.keking.utils.CaptchaUtil.CAPTCHA_GENERATE_TIME; @@ -54,23 +52,72 @@ public class FileController { public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!"; @PostMapping("/fileUpload") - public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file) { - ReturnResponse checkResult = this.fileUploadCheck(file); + public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file, + @RequestParam(value = "path", defaultValue = "") String path) { + ReturnResponse checkResult = this.fileUploadCheck(file, path); if (checkResult.isFailure()) { return checkResult; } - File outFile = new File(fileDir + demoPath); - if (!outFile.exists() && !outFile.mkdirs()) { - logger.error("创建文件夹【{}】失败,请检查目录权限!", fileDir + demoPath); + + String uploadPath = fileDir + demoPath; + if (!ObjectUtils.isEmpty(path)) { + uploadPath += path + File.separator; } + + File outFile = new File(uploadPath); + if (!outFile.exists() && !outFile.mkdirs()) { + logger.error("创建文件夹【{}】失败,请检查目录权限!", uploadPath); + return ReturnResponse.failure("创建文件夹失败,请检查目录权限!"); + } + String fileName = checkResult.getContent().toString(); - logger.info("上传文件:{}{}{}", fileDir, demoPath, fileName); - try (InputStream in = file.getInputStream(); OutputStream out = Files.newOutputStream(Paths.get(fileDir + demoPath + fileName))) { + logger.info("上传文件:{}{}", uploadPath, fileName); + + try (InputStream in = file.getInputStream(); + OutputStream out = Files.newOutputStream(Paths.get(uploadPath + fileName))) { StreamUtils.copy(in, out); return ReturnResponse.success(null); } catch (IOException e) { logger.error("文件上传失败", e); - return ReturnResponse.failure(); + return ReturnResponse.failure("文件上传失败"); + } + } + + @PostMapping("/createFolder") + public ReturnResponse createFolder(@RequestParam(value = "path", defaultValue = "") String path, + @RequestParam("folderName") String folderName) { + if (ConfigConstants.getFileUploadDisable()) { + return ReturnResponse.failure("文件上传接口已禁用"); + } + try { + // 验证文件夹名称 + if (ObjectUtils.isEmpty(folderName)) { + return ReturnResponse.failure("文件夹名称不能为空"); + } + + if (KkFileUtils.isIllegalFileName(folderName)) { + return ReturnResponse.failure("非法文件夹名称"); + } + String basePath = fileDir + demoPath; + if (!ObjectUtils.isEmpty(path)) { + basePath += path + File.separator; + } + + File newFolder = new File(basePath + folderName); + if (newFolder.exists()) { + return ReturnResponse.failure("文件夹已存在"); + } + + if (newFolder.mkdirs()) { + logger.info("创建文件夹:{}", newFolder.getAbsolutePath()); + return ReturnResponse.success(); + } else { + logger.error("创建文件夹失败:{}", newFolder.getAbsolutePath()); + return ReturnResponse.failure("创建文件夹失败,请检查目录权限"); + } + } catch (Exception e) { + logger.error("创建文件夹异常", e); + return ReturnResponse.failure("创建文件夹失败:" + e.getMessage()); } } @@ -81,15 +128,55 @@ public class FileController { return checkResult; } fileName = checkResult.getContent().toString(); - File file = new File(fileDir + demoPath + fileName); - logger.info("删除文件:{}", file.getAbsolutePath()); - if (file.exists() && !file.delete()) { - String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath()); - logger.error(msg); - return ReturnResponse.failure(msg); + + // 构建完整路径 + String fullPath = fileDir + demoPath + fileName; + File file = new File(fullPath); + + logger.info("删除文件/文件夹:{}", file.getAbsolutePath()); + if (file.exists()) { + if (file.isDirectory()) { + // 删除文件夹及其内容 + if (deleteDirectory(file)) { + WebUtils.removeSessionAttr(request, CAPTCHA_CODE); + return ReturnResponse.success(); + } else { + String msg = String.format("删除文件夹【%s】失败,请检查目录权限!", file.getPath()); + logger.error(msg); + return ReturnResponse.failure(msg); + } + } else { + // 删除文件 + if (file.delete()) { + WebUtils.removeSessionAttr(request, CAPTCHA_CODE); + return ReturnResponse.success(); + } else { + String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath()); + logger.error(msg); + return ReturnResponse.failure(msg); + } + } + } else { + return ReturnResponse.failure("文件或文件夹不存在"); } - WebUtils.removeSessionAttr(request, CAPTCHA_CODE); //删除缓存验证码 - return ReturnResponse.success(); + } + + /** + * 递归删除目录 + */ + private boolean deleteDirectory(File dir) { + if (dir.isDirectory()) { + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + boolean success = deleteDirectory(child); + if (!success) { + return false; + } + } + } + } + return dir.delete(); } /** @@ -124,32 +211,156 @@ public class FileController { outputStream.close(); } - @GetMapping("/listFiles") - public List> getFiles() { - List> list = new ArrayList<>(); - File file = new File(fileDir + demoPath); - if (file.exists()) { - File[] files = Objects.requireNonNull(file.listFiles()); - Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified())); - Arrays.stream(files).forEach(file1 -> { - Map fileName = new HashMap<>(); - fileName.put("fileName", demoDir + "/" + file1.getName()); - list.add(fileName); - }); + @PostMapping("/listFiles") + public Map getFiles(@RequestParam(value = "path", defaultValue = "") String path, + @RequestParam(value = "searchText", defaultValue = "") String searchText, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String sort, + @RequestParam(required = false) String order) { + Map result = new HashMap<>(); + List> fileList = new ArrayList<>(); + + try { + // 构建完整路径 + String basePath = fileDir + demoPath; + if (!ObjectUtils.isEmpty(path)) { + basePath += path + File.separator; + } + + File currentDir = new File(basePath); + if (currentDir.exists() && currentDir.isDirectory()) { + File[] files = currentDir.listFiles(); + if (files != null) { + // 转换为List + List fileObjects = new ArrayList<>(Arrays.asList(files)); + + // 如果搜索文本不为空,进行过滤 + if (!ObjectUtils.isEmpty(searchText)) { + String searchLower = searchText.toLowerCase(); + fileObjects.removeIf(f -> !f.getName().toLowerCase().contains(searchLower)); + } + + // 排序 + Comparator comparator = getFileComparator(sort, order); + if (comparator != null) { + fileObjects.sort(comparator); + } + + int total = fileObjects.size(); + int start = page * size; + int end = Math.min(start + size, total); + + if (start < total) { + for (int i = start; i < end; i++) { + File f = fileObjects.get(i); + Map fileInfo = new HashMap<>(); + + fileInfo.put("name", f.getName()); + fileInfo.put("isDirectory", f.isDirectory()); + fileInfo.put("lastModified", f.lastModified()); + fileInfo.put("size", f.length()); + + // 构建路径信息 + String relativePath = demoDir + "/" + (ObjectUtils.isEmpty(path) ? "" : path + "/") + f.getName(); + fileInfo.put("relativePath", relativePath); + + // 如果是目录,保存完整的相对路径用于导航 + if (f.isDirectory()) { + String fullPath = ObjectUtils.isEmpty(path) ? f.getName() : path + "/" + f.getName(); + fileInfo.put("fullPath", fullPath); + } + + // 获取文件属性 + try { + Path filePath = f.toPath(); + BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); + fileInfo.put("creationTime", attrs.creationTime().toMillis()); + } catch (IOException e) { + logger.warn("获取文件属性失败: {}", f.getName(), e); + } + + fileList.add(fileInfo); + } + } + + result.put("total", total); + result.put("data", fileList); + } + } else { + result.put("total", 0); + result.put("data", Collections.emptyList()); + } + } catch (Exception e) { + logger.error("获取文件列表失败", e); + result.put("total", 0); + result.put("data", Collections.emptyList()); } - return list; + + return result; + } + + /** + * 获取文件比较器 + */ + private Comparator getFileComparator(String sort, String order) { + if (ObjectUtils.isEmpty(sort)) { + // 默认按文件夹优先,然后按名称排序 + return (f1, f2) -> { + if (f1.isDirectory() && !f2.isDirectory()) { + return -1; + } else if (!f1.isDirectory() && f2.isDirectory()) { + return 1; + } else { + return f1.getName().compareToIgnoreCase(f2.getName()); + } + }; + } + + boolean isDesc = "desc".equalsIgnoreCase(order); + + return switch (sort) { + case "name" -> (f1, f2) -> { + int compare = f1.getName().compareToIgnoreCase(f2.getName()); + return isDesc ? -compare : compare; + }; + case "lastModified" -> (f1, f2) -> { + long compare = Long.compare(f1.lastModified(), f2.lastModified()); + return isDesc ? (int) -compare : (int) compare; + }; + case "size" -> (f1, f2) -> { + if (f1.isDirectory() && f2.isDirectory()) { + return 0; + } else if (f1.isDirectory()) { + return isDesc ? 1 : -1; + } else if (f2.isDirectory()) { + return isDesc ? -1 : 1; + } else { + long compare = Long.compare(f1.length(), f2.length()); + return isDesc ? (int) -compare : (int) compare; + } + }; + case "isDirectory" -> (f1, f2) -> { + if (f1.isDirectory() && !f2.isDirectory()) { + return isDesc ? 1 : -1; + } else if (!f1.isDirectory() && f2.isDirectory()) { + return isDesc ? -1 : 1; + } else { + return f1.getName().compareToIgnoreCase(f2.getName()); + } + }; + default -> null; + }; } /** * 上传文件前校验 - * - * @param file 文件 - * @return 校验结果 */ - private ReturnResponse fileUploadCheck(MultipartFile file) { + private ReturnResponse fileUploadCheck(MultipartFile file, String path) { if (ConfigConstants.getFileUploadDisable()) { - return ReturnResponse.failure("文件传接口已禁用"); + return ReturnResponse.failure("文件上传接口已禁用"); } + String fileName = WebUtils.getFileNameFromMultipartFile(file); if (fileName.lastIndexOf(".") == -1) { return ReturnResponse.failure("不允许上传的类型"); @@ -160,47 +371,56 @@ public class FileController { if (KkFileUtils.isIllegalFileName(fileName)) { return ReturnResponse.failure("不允许上传的文件名: " + fileName); } + FileType type = FileType.typeFromFileName(fileName); + if (Objects.equals(type, FileType.OTHER)) { + return ReturnResponse.failure("该文件格式还不支持预览,请联系管理员,添加该格式: " + fileName); + } + // 判断是否存在同名文件 - if (existsFile(fileName)) { + if (existsFile(fileName, path)) { return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传"); } + return ReturnResponse.success(fileName); } - /** * 删除文件前校验 - * - * @param fileName 文件名 - * @return 校验结果 */ private ReturnResponse deleteFileCheck(HttpServletRequest request, String fileName, String password) { if (ObjectUtils.isEmpty(fileName)) { return ReturnResponse.failure("文件名为空,删除失败!"); } try { - fileName = WebUtils.decodeUrl(fileName,"base64"); + fileName = WebUtils.decodeUrl(fileName, "base64"); } catch (Exception ex) { String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName); return ReturnResponse.failure(errorMsg + "删除失败!"); } - assert fileName != null; + + if (ObjectUtils.isEmpty(fileName)) { + return ReturnResponse.failure("文件名为空,删除失败!"); + } if (fileName.contains("/")) { fileName = fileName.substring(fileName.lastIndexOf("/") + 1); } if (KkFileUtils.isIllegalFileName(fileName)) { return ReturnResponse.failure("非法文件名,删除失败!"); } + if (ObjectUtils.isEmpty(password)) { return ReturnResponse.failure("密码 or 验证码为空,删除失败!"); } - String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, CAPTCHA_CODE) : ConfigConstants.getPassword(); + String expectedPassword = ConfigConstants.getDeleteCaptcha() ? + WebUtils.getSessionAttr(request, CAPTCHA_CODE) : + ConfigConstants.getPassword(); if (!password.equalsIgnoreCase(expectedPassword)) { logger.error("删除文件【{}】失败,密码错误!", fileName); return ReturnResponse.failure("删除文件失败,密码错误!"); } + return ReturnResponse.success(fileName); } @@ -220,8 +440,12 @@ public class FileController { return RarUtils.getTree(fileUrl); } - private boolean existsFile(String fileName) { - File file = new File(fileDir + demoPath + fileName); + private boolean existsFile(String fileName, String path) { + String fullPath = fileDir + demoPath; + if (!ObjectUtils.isEmpty(path)) { + fullPath += path + File.separator; + } + File file = new File(fullPath + fileName); return file.exists(); } -} +} \ No newline at end of file diff --git a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java index 6882ba30..c4d99474 100644 --- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java +++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java @@ -77,6 +77,9 @@ public class OnlinePreviewController { public String onlinePreview(@RequestParam String url, @RequestParam(required = false) String key, @RequestParam(required = false) String encryption, + @RequestParam(defaultValue = "false") String highlightall, + @RequestParam(defaultValue = "0") String page, + @RequestParam(defaultValue = "false") String kkagent, Model model, HttpServletRequest req) { // 验证访问权限 @@ -90,7 +93,12 @@ public class OnlinePreviewController { String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url"); return otherFilePreview.notSupportedFile(model, errorMsg); } - FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req); //这里不在进行URL 处理了 + FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req); + + highlightall= KkFileUtils.htmlEscape(highlightall); + model.addAttribute("highlightall", highlightall); + model.addAttribute("page", page); + model.addAttribute("kkagent", kkagent); model.addAttribute("file", fileAttribute); FilePreview filePreview = previewFactory.get(fileAttribute); logger.info("预览文件url:{},previewType:{}", fileUrl, fileAttribute.getType()); @@ -151,8 +159,8 @@ public class OnlinePreviewController { public void getCorsFile(@RequestParam String urlPath, @RequestParam(required = false) String key, HttpServletResponse response, - @RequestParam(required = false) String encryption, - FileAttribute fileAttribute) throws Exception { + HttpServletRequest req, + @RequestParam(required = false) String encryption) throws Exception { // 1. 验证接口是否开启 if (!ConfigConstants.getGetCorsFile()) { @@ -177,6 +185,7 @@ public class OnlinePreviewController { logger.info("读取跨域文件异常,可能存在非法访问,urlPath:{}", urlPath); return; } + FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req); InputStream inputStream = null; logger.info("读取跨域pdf文件url:{}", urlPath); if (!isFtpUrl(url)) { @@ -188,7 +197,6 @@ public class OnlinePreviewController { RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(factory); - RequestCallback requestCallback = request -> { request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute); diff --git a/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java b/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java index 1688d504..ecc60aa9 100644 --- a/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java +++ b/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java @@ -47,6 +47,14 @@ public class AttributeSetFilter implements Filter { request.setAttribute("homePagination", ConfigConstants.getHomePagination()); request.setAttribute("homePageSize", ConfigConstants.getHomePageSize()); request.setAttribute("homeSearch", ConfigConstants.getHomeSearch()); + request.setAttribute("isshowaeskey", ConfigConstants.getisShowaesKey()); + request.setAttribute("isjavascript", ConfigConstants.getisJavaScript()); + request.setAttribute("xlsxallowEdit", ConfigConstants.getxlsxAllowEdit()); + request.setAttribute("xlsxshowtoolbar", ConfigConstants.getxlsxShowtoolbar()); + request.setAttribute("aeskey", ConfigConstants.getaesKey()); + request.setAttribute("isshowkey", ConfigConstants.getisShowKey()); + request.setAttribute("kkkey", ConfigConstants.getKey()); + request.setAttribute("scriptjs", ConfigConstants.getscriptJs()); } /** diff --git a/server/src/main/resources/static/css/loading.gif b/server/src/main/resources/static/css/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..7980d81e83e9ac406496b9d6e57b3a6bff630018 GIT binary patch literal 69010 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 8) ^ (sx & 0xff) ^ 0x63; + SBOX[x] = sx; + INV_SBOX[sx] = x; + + // Compute multiplication + var x2 = d[x]; + var x4 = d[x2]; + var x8 = d[x4]; + + // Compute sub bytes, mix columns tables + var t = (d[sx] * 0x101) ^ (sx * 0x1010100); + SUB_MIX_0[x] = (t << 24) | (t >>> 8); + SUB_MIX_1[x] = (t << 16) | (t >>> 16); + SUB_MIX_2[x] = (t << 8) | (t >>> 24); + SUB_MIX_3[x] = t; + + // Compute inv sub bytes, inv mix columns tables + var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); + INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8); + INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16); + INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24); + INV_SUB_MIX_3[sx] = t; + + // Compute next counter + if (!x) { + x = xi = 1; + } else { + x = x2 ^ d[d[d[x8 ^ x2]]]; + xi ^= d[d[xi]]; + } + } + }()); + + // Precomputed Rcon lookup + var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + + /** + * AES block cipher algorithm. + */ + var AES = C_algo.AES = BlockCipher.extend({ + _doReset: function () { + var t; + + // Skip reset of nRounds has been set before and key did not change + if (this._nRounds && this._keyPriorReset === this._key) { + return; + } + + // Shortcuts + var key = this._keyPriorReset = this._key; + var keyWords = key.words; + var keySize = key.sigBytes / 4; + + // Compute number of rounds + var nRounds = this._nRounds = keySize + 6; + + // Compute number of key schedule rows + var ksRows = (nRounds + 1) * 4; + + // Compute key schedule + var keySchedule = this._keySchedule = []; + for (var ksRow = 0; ksRow < ksRows; ksRow++) { + if (ksRow < keySize) { + keySchedule[ksRow] = keyWords[ksRow]; + } else { + t = keySchedule[ksRow - 1]; + + if (!(ksRow % keySize)) { + // Rot word + t = (t << 8) | (t >>> 24); + + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + + // Mix Rcon + t ^= RCON[(ksRow / keySize) | 0] << 24; + } else if (keySize > 6 && ksRow % keySize == 4) { + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + } + + keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t; + } + } + + // Compute inv key schedule + var invKeySchedule = this._invKeySchedule = []; + for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) { + var ksRow = ksRows - invKsRow; + + if (invKsRow % 4) { + var t = keySchedule[ksRow]; + } else { + var t = keySchedule[ksRow - 4]; + } + + if (invKsRow < 4 || ksRow <= 4) { + invKeySchedule[invKsRow] = t; + } else { + invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^ + INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]]; + } + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX); + }, + + decryptBlock: function (M, offset) { + // Swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + + this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX); + + // Inv swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + }, + + _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) { + // Shortcut + var nRounds = this._nRounds; + + // Get input, add round key + var s0 = M[offset] ^ keySchedule[0]; + var s1 = M[offset + 1] ^ keySchedule[1]; + var s2 = M[offset + 2] ^ keySchedule[2]; + var s3 = M[offset + 3] ^ keySchedule[3]; + + // Key schedule row counter + var ksRow = 4; + + // Rounds + for (var round = 1; round < nRounds; round++) { + // Shift rows, sub bytes, mix columns, add round key + var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++]; + var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++]; + var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++]; + var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++]; + + // Update state + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Shift rows, sub bytes, add round key + var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++]; + var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++]; + var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++]; + var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++]; + + // Set output + M[offset] = t0; + M[offset + 1] = t1; + M[offset + 2] = t2; + M[offset + 3] = t3; + }, + + keySize: 256/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); + */ + C.AES = BlockCipher._createHelper(AES); + }()); + + + return CryptoJS.AES; + +})); \ No newline at end of file diff --git a/server/src/main/resources/static/js/crypto-js.js b/server/src/main/resources/static/js/crypto-js.js new file mode 100644 index 00000000..27f0a620 --- /dev/null +++ b/server/src/main/resources/static/js/crypto-js.js @@ -0,0 +1,6191 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(); + } + else if (typeof define === "function" && define.amd) { + // AMD + define([], factory); + } + else { + // Global (browser) + root.CryptoJS = factory(); + } +}(this, function () { + + /*globals window, global, require*/ + + /** + * CryptoJS core components. + */ + var CryptoJS = CryptoJS || (function (Math, undefined) { + + var crypto; + + // Native crypto from window (Browser) + if (typeof window !== 'undefined' && window.crypto) { + crypto = window.crypto; + } + + // Native crypto in web worker (Browser) + if (typeof self !== 'undefined' && self.crypto) { + crypto = self.crypto; + } + + // Native crypto from worker + if (typeof globalThis !== 'undefined' && globalThis.crypto) { + crypto = globalThis.crypto; + } + + // Native (experimental IE 11) crypto from window (Browser) + if (!crypto && typeof window !== 'undefined' && window.msCrypto) { + crypto = window.msCrypto; + } + + // Native crypto from global (NodeJS) + if (!crypto && typeof global !== 'undefined' && global.crypto) { + crypto = global.crypto; + } + + // Native crypto import via require (NodeJS) + if (!crypto && typeof require === 'function') { + try { + crypto = require('crypto'); + } catch (err) {} + } + + /* + * Cryptographically secure pseudorandom number generator + * + * As Math.random() is cryptographically not safe to use + */ + var cryptoSecureRandomInt = function () { + if (crypto) { + // Use getRandomValues method (Browser) + if (typeof crypto.getRandomValues === 'function') { + try { + return crypto.getRandomValues(new Uint32Array(1))[0]; + } catch (err) {} + } + + // Use randomBytes method (NodeJS) + if (typeof crypto.randomBytes === 'function') { + try { + return crypto.randomBytes(4).readInt32LE(); + } catch (err) {} + } + } + + throw new Error('Native crypto module could not be used to get secure random number.'); + }; + + /* + * Local polyfill of Object.create + + */ + var create = Object.create || (function () { + function F() {} + + return function (obj) { + var subtype; + + F.prototype = obj; + + subtype = new F(); + + F.prototype = null; + + return subtype; + }; + }()); + + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + var subtype = create(this); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else { + // Copy one word at a time + for (var j = 0; j < thatSigBytes; j += 4) { + thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2]; + } + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + + for (var i = 0; i < nBytes; i += 4) { + words.push(cryptoSecureRandomInt()); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + var processedWords; + + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; + }(Math)); + + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var X32WordArray = C_lib.WordArray; + + /** + * x64 namespace. + */ + var C_x64 = C.x64 = {}; + + /** + * A 64-bit word. + */ + var X64Word = C_x64.Word = Base.extend({ + /** + * Initializes a newly created 64-bit word. + * + * @param {number} high The high 32 bits. + * @param {number} low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + init: function (high, low) { + this.high = high; + this.low = low; + } + + /** + * Bitwise NOTs this word. + * + * @return {X64Word} A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + // not: function () { + // var high = ~this.high; + // var low = ~this.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ANDs this word with the passed word. + * + * @param {X64Word} word The x64-Word to AND with this word. + * + * @return {X64Word} A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + // and: function (word) { + // var high = this.high & word.high; + // var low = this.low & word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to OR with this word. + * + * @return {X64Word} A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + // or: function (word) { + // var high = this.high | word.high; + // var low = this.low | word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise XORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to XOR with this word. + * + * @return {X64Word} A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + // xor: function (word) { + // var high = this.high ^ word.high; + // var low = this.low ^ word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the left. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + // shiftL: function (n) { + // if (n < 32) { + // var high = (this.high << n) | (this.low >>> (32 - n)); + // var low = this.low << n; + // } else { + // var high = this.low << (n - 32); + // var low = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the right. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + // shiftR: function (n) { + // if (n < 32) { + // var low = (this.low >>> n) | (this.high << (32 - n)); + // var high = this.high >>> n; + // } else { + // var low = this.high >>> (n - 32); + // var high = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Rotates this word n bits to the left. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + // rotL: function (n) { + // return this.shiftL(n).or(this.shiftR(64 - n)); + // }, + + /** + * Rotates this word n bits to the right. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + // rotR: function (n) { + // return this.shiftR(n).or(this.shiftL(64 - n)); + // }, + + /** + * Adds this word with the passed word. + * + * @param {X64Word} word The x64-Word to add with this word. + * + * @return {X64Word} A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + // add: function (word) { + // var low = (this.low + word.low) | 0; + // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0; + // var high = (this.high + word.high + carry) | 0; + + // return X64Word.create(high, low); + // } + }); + + /** + * An array of 64-bit words. + * + * @property {Array} words The array of CryptoJS.x64.Word objects. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var X64WordArray = C_x64.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 8; + } + }, + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32: function () { + // Shortcuts + var x64Words = this.words; + var x64WordsLength = x64Words.length; + + // Convert + var x32Words = []; + for (var i = 0; i < x64WordsLength; i++) { + var x64Word = x64Words[i]; + x32Words.push(x64Word.high); + x32Words.push(x64Word.low); + } + + return X32WordArray.create(x32Words, this.sigBytes); + }, + + /** + * Creates a copy of this word array. + * + * @return {X64WordArray} The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + + // Clone "words" array + var words = clone.words = this.words.slice(0); + + // Clone each X64Word object + var wordsLength = words.length; + for (var i = 0; i < wordsLength; i++) { + words[i] = words[i].clone(); + } + + return clone; + } + }); + }()); + + + (function () { + // Check if typed arrays are supported + if (typeof ArrayBuffer != 'function') { + return; + } + + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + + // Reference original init + var superInit = WordArray.init; + + // Augment WordArray.init to handle typed arrays + var subInit = WordArray.init = function (typedArray) { + // Convert buffers to uint8 + if (typedArray instanceof ArrayBuffer) { + typedArray = new Uint8Array(typedArray); + } + + // Convert other array views to uint8 + if ( + typedArray instanceof Int8Array || + (typeof Uint8ClampedArray !== "undefined" && typedArray instanceof Uint8ClampedArray) || + typedArray instanceof Int16Array || + typedArray instanceof Uint16Array || + typedArray instanceof Int32Array || + typedArray instanceof Uint32Array || + typedArray instanceof Float32Array || + typedArray instanceof Float64Array + ) { + typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); + } + + // Handle Uint8Array + if (typedArray instanceof Uint8Array) { + // Shortcut + var typedArrayByteLength = typedArray.byteLength; + + // Extract bytes + var words = []; + for (var i = 0; i < typedArrayByteLength; i++) { + words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8); + } + + // Initialize this word array + superInit.call(this, words, typedArrayByteLength); + } else { + // Else call normal init + superInit.apply(this, arguments); + } + }; + + subInit.prototype = WordArray; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * UTF-16 BE encoding strategy. + */ + var Utf16BE = C_enc.Utf16 = C_enc.Utf16BE = { + /** + * Converts a word array to a UTF-16 BE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 BE string. + * + * @static + * + * @example + * + * var utf16String = CryptoJS.enc.Utf16.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = (words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff; + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 BE string to a word array. + * + * @param {string} utf16Str The UTF-16 BE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16.parse(utf16String); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= utf16Str.charCodeAt(i) << (16 - (i % 2) * 16); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + /** + * UTF-16 LE encoding strategy. + */ + C_enc.Utf16LE = { + /** + * Converts a word array to a UTF-16 LE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 LE string. + * + * @static + * + * @example + * + * var utf16Str = CryptoJS.enc.Utf16LE.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = swapEndian((words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff); + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 LE string to a word array. + * + * @param {string} utf16Str The UTF-16 LE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16LE.parse(utf16Str); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= swapEndian(utf16Str.charCodeAt(i) << (16 - (i % 2) * 16)); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + function swapEndian(word) { + return ((word << 8) & 0xff00ff00) | ((word >>> 8) & 0x00ff00ff); + } + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64 encoding strategy. + */ + var Base64 = C_enc.Base64 = { + /** + * Converts a word array to a Base64 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Base64 string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64 string to a word array. + * + * @param {string} base64Str The Base64 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64.parse(base64String); + */ + parse: function (base64Str) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64url encoding strategy. + */ + var Base64url = C_enc.Base64url = { + /** + * Converts a word array to a Base64url string. + * + * @param {WordArray} wordArray The word array. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {string} The Base64url string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64url.stringify(wordArray); + */ + stringify: function (wordArray, urlSafe=true) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = urlSafe ? this._safe_map : this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64url string to a word array. + * + * @param {string} base64Str The Base64url string. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64url.parse(base64String); + */ + parse: function (base64Str, urlSafe=true) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = urlSafe ? this._safe_map : this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + _safe_map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var T = []; + + // Compute constants + (function () { + for (var i = 0; i < 64; i++) { + T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0; + } + }()); + + /** + * MD5 hash algorithm. + */ + var MD5 = C_algo.MD5 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Reusable object + var W = []; + + /** + * SHA-1 hash algorithm. + */ + var SHA1 = C_algo.SHA1 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476, + 0xc3d2e1f0 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + + // Computation + for (var i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (n << 1) | (n >>> 31); + } + + var t = ((a << 5) | (a >>> 27)) + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5a827999; + } else if (i < 40) { + t += (b ^ c ^ d) + 0x6ed9eba1; + } else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; + } else /* if (i < 80) */ { + t += (b ^ c ^ d) - 0x359d3e2a; + } + + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + C.SHA1 = Hasher._createHelper(SHA1); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + C.HmacSHA1 = Hasher._createHmacHelper(SHA1); + }()); + + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Initialization and round constants tables + var H = []; + var K = []; + + // Compute constants + (function () { + function isPrime(n) { + var sqrtN = Math.sqrt(n); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n % factor)) { + return false; + } + } + + return true; + } + + function getFractionalBits(n) { + return ((n - (n | 0)) * 0x100000000) | 0; + } + + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); + + nPrime++; + } + + n++; + } + }()); + + // Reusable object + var W = []; + + /** + * SHA-256 hash algorithm. + */ + var SHA256 = C_algo.SHA256 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init(H.slice(0)); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + var f = H[5]; + var g = H[6]; + var h = H[7]; + + // Computation + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ + ((gamma0x << 14) | (gamma0x >>> 18)) ^ + (gamma0x >>> 3); + + var gamma1x = W[i - 2]; + var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ + ((gamma1x << 13) | (gamma1x >>> 19)) ^ + (gamma1x >>> 10); + + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + + var ch = (e & f) ^ (~e & g); + var maj = (a & b) ^ (a & c) ^ (b & c); + + var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); + var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); + + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + H[5] = (H[5] + f) | 0; + H[6] = (H[6] + g) | 0; + H[7] = (H[7] + h) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA256('message'); + * var hash = CryptoJS.SHA256(wordArray); + */ + C.SHA256 = Hasher._createHelper(SHA256); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA256(message, key); + */ + C.HmacSHA256 = Hasher._createHmacHelper(SHA256); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA256 = C_algo.SHA256; + + /** + * SHA-224 hash algorithm. + */ + var SHA224 = C_algo.SHA224 = SHA256.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, + 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 + ]); + }, + + _doFinalize: function () { + var hash = SHA256._doFinalize.call(this); + + hash.sigBytes -= 4; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA224('message'); + * var hash = CryptoJS.SHA224(wordArray); + */ + C.SHA224 = SHA256._createHelper(SHA224); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA224(message, key); + */ + C.HmacSHA224 = SHA256._createHmacHelper(SHA224); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + + function X64Word_create() { + return X64Word.create.apply(X64Word, arguments); + } + + // Constants + var K = [ + X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd), + X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc), + X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019), + X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118), + X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe), + X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2), + X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1), + X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694), + X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3), + X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65), + X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483), + X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5), + X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210), + X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4), + X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725), + X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70), + X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926), + X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df), + X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8), + X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b), + X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001), + X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30), + X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910), + X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8), + X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53), + X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8), + X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb), + X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3), + X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60), + X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec), + X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9), + X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b), + X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207), + X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178), + X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6), + X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b), + X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493), + X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c), + X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a), + X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817) + ]; + + // Reusable objects + var W = []; + (function () { + for (var i = 0; i < 80; i++) { + W[i] = X64Word_create(); + } + }()); + + /** + * SHA-512 hash algorithm. + */ + var SHA512 = C_algo.SHA512 = Hasher.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b), + new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1), + new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f), + new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179) + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var H = this._hash.words; + + var H0 = H[0]; + var H1 = H[1]; + var H2 = H[2]; + var H3 = H[3]; + var H4 = H[4]; + var H5 = H[5]; + var H6 = H[6]; + var H7 = H[7]; + + var H0h = H0.high; + var H0l = H0.low; + var H1h = H1.high; + var H1l = H1.low; + var H2h = H2.high; + var H2l = H2.low; + var H3h = H3.high; + var H3l = H3.low; + var H4h = H4.high; + var H4l = H4.low; + var H5h = H5.high; + var H5l = H5.low; + var H6h = H6.high; + var H6l = H6.low; + var H7h = H7.high; + var H7l = H7.low; + + // Working variables + var ah = H0h; + var al = H0l; + var bh = H1h; + var bl = H1l; + var ch = H2h; + var cl = H2l; + var dh = H3h; + var dl = H3l; + var eh = H4h; + var el = H4l; + var fh = H5h; + var fl = H5l; + var gh = H6h; + var gl = H6l; + var hh = H7h; + var hl = H7l; + + // Rounds + for (var i = 0; i < 80; i++) { + var Wil; + var Wih; + + // Shortcut + var Wi = W[i]; + + // Extend message + if (i < 16) { + Wih = Wi.high = M[offset + i * 2] | 0; + Wil = Wi.low = M[offset + i * 2 + 1] | 0; + } else { + // Gamma0 + var gamma0x = W[i - 15]; + var gamma0xh = gamma0x.high; + var gamma0xl = gamma0x.low; + var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7); + var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25)); + + // Gamma1 + var gamma1x = W[i - 2]; + var gamma1xh = gamma1x.high; + var gamma1xl = gamma1x.low; + var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6); + var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26)); + + // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16] + var Wi7 = W[i - 7]; + var Wi7h = Wi7.high; + var Wi7l = Wi7.low; + + var Wi16 = W[i - 16]; + var Wi16h = Wi16.high; + var Wi16l = Wi16.low; + + Wil = gamma0l + Wi7l; + Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0); + Wil = Wil + gamma1l; + Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0); + Wil = Wil + Wi16l; + Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0); + + Wi.high = Wih; + Wi.low = Wil; + } + + var chh = (eh & fh) ^ (~eh & gh); + var chl = (el & fl) ^ (~el & gl); + var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); + var majl = (al & bl) ^ (al & cl) ^ (bl & cl); + + var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); + var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); + var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9)); + var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9)); + + // t1 = h + sigma1 + ch + K[i] + W[i] + var Ki = K[i]; + var Kih = Ki.high; + var Kil = Ki.low; + + var t1l = hl + sigma1l; + var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); + var t1l = t1l + chl; + var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); + var t1l = t1l + Kil; + var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0); + var t1l = t1l + Wil; + var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0); + + // t2 = sigma0 + maj + var t2l = sigma0l + majl; + var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); + + // Update working variables + hh = gh; + hl = gl; + gh = fh; + gl = fl; + fh = eh; + fl = el; + el = (dl + t1l) | 0; + eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + dh = ch; + dl = cl; + ch = bh; + cl = bl; + bh = ah; + bl = al; + al = (t1l + t2l) | 0; + ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; + } + + // Intermediate hash value + H0l = H0.low = (H0l + al); + H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0)); + H1l = H1.low = (H1l + bl); + H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0)); + H2l = H2.low = (H2l + cl); + H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0)); + H3l = H3.low = (H3l + dl); + H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0)); + H4l = H4.low = (H4l + el); + H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0)); + H5l = H5.low = (H5l + fl); + H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0)); + H6l = H6.low = (H6l + gl); + H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0)); + H7l = H7.low = (H7l + hl); + H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0)); + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Convert hash to 32-bit word array before returning + var hash = this._hash.toX32(); + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + }, + + blockSize: 1024/32 + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA512('message'); + * var hash = CryptoJS.SHA512(wordArray); + */ + C.SHA512 = Hasher._createHelper(SHA512); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA512(message, key); + */ + C.HmacSHA512 = Hasher._createHmacHelper(SHA512); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + var SHA512 = C_algo.SHA512; + + /** + * SHA-384 hash algorithm. + */ + var SHA384 = C_algo.SHA384 = SHA512.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0xcbbb9d5d, 0xc1059ed8), new X64Word.init(0x629a292a, 0x367cd507), + new X64Word.init(0x9159015a, 0x3070dd17), new X64Word.init(0x152fecd8, 0xf70e5939), + new X64Word.init(0x67332667, 0xffc00b31), new X64Word.init(0x8eb44a87, 0x68581511), + new X64Word.init(0xdb0c2e0d, 0x64f98fa7), new X64Word.init(0x47b5481d, 0xbefa4fa4) + ]); + }, + + _doFinalize: function () { + var hash = SHA512._doFinalize.call(this); + + hash.sigBytes -= 16; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA384('message'); + * var hash = CryptoJS.SHA384(wordArray); + */ + C.SHA384 = SHA512._createHelper(SHA384); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA384(message, key); + */ + C.HmacSHA384 = SHA512._createHmacHelper(SHA384); + }()); + + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var C_algo = C.algo; + + // Constants tables + var RHO_OFFSETS = []; + var PI_INDEXES = []; + var ROUND_CONSTANTS = []; + + // Compute Constants + (function () { + // Compute rho offset constants + var x = 1, y = 0; + for (var t = 0; t < 24; t++) { + RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64; + + var newX = y % 5; + var newY = (2 * x + 3 * y) % 5; + x = newX; + y = newY; + } + + // Compute pi index constants + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5; + } + } + + // Compute round constants + var LFSR = 0x01; + for (var i = 0; i < 24; i++) { + var roundConstantMsw = 0; + var roundConstantLsw = 0; + + for (var j = 0; j < 7; j++) { + if (LFSR & 0x01) { + var bitPosition = (1 << j) - 1; + if (bitPosition < 32) { + roundConstantLsw ^= 1 << bitPosition; + } else /* if (bitPosition >= 32) */ { + roundConstantMsw ^= 1 << (bitPosition - 32); + } + } + + // Compute next LFSR + if (LFSR & 0x80) { + // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1 + LFSR = (LFSR << 1) ^ 0x71; + } else { + LFSR <<= 1; + } + } + + ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw); + } + }()); + + // Reusable objects for temporary values + var T = []; + (function () { + for (var i = 0; i < 25; i++) { + T[i] = X64Word.create(); + } + }()); + + /** + * SHA-3 hash algorithm. + */ + var SHA3 = C_algo.SHA3 = Hasher.extend({ + /** + * Configuration options. + * + * @property {number} outputLength + * The desired number of bits in the output hash. + * Only values permitted are: 224, 256, 384, 512. + * Default: 512 + */ + cfg: Hasher.cfg.extend({ + outputLength: 512 + }), + + _doReset: function () { + var state = this._state = [] + for (var i = 0; i < 25; i++) { + state[i] = new X64Word.init(); + } + + this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32; + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var state = this._state; + var nBlockSizeLanes = this.blockSize / 2; + + // Absorb + for (var i = 0; i < nBlockSizeLanes; i++) { + // Shortcuts + var M2i = M[offset + 2 * i]; + var M2i1 = M[offset + 2 * i + 1]; + + // Swap endian + M2i = ( + (((M2i << 8) | (M2i >>> 24)) & 0x00ff00ff) | + (((M2i << 24) | (M2i >>> 8)) & 0xff00ff00) + ); + M2i1 = ( + (((M2i1 << 8) | (M2i1 >>> 24)) & 0x00ff00ff) | + (((M2i1 << 24) | (M2i1 >>> 8)) & 0xff00ff00) + ); + + // Absorb message into state + var lane = state[i]; + lane.high ^= M2i1; + lane.low ^= M2i; + } + + // Rounds + for (var round = 0; round < 24; round++) { + // Theta + for (var x = 0; x < 5; x++) { + // Mix column lanes + var tMsw = 0, tLsw = 0; + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + tMsw ^= lane.high; + tLsw ^= lane.low; + } + + // Temporary values + var Tx = T[x]; + Tx.high = tMsw; + Tx.low = tLsw; + } + for (var x = 0; x < 5; x++) { + // Shortcuts + var Tx4 = T[(x + 4) % 5]; + var Tx1 = T[(x + 1) % 5]; + var Tx1Msw = Tx1.high; + var Tx1Lsw = Tx1.low; + + // Mix surrounding columns + var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31)); + var tLsw = Tx4.low ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31)); + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + lane.high ^= tMsw; + lane.low ^= tLsw; + } + } + + // Rho Pi + for (var laneIndex = 1; laneIndex < 25; laneIndex++) { + var tMsw; + var tLsw; + + // Shortcuts + var lane = state[laneIndex]; + var laneMsw = lane.high; + var laneLsw = lane.low; + var rhoOffset = RHO_OFFSETS[laneIndex]; + + // Rotate lanes + if (rhoOffset < 32) { + tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset)); + tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset)); + } else /* if (rhoOffset >= 32) */ { + tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset)); + tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset)); + } + + // Transpose lanes + var TPiLane = T[PI_INDEXES[laneIndex]]; + TPiLane.high = tMsw; + TPiLane.low = tLsw; + } + + // Rho pi at x = y = 0 + var T0 = T[0]; + var state0 = state[0]; + T0.high = state0.high; + T0.low = state0.low; + + // Chi + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + // Shortcuts + var laneIndex = x + 5 * y; + var lane = state[laneIndex]; + var TLane = T[laneIndex]; + var Tx1Lane = T[((x + 1) % 5) + 5 * y]; + var Tx2Lane = T[((x + 2) % 5) + 5 * y]; + + // Mix rows + lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high); + lane.low = TLane.low ^ (~Tx1Lane.low & Tx2Lane.low); + } + } + + // Iota + var lane = state[0]; + var roundConstant = ROUND_CONSTANTS[round]; + lane.high ^= roundConstant.high; + lane.low ^= roundConstant.low; + } + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + var blockSizeBits = this.blockSize * 32; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32); + dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var state = this._state; + var outputLengthBytes = this.cfg.outputLength / 8; + var outputLengthLanes = outputLengthBytes / 8; + + // Squeeze + var hashWords = []; + for (var i = 0; i < outputLengthLanes; i++) { + // Shortcuts + var lane = state[i]; + var laneMsw = lane.high; + var laneLsw = lane.low; + + // Swap endian + laneMsw = ( + (((laneMsw << 8) | (laneMsw >>> 24)) & 0x00ff00ff) | + (((laneMsw << 24) | (laneMsw >>> 8)) & 0xff00ff00) + ); + laneLsw = ( + (((laneLsw << 8) | (laneLsw >>> 24)) & 0x00ff00ff) | + (((laneLsw << 24) | (laneLsw >>> 8)) & 0xff00ff00) + ); + + // Squeeze state to retrieve hash + hashWords.push(laneLsw); + hashWords.push(laneMsw); + } + + // Return final computed hash + return new WordArray.init(hashWords, outputLengthBytes); + }, + + clone: function () { + var clone = Hasher.clone.call(this); + + var state = clone._state = this._state.slice(0); + for (var i = 0; i < 25; i++) { + state[i] = state[i].clone(); + } + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA3('message'); + * var hash = CryptoJS.SHA3(wordArray); + */ + C.SHA3 = Hasher._createHelper(SHA3); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA3(message, key); + */ + C.HmacSHA3 = Hasher._createHmacHelper(SHA3); + }(Math)); + + + /** @preserve + (c) 2012 by Cédric Mesnil. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var _zl = WordArray.create([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13]); + var _zr = WordArray.create([ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11]); + var _sl = WordArray.create([ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]); + var _sr = WordArray.create([ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]); + + var _hl = WordArray.create([ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]); + var _hr = WordArray.create([ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]); + + /** + * RIPEMD160 hash algorithm. + */ + var RIPEMD160 = C_algo.RIPEMD160 = Hasher.extend({ + _doReset: function () { + this._hash = WordArray.create([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]); + }, + + _doProcessBlock: function (M, offset) { + + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + // Swap + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + // Shortcut + var H = this._hash.words; + var hl = _hl.words; + var hr = _hr.words; + var zl = _zl.words; + var zr = _zr.words; + var sl = _sl.words; + var sr = _sr.words; + + // Working variables + var al, bl, cl, dl, el; + var ar, br, cr, dr, er; + + ar = al = H[0]; + br = bl = H[1]; + cr = cl = H[2]; + dr = dl = H[3]; + er = el = H[4]; + // Computation + var t; + for (var i = 0; i < 80; i += 1) { + t = (al + M[offset+zl[i]])|0; + if (i<16){ + t += f1(bl,cl,dl) + hl[0]; + } else if (i<32) { + t += f2(bl,cl,dl) + hl[1]; + } else if (i<48) { + t += f3(bl,cl,dl) + hl[2]; + } else if (i<64) { + t += f4(bl,cl,dl) + hl[3]; + } else {// if (i<80) { + t += f5(bl,cl,dl) + hl[4]; + } + t = t|0; + t = rotl(t,sl[i]); + t = (t+el)|0; + al = el; + el = dl; + dl = rotl(cl, 10); + cl = bl; + bl = t; + + t = (ar + M[offset+zr[i]])|0; + if (i<16){ + t += f5(br,cr,dr) + hr[0]; + } else if (i<32) { + t += f4(br,cr,dr) + hr[1]; + } else if (i<48) { + t += f3(br,cr,dr) + hr[2]; + } else if (i<64) { + t += f2(br,cr,dr) + hr[3]; + } else {// if (i<80) { + t += f1(br,cr,dr) + hr[4]; + } + t = t|0; + t = rotl(t,sr[i]) ; + t = (t+er)|0; + ar = er; + er = dr; + dr = rotl(cr, 10); + cr = br; + br = t; + } + // Intermediate hash value + t = (H[1] + cl + dr)|0; + H[1] = (H[2] + dl + er)|0; + H[2] = (H[3] + el + ar)|0; + H[3] = (H[4] + al + br)|0; + H[4] = (H[0] + bl + cr)|0; + H[0] = t; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotal << 8) | (nBitsTotal >>> 24)) & 0x00ff00ff) | + (((nBitsTotal << 24) | (nBitsTotal >>> 8)) & 0xff00ff00) + ); + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 5; i++) { + // Shortcut + var H_i = H[i]; + + // Swap + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + + function f1(x, y, z) { + return ((x) ^ (y) ^ (z)); + + } + + function f2(x, y, z) { + return (((x)&(y)) | ((~x)&(z))); + } + + function f3(x, y, z) { + return (((x) | (~(y))) ^ (z)); + } + + function f4(x, y, z) { + return (((x) & (z)) | ((y)&(~(z)))); + } + + function f5(x, y, z) { + return ((x) ^ ((y) |(~(z)))); + + } + + function rotl(x,n) { + return (x<>>(32-n)); + } + + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.RIPEMD160('message'); + * var hash = CryptoJS.RIPEMD160(wordArray); + */ + C.RIPEMD160 = Hasher._createHelper(RIPEMD160); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacRIPEMD160(message, key); + */ + C.HmacRIPEMD160 = Hasher._createHmacHelper(RIPEMD160); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var C_algo = C.algo; + + /** + * HMAC algorithm. + */ + var HMAC = C_algo.HMAC = Base.extend({ + /** + * Initializes a newly created HMAC. + * + * @param {Hasher} hasher The hash algorithm to use. + * @param {WordArray|string} key The secret key. + * + * @example + * + * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); + */ + init: function (hasher, key) { + // Init hasher + hasher = this._hasher = new hasher.init(); + + // Convert string to WordArray, else assume WordArray already + if (typeof key == 'string') { + key = Utf8.parse(key); + } + + // Shortcuts + var hasherBlockSize = hasher.blockSize; + var hasherBlockSizeBytes = hasherBlockSize * 4; + + // Allow arbitrary length keys + if (key.sigBytes > hasherBlockSizeBytes) { + key = hasher.finalize(key); + } + + // Clamp excess bits + key.clamp(); + + // Clone key for inner and outer pads + var oKey = this._oKey = key.clone(); + var iKey = this._iKey = key.clone(); + + // Shortcuts + var oKeyWords = oKey.words; + var iKeyWords = iKey.words; + + // XOR keys with pad constants + for (var i = 0; i < hasherBlockSize; i++) { + oKeyWords[i] ^= 0x5c5c5c5c; + iKeyWords[i] ^= 0x36363636; + } + oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes; + + // Set initial values + this.reset(); + }, + + /** + * Resets this HMAC to its initial state. + * + * @example + * + * hmacHasher.reset(); + */ + reset: function () { + // Shortcut + var hasher = this._hasher; + + // Reset + hasher.reset(); + hasher.update(this._iKey); + }, + + /** + * Updates this HMAC with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {HMAC} This HMAC instance. + * + * @example + * + * hmacHasher.update('message'); + * hmacHasher.update(wordArray); + */ + update: function (messageUpdate) { + this._hasher.update(messageUpdate); + + // Chainable + return this; + }, + + /** + * Finalizes the HMAC computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The HMAC. + * + * @example + * + * var hmac = hmacHasher.finalize(); + * var hmac = hmacHasher.finalize('message'); + * var hmac = hmacHasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Shortcut + var hasher = this._hasher; + + // Compute HMAC + var innerHash = hasher.finalize(messageUpdate); + hasher.reset(); + var hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); + + return hmac; + } + }); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA1 = C_algo.SHA1; + var HMAC = C_algo.HMAC; + + /** + * Password-Based Key Derivation Function 2 algorithm. + */ + var PBKDF2 = C_algo.PBKDF2 = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hasher to use. Default: SHA1 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: SHA1, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.PBKDF2.create(); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + // Shortcut + var cfg = this.cfg; + + // Init HMAC + var hmac = HMAC.create(cfg.hasher, password); + + // Initial values + var derivedKey = WordArray.create(); + var blockIndex = WordArray.create([0x00000001]); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var blockIndexWords = blockIndex.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + var block = hmac.update(salt).finalize(blockIndex); + hmac.reset(); + + // Shortcuts + var blockWords = block.words; + var blockWordsLength = blockWords.length; + + // Iterations + var intermediate = block; + for (var i = 1; i < iterations; i++) { + intermediate = hmac.finalize(intermediate); + hmac.reset(); + + // Shortcut + var intermediateWords = intermediate.words; + + // XOR intermediate with block + for (var j = 0; j < blockWordsLength; j++) { + blockWords[j] ^= intermediateWords[j]; + } + } + + derivedKey.concat(block); + blockIndexWords[0]++; + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.PBKDF2(password, salt); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 }); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.PBKDF2 = function (password, salt, cfg) { + return PBKDF2.create(cfg).compute(password, salt); + }; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var MD5 = C_algo.MD5; + + /** + * This key derivation function is meant to conform with EVP_BytesToKey. + * www.openssl.org/docs/crypto/EVP_BytesToKey.html + */ + var EvpKDF = C_algo.EvpKDF = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hash algorithm to use. Default: MD5 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: MD5, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.EvpKDF.create(); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + var block; + + // Shortcut + var cfg = this.cfg; + + // Init hasher + var hasher = cfg.hasher.create(); + + // Initial values + var derivedKey = WordArray.create(); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + if (block) { + hasher.update(block); + } + block = hasher.update(password).finalize(salt); + hasher.reset(); + + // Iterations + for (var i = 1; i < iterations; i++) { + block = hasher.finalize(block); + hasher.reset(); + } + + derivedKey.concat(block); + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.EvpKDF(password, salt); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 }); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.EvpKDF = function (password, salt, cfg) { + return EvpKDF.create(cfg).compute(password, salt); + }; + }()); + + + /** + * Cipher core components. + */ + CryptoJS.lib.Cipher || (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var Base64 = C_enc.Base64; + var C_algo = C.algo; + var EvpKDF = C_algo.EvpKDF; + + /** + * Abstract base cipher template. + * + * @property {number} keySize This cipher's key size. Default: 4 (128 bits) + * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits) + * @property {number} _ENC_XFORM_MODE A constant representing encryption mode. + * @property {number} _DEC_XFORM_MODE A constant representing decryption mode. + */ + var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + * + * @property {WordArray} iv The IV to use for this operation. + */ + cfg: Base.extend(), + + /** + * Creates this cipher in encryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray }); + */ + createEncryptor: function (key, cfg) { + return this.create(this._ENC_XFORM_MODE, key, cfg); + }, + + /** + * Creates this cipher in decryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray }); + */ + createDecryptor: function (key, cfg) { + return this.create(this._DEC_XFORM_MODE, key, cfg); + }, + + /** + * Initializes a newly created cipher. + * + * @param {number} xformMode Either the encryption or decryption transormation mode constant. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @example + * + * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray }); + */ + init: function (xformMode, key, cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Store transform mode and key + this._xformMode = xformMode; + this._key = key; + + // Set initial values + this.reset(); + }, + + /** + * Resets this cipher to its initial state. + * + * @example + * + * cipher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-cipher logic + this._doReset(); + }, + + /** + * Adds data to be encrypted or decrypted. + * + * @param {WordArray|string} dataUpdate The data to encrypt or decrypt. + * + * @return {WordArray} The data after processing. + * + * @example + * + * var encrypted = cipher.process('data'); + * var encrypted = cipher.process(wordArray); + */ + process: function (dataUpdate) { + // Append + this._append(dataUpdate); + + // Process available blocks + return this._process(); + }, + + /** + * Finalizes the encryption or decryption process. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt. + * + * @return {WordArray} The data after final processing. + * + * @example + * + * var encrypted = cipher.finalize(); + * var encrypted = cipher.finalize('data'); + * var encrypted = cipher.finalize(wordArray); + */ + finalize: function (dataUpdate) { + // Final data update + if (dataUpdate) { + this._append(dataUpdate); + } + + // Perform concrete-cipher logic + var finalProcessedData = this._doFinalize(); + + return finalProcessedData; + }, + + keySize: 128/32, + + ivSize: 128/32, + + _ENC_XFORM_MODE: 1, + + _DEC_XFORM_MODE: 2, + + /** + * Creates shortcut functions to a cipher's object interface. + * + * @param {Cipher} cipher The cipher to create a helper for. + * + * @return {Object} An object with encrypt and decrypt shortcut functions. + * + * @static + * + * @example + * + * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES); + */ + _createHelper: (function () { + function selectCipherStrategy(key) { + if (typeof key == 'string') { + return PasswordBasedCipher; + } else { + return SerializableCipher; + } + } + + return function (cipher) { + return { + encrypt: function (message, key, cfg) { + return selectCipherStrategy(key).encrypt(cipher, message, key, cfg); + }, + + decrypt: function (ciphertext, key, cfg) { + return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg); + } + }; + }; + }()) + }); + + /** + * Abstract base stream cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits) + */ + var StreamCipher = C_lib.StreamCipher = Cipher.extend({ + _doFinalize: function () { + // Process partial blocks + var finalProcessedBlocks = this._process(!!'flush'); + + return finalProcessedBlocks; + }, + + blockSize: 1 + }); + + /** + * Mode namespace. + */ + var C_mode = C.mode = {}; + + /** + * Abstract base block cipher mode template. + */ + var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({ + /** + * Creates this mode for encryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor: function (cipher, iv) { + return this.Encryptor.create(cipher, iv); + }, + + /** + * Creates this mode for decryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words); + */ + createDecryptor: function (cipher, iv) { + return this.Decryptor.create(cipher, iv); + }, + + /** + * Initializes a newly created mode. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words); + */ + init: function (cipher, iv) { + this._cipher = cipher; + this._iv = iv; + } + }); + + /** + * Cipher Block Chaining mode. + */ + var CBC = C_mode.CBC = (function () { + /** + * Abstract base CBC mode. + */ + var CBC = BlockCipherMode.extend(); + + /** + * CBC encryptor. + */ + CBC.Encryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // XOR and encrypt + xorBlock.call(this, words, offset, blockSize); + cipher.encryptBlock(words, offset); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + /** + * CBC decryptor. + */ + CBC.Decryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + // Decrypt and XOR + cipher.decryptBlock(words, offset); + xorBlock.call(this, words, offset, blockSize); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function xorBlock(words, offset, blockSize) { + var block; + + // Shortcut + var iv = this._iv; + + // Choose mixing block + if (iv) { + block = iv; + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + block = this._prevBlock; + } + + // XOR blocks + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= block[i]; + } + } + + return CBC; + }()); + + /** + * Padding namespace. + */ + var C_pad = C.pad = {}; + + /** + * PKCS #5/7 padding strategy. + */ + var Pkcs7 = C_pad.Pkcs7 = { + /** + * Pads data using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to pad. + * @param {number} blockSize The multiple that the data should be padded to. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.pad(wordArray, 4); + */ + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Create padding word + var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes; + + // Create padding + var paddingWords = []; + for (var i = 0; i < nPaddingBytes; i += 4) { + paddingWords.push(paddingWord); + } + var padding = WordArray.create(paddingWords, nPaddingBytes); + + // Add padding + data.concat(padding); + }, + + /** + * Unpads data that had been padded using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to unpad. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.unpad(wordArray); + */ + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + /** + * Abstract base block cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits) + */ + var BlockCipher = C_lib.BlockCipher = Cipher.extend({ + /** + * Configuration options. + * + * @property {Mode} mode The block mode to use. Default: CBC + * @property {Padding} padding The padding strategy to use. Default: Pkcs7 + */ + cfg: Cipher.cfg.extend({ + mode: CBC, + padding: Pkcs7 + }), + + reset: function () { + var modeCreator; + + // Reset cipher + Cipher.reset.call(this); + + // Shortcuts + var cfg = this.cfg; + var iv = cfg.iv; + var mode = cfg.mode; + + // Reset block mode + if (this._xformMode == this._ENC_XFORM_MODE) { + modeCreator = mode.createEncryptor; + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + modeCreator = mode.createDecryptor; + // Keep at least one block in the buffer for unpadding + this._minBufferSize = 1; + } + + if (this._mode && this._mode.__creator == modeCreator) { + this._mode.init(this, iv && iv.words); + } else { + this._mode = modeCreator.call(mode, this, iv && iv.words); + this._mode.__creator = modeCreator; + } + }, + + _doProcessBlock: function (words, offset) { + this._mode.processBlock(words, offset); + }, + + _doFinalize: function () { + var finalProcessedBlocks; + + // Shortcut + var padding = this.cfg.padding; + + // Finalize + if (this._xformMode == this._ENC_XFORM_MODE) { + // Pad data + padding.pad(this._data, this.blockSize); + + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + + // Unpad data + padding.unpad(finalProcessedBlocks); + } + + return finalProcessedBlocks; + }, + + blockSize: 128/32 + }); + + /** + * A collection of cipher parameters. + * + * @property {WordArray} ciphertext The raw ciphertext. + * @property {WordArray} key The key to this ciphertext. + * @property {WordArray} iv The IV used in the ciphering operation. + * @property {WordArray} salt The salt used with a key derivation function. + * @property {Cipher} algorithm The cipher algorithm. + * @property {Mode} mode The block mode used in the ciphering operation. + * @property {Padding} padding The padding scheme used in the ciphering operation. + * @property {number} blockSize The block size of the cipher. + * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string. + */ + var CipherParams = C_lib.CipherParams = Base.extend({ + /** + * Initializes a newly created cipher params object. + * + * @param {Object} cipherParams An object with any of the possible cipher parameters. + * + * @example + * + * var cipherParams = CryptoJS.lib.CipherParams.create({ + * ciphertext: ciphertextWordArray, + * key: keyWordArray, + * iv: ivWordArray, + * salt: saltWordArray, + * algorithm: CryptoJS.algo.AES, + * mode: CryptoJS.mode.CBC, + * padding: CryptoJS.pad.PKCS7, + * blockSize: 4, + * formatter: CryptoJS.format.OpenSSL + * }); + */ + init: function (cipherParams) { + this.mixIn(cipherParams); + }, + + /** + * Converts this cipher params object to a string. + * + * @param {Format} formatter (Optional) The formatting strategy to use. + * + * @return {string} The stringified cipher params. + * + * @throws Error If neither the formatter nor the default formatter is set. + * + * @example + * + * var string = cipherParams + ''; + * var string = cipherParams.toString(); + * var string = cipherParams.toString(CryptoJS.format.OpenSSL); + */ + toString: function (formatter) { + return (formatter || this.formatter).stringify(this); + } + }); + + /** + * Format namespace. + */ + var C_format = C.format = {}; + + /** + * OpenSSL formatting strategy. + */ + var OpenSSLFormatter = C_format.OpenSSL = { + /** + * Converts a cipher params object to an OpenSSL-compatible string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The OpenSSL-compatible string. + * + * @static + * + * @example + * + * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams); + */ + stringify: function (cipherParams) { + var wordArray; + + // Shortcuts + var ciphertext = cipherParams.ciphertext; + var salt = cipherParams.salt; + + // Format + if (salt) { + wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext); + } else { + wordArray = ciphertext; + } + + return wordArray.toString(Base64); + }, + + /** + * Converts an OpenSSL-compatible string to a cipher params object. + * + * @param {string} openSSLStr The OpenSSL-compatible string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString); + */ + parse: function (openSSLStr) { + var salt; + + // Parse base64 + var ciphertext = Base64.parse(openSSLStr); + + // Shortcut + var ciphertextWords = ciphertext.words; + + // Test for salt + if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) { + // Extract salt + salt = WordArray.create(ciphertextWords.slice(2, 4)); + + // Remove salt from ciphertext + ciphertextWords.splice(0, 4); + ciphertext.sigBytes -= 16; + } + + return CipherParams.create({ ciphertext: ciphertext, salt: salt }); + } + }; + + /** + * A cipher wrapper that returns ciphertext as a serializable cipher params object. + */ + var SerializableCipher = C_lib.SerializableCipher = Base.extend({ + /** + * Configuration options. + * + * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL + */ + cfg: Base.extend({ + format: OpenSSLFormatter + }), + + /** + * Encrypts a message. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv }); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Encrypt + var encryptor = cipher.createEncryptor(key, cfg); + var ciphertext = encryptor.finalize(message); + + // Shortcut + var cipherCfg = encryptor.cfg; + + // Create and return serializable cipher params + return CipherParams.create({ + ciphertext: ciphertext, + key: key, + iv: cipherCfg.iv, + algorithm: cipher, + mode: cipherCfg.mode, + padding: cipherCfg.padding, + blockSize: cipher.blockSize, + formatter: cfg.format + }); + }, + + /** + * Decrypts serialized ciphertext. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Decrypt + var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext); + + return plaintext; + }, + + /** + * Converts serialized ciphertext to CipherParams, + * else assumed CipherParams already and returns ciphertext unchanged. + * + * @param {CipherParams|string} ciphertext The ciphertext. + * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext. + * + * @return {CipherParams} The unserialized ciphertext. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format); + */ + _parse: function (ciphertext, format) { + if (typeof ciphertext == 'string') { + return format.parse(ciphertext, this); + } else { + return ciphertext; + } + } + }); + + /** + * Key derivation function namespace. + */ + var C_kdf = C.kdf = {}; + + /** + * OpenSSL key derivation function. + */ + var OpenSSLKdf = C_kdf.OpenSSL = { + /** + * Derives a key and IV from a password. + * + * @param {string} password The password to derive from. + * @param {number} keySize The size in words of the key to generate. + * @param {number} ivSize The size in words of the IV to generate. + * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly. + * + * @return {CipherParams} A cipher params object with the key, IV, and salt. + * + * @static + * + * @example + * + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32); + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt'); + */ + execute: function (password, keySize, ivSize, salt) { + // Generate random salt + if (!salt) { + salt = WordArray.random(64/8); + } + + // Derive key and IV + var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt); + + // Separate key and IV + var iv = WordArray.create(key.words.slice(keySize), ivSize * 4); + key.sigBytes = keySize * 4; + + // Return params + return CipherParams.create({ key: key, iv: iv, salt: salt }); + } + }; + + /** + * A serializable cipher wrapper that derives the key from a password, + * and returns ciphertext as a serializable cipher params object. + */ + var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({ + /** + * Configuration options. + * + * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL + */ + cfg: SerializableCipher.cfg.extend({ + kdf: OpenSSLKdf + }), + + /** + * Encrypts a message using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password'); + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Encrypt + var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg); + + // Mix in derived params + ciphertext.mixIn(derivedParams); + + return ciphertext; + }, + + /** + * Decrypts serialized ciphertext using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Decrypt + var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg); + + return plaintext; + } + }); + }()); + + + /** + * Cipher Feedback block mode. + */ + CryptoJS.mode.CFB = (function () { + var CFB = CryptoJS.lib.BlockCipherMode.extend(); + + CFB.Encryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + CFB.Decryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) { + var keystream; + + // Shortcut + var iv = this._iv; + + // Generate keystream + if (iv) { + keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + keystream = this._prevBlock; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + + return CFB; + }()); + + + /** + * Counter block mode. + */ + CryptoJS.mode.CTR = (function () { + var CTR = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = CTR.Encryptor = CTR.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Increment counter + counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0 + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTR.Decryptor = Encryptor; + + return CTR; + }()); + + + /** @preserve + * Counter block mode compatible with Dr Brian Gladman fileenc.c + * derived from CryptoJS.mode.CTR + * Jan Hruby jhruby.web@gmail.com + */ + CryptoJS.mode.CTRGladman = (function () { + var CTRGladman = CryptoJS.lib.BlockCipherMode.extend(); + + function incWord(word) + { + if (((word >> 24) & 0xff) === 0xff) { //overflow + var b1 = (word >> 16)&0xff; + var b2 = (word >> 8)&0xff; + var b3 = word & 0xff; + + if (b1 === 0xff) // overflow b1 + { + b1 = 0; + if (b2 === 0xff) + { + b2 = 0; + if (b3 === 0xff) + { + b3 = 0; + } + else + { + ++b3; + } + } + else + { + ++b2; + } + } + else + { + ++b1; + } + + word = 0; + word += (b1 << 16); + word += (b2 << 8); + word += b3; + } + else + { + word += (0x01 << 24); + } + return word; + } + + function incCounter(counter) + { + if ((counter[0] = incWord(counter[0])) === 0) + { + // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 + counter[1] = incWord(counter[1]); + } + return counter; + } + + var Encryptor = CTRGladman.Encryptor = CTRGladman.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + + incCounter(counter); + + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTRGladman.Decryptor = Encryptor; + + return CTRGladman; + }()); + + + + + /** + * Output Feedback block mode. + */ + CryptoJS.mode.OFB = (function () { + var OFB = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = OFB.Encryptor = OFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var keystream = this._keystream; + + // Generate keystream + if (iv) { + keystream = this._keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + OFB.Decryptor = Encryptor; + + return OFB; + }()); + + + /** + * Electronic Codebook block mode. + */ + CryptoJS.mode.ECB = (function () { + var ECB = CryptoJS.lib.BlockCipherMode.extend(); + + ECB.Encryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.encryptBlock(words, offset); + } + }); + + ECB.Decryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.decryptBlock(words, offset); + } + }); + + return ECB; + }()); + + + /** + * ANSI X.923 padding strategy. + */ + CryptoJS.pad.AnsiX923 = { + pad: function (data, blockSize) { + // Shortcuts + var dataSigBytes = data.sigBytes; + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - dataSigBytes % blockSizeBytes; + + // Compute last byte position + var lastBytePos = dataSigBytes + nPaddingBytes - 1; + + // Pad + data.clamp(); + data.words[lastBytePos >>> 2] |= nPaddingBytes << (24 - (lastBytePos % 4) * 8); + data.sigBytes += nPaddingBytes; + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + /** + * ISO 10126 padding strategy. + */ + CryptoJS.pad.Iso10126 = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Pad + data.concat(CryptoJS.lib.WordArray.random(nPaddingBytes - 1)). + concat(CryptoJS.lib.WordArray.create([nPaddingBytes << 24], 1)); + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + /** + * ISO/IEC 9797-1 Padding Method 2. + */ + CryptoJS.pad.Iso97971 = { + pad: function (data, blockSize) { + // Add 0x80 byte + data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1)); + + // Zero pad the rest + CryptoJS.pad.ZeroPadding.pad(data, blockSize); + }, + + unpad: function (data) { + // Remove zero padding + CryptoJS.pad.ZeroPadding.unpad(data); + + // Remove one more byte -- the 0x80 byte + data.sigBytes--; + } + }; + + + /** + * Zero padding strategy. + */ + CryptoJS.pad.ZeroPadding = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Pad + data.clamp(); + data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes); + }, + + unpad: function (data) { + // Shortcut + var dataWords = data.words; + + // Unpad + var i = data.sigBytes - 1; + for (var i = data.sigBytes - 1; i >= 0; i--) { + if (((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) { + data.sigBytes = i + 1; + break; + } + } + } + }; + + + /** + * A noop padding strategy. + */ + CryptoJS.pad.NoPadding = { + pad: function () { + }, + + unpad: function () { + } + }; + + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var CipherParams = C_lib.CipherParams; + var C_enc = C.enc; + var Hex = C_enc.Hex; + var C_format = C.format; + + var HexFormatter = C_format.Hex = { + /** + * Converts the ciphertext of a cipher params object to a hexadecimally encoded string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The hexadecimally encoded string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.format.Hex.stringify(cipherParams); + */ + stringify: function (cipherParams) { + return cipherParams.ciphertext.toString(Hex); + }, + + /** + * Converts a hexadecimally encoded ciphertext string to a cipher params object. + * + * @param {string} input The hexadecimally encoded string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.Hex.parse(hexString); + */ + parse: function (input) { + var ciphertext = Hex.parse(input); + return CipherParams.create({ ciphertext: ciphertext }); + } + }; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Lookup tables + var SBOX = []; + var INV_SBOX = []; + var SUB_MIX_0 = []; + var SUB_MIX_1 = []; + var SUB_MIX_2 = []; + var SUB_MIX_3 = []; + var INV_SUB_MIX_0 = []; + var INV_SUB_MIX_1 = []; + var INV_SUB_MIX_2 = []; + var INV_SUB_MIX_3 = []; + + // Compute lookup tables + (function () { + // Compute double table + var d = []; + for (var i = 0; i < 256; i++) { + if (i < 128) { + d[i] = i << 1; + } else { + d[i] = (i << 1) ^ 0x11b; + } + } + + // Walk GF(2^8) + var x = 0; + var xi = 0; + for (var i = 0; i < 256; i++) { + // Compute sbox + var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4); + sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63; + SBOX[x] = sx; + INV_SBOX[sx] = x; + + // Compute multiplication + var x2 = d[x]; + var x4 = d[x2]; + var x8 = d[x4]; + + // Compute sub bytes, mix columns tables + var t = (d[sx] * 0x101) ^ (sx * 0x1010100); + SUB_MIX_0[x] = (t << 24) | (t >>> 8); + SUB_MIX_1[x] = (t << 16) | (t >>> 16); + SUB_MIX_2[x] = (t << 8) | (t >>> 24); + SUB_MIX_3[x] = t; + + // Compute inv sub bytes, inv mix columns tables + var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); + INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8); + INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16); + INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24); + INV_SUB_MIX_3[sx] = t; + + // Compute next counter + if (!x) { + x = xi = 1; + } else { + x = x2 ^ d[d[d[x8 ^ x2]]]; + xi ^= d[d[xi]]; + } + } + }()); + + // Precomputed Rcon lookup + var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + + /** + * AES block cipher algorithm. + */ + var AES = C_algo.AES = BlockCipher.extend({ + _doReset: function () { + var t; + + // Skip reset of nRounds has been set before and key did not change + if (this._nRounds && this._keyPriorReset === this._key) { + return; + } + + // Shortcuts + var key = this._keyPriorReset = this._key; + var keyWords = key.words; + var keySize = key.sigBytes / 4; + + // Compute number of rounds + var nRounds = this._nRounds = keySize + 6; + + // Compute number of key schedule rows + var ksRows = (nRounds + 1) * 4; + + // Compute key schedule + var keySchedule = this._keySchedule = []; + for (var ksRow = 0; ksRow < ksRows; ksRow++) { + if (ksRow < keySize) { + keySchedule[ksRow] = keyWords[ksRow]; + } else { + t = keySchedule[ksRow - 1]; + + if (!(ksRow % keySize)) { + // Rot word + t = (t << 8) | (t >>> 24); + + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + + // Mix Rcon + t ^= RCON[(ksRow / keySize) | 0] << 24; + } else if (keySize > 6 && ksRow % keySize == 4) { + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + } + + keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t; + } + } + + // Compute inv key schedule + var invKeySchedule = this._invKeySchedule = []; + for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) { + var ksRow = ksRows - invKsRow; + + if (invKsRow % 4) { + var t = keySchedule[ksRow]; + } else { + var t = keySchedule[ksRow - 4]; + } + + if (invKsRow < 4 || ksRow <= 4) { + invKeySchedule[invKsRow] = t; + } else { + invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^ + INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]]; + } + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX); + }, + + decryptBlock: function (M, offset) { + // Swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + + this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX); + + // Inv swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + }, + + _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) { + // Shortcut + var nRounds = this._nRounds; + + // Get input, add round key + var s0 = M[offset] ^ keySchedule[0]; + var s1 = M[offset + 1] ^ keySchedule[1]; + var s2 = M[offset + 2] ^ keySchedule[2]; + var s3 = M[offset + 3] ^ keySchedule[3]; + + // Key schedule row counter + var ksRow = 4; + + // Rounds + for (var round = 1; round < nRounds; round++) { + // Shift rows, sub bytes, mix columns, add round key + var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++]; + var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++]; + var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++]; + var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++]; + + // Update state + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Shift rows, sub bytes, add round key + var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++]; + var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++]; + var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++]; + var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++]; + + // Set output + M[offset] = t0; + M[offset + 1] = t1; + M[offset + 2] = t2; + M[offset + 3] = t3; + }, + + keySize: 256/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); + */ + C.AES = BlockCipher._createHelper(AES); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Permuted Choice 1 constants + var PC1 = [ + 57, 49, 41, 33, 25, 17, 9, 1, + 58, 50, 42, 34, 26, 18, 10, 2, + 59, 51, 43, 35, 27, 19, 11, 3, + 60, 52, 44, 36, 63, 55, 47, 39, + 31, 23, 15, 7, 62, 54, 46, 38, + 30, 22, 14, 6, 61, 53, 45, 37, + 29, 21, 13, 5, 28, 20, 12, 4 + ]; + + // Permuted Choice 2 constants + var PC2 = [ + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 + ]; + + // Cumulative bit shift constants + var BIT_SHIFTS = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28]; + + // SBOXes and round permutation constants + var SBOX_P = [ + { + 0x0: 0x808200, + 0x10000000: 0x8000, + 0x20000000: 0x808002, + 0x30000000: 0x2, + 0x40000000: 0x200, + 0x50000000: 0x808202, + 0x60000000: 0x800202, + 0x70000000: 0x800000, + 0x80000000: 0x202, + 0x90000000: 0x800200, + 0xa0000000: 0x8200, + 0xb0000000: 0x808000, + 0xc0000000: 0x8002, + 0xd0000000: 0x800002, + 0xe0000000: 0x0, + 0xf0000000: 0x8202, + 0x8000000: 0x0, + 0x18000000: 0x808202, + 0x28000000: 0x8202, + 0x38000000: 0x8000, + 0x48000000: 0x808200, + 0x58000000: 0x200, + 0x68000000: 0x808002, + 0x78000000: 0x2, + 0x88000000: 0x800200, + 0x98000000: 0x8200, + 0xa8000000: 0x808000, + 0xb8000000: 0x800202, + 0xc8000000: 0x800002, + 0xd8000000: 0x8002, + 0xe8000000: 0x202, + 0xf8000000: 0x800000, + 0x1: 0x8000, + 0x10000001: 0x2, + 0x20000001: 0x808200, + 0x30000001: 0x800000, + 0x40000001: 0x808002, + 0x50000001: 0x8200, + 0x60000001: 0x200, + 0x70000001: 0x800202, + 0x80000001: 0x808202, + 0x90000001: 0x808000, + 0xa0000001: 0x800002, + 0xb0000001: 0x8202, + 0xc0000001: 0x202, + 0xd0000001: 0x800200, + 0xe0000001: 0x8002, + 0xf0000001: 0x0, + 0x8000001: 0x808202, + 0x18000001: 0x808000, + 0x28000001: 0x800000, + 0x38000001: 0x200, + 0x48000001: 0x8000, + 0x58000001: 0x800002, + 0x68000001: 0x2, + 0x78000001: 0x8202, + 0x88000001: 0x8002, + 0x98000001: 0x800202, + 0xa8000001: 0x202, + 0xb8000001: 0x808200, + 0xc8000001: 0x800200, + 0xd8000001: 0x0, + 0xe8000001: 0x8200, + 0xf8000001: 0x808002 + }, + { + 0x0: 0x40084010, + 0x1000000: 0x4000, + 0x2000000: 0x80000, + 0x3000000: 0x40080010, + 0x4000000: 0x40000010, + 0x5000000: 0x40084000, + 0x6000000: 0x40004000, + 0x7000000: 0x10, + 0x8000000: 0x84000, + 0x9000000: 0x40004010, + 0xa000000: 0x40000000, + 0xb000000: 0x84010, + 0xc000000: 0x80010, + 0xd000000: 0x0, + 0xe000000: 0x4010, + 0xf000000: 0x40080000, + 0x800000: 0x40004000, + 0x1800000: 0x84010, + 0x2800000: 0x10, + 0x3800000: 0x40004010, + 0x4800000: 0x40084010, + 0x5800000: 0x40000000, + 0x6800000: 0x80000, + 0x7800000: 0x40080010, + 0x8800000: 0x80010, + 0x9800000: 0x0, + 0xa800000: 0x4000, + 0xb800000: 0x40080000, + 0xc800000: 0x40000010, + 0xd800000: 0x84000, + 0xe800000: 0x40084000, + 0xf800000: 0x4010, + 0x10000000: 0x0, + 0x11000000: 0x40080010, + 0x12000000: 0x40004010, + 0x13000000: 0x40084000, + 0x14000000: 0x40080000, + 0x15000000: 0x10, + 0x16000000: 0x84010, + 0x17000000: 0x4000, + 0x18000000: 0x4010, + 0x19000000: 0x80000, + 0x1a000000: 0x80010, + 0x1b000000: 0x40000010, + 0x1c000000: 0x84000, + 0x1d000000: 0x40004000, + 0x1e000000: 0x40000000, + 0x1f000000: 0x40084010, + 0x10800000: 0x84010, + 0x11800000: 0x80000, + 0x12800000: 0x40080000, + 0x13800000: 0x4000, + 0x14800000: 0x40004000, + 0x15800000: 0x40084010, + 0x16800000: 0x10, + 0x17800000: 0x40000000, + 0x18800000: 0x40084000, + 0x19800000: 0x40000010, + 0x1a800000: 0x40004010, + 0x1b800000: 0x80010, + 0x1c800000: 0x0, + 0x1d800000: 0x4010, + 0x1e800000: 0x40080010, + 0x1f800000: 0x84000 + }, + { + 0x0: 0x104, + 0x100000: 0x0, + 0x200000: 0x4000100, + 0x300000: 0x10104, + 0x400000: 0x10004, + 0x500000: 0x4000004, + 0x600000: 0x4010104, + 0x700000: 0x4010000, + 0x800000: 0x4000000, + 0x900000: 0x4010100, + 0xa00000: 0x10100, + 0xb00000: 0x4010004, + 0xc00000: 0x4000104, + 0xd00000: 0x10000, + 0xe00000: 0x4, + 0xf00000: 0x100, + 0x80000: 0x4010100, + 0x180000: 0x4010004, + 0x280000: 0x0, + 0x380000: 0x4000100, + 0x480000: 0x4000004, + 0x580000: 0x10000, + 0x680000: 0x10004, + 0x780000: 0x104, + 0x880000: 0x4, + 0x980000: 0x100, + 0xa80000: 0x4010000, + 0xb80000: 0x10104, + 0xc80000: 0x10100, + 0xd80000: 0x4000104, + 0xe80000: 0x4010104, + 0xf80000: 0x4000000, + 0x1000000: 0x4010100, + 0x1100000: 0x10004, + 0x1200000: 0x10000, + 0x1300000: 0x4000100, + 0x1400000: 0x100, + 0x1500000: 0x4010104, + 0x1600000: 0x4000004, + 0x1700000: 0x0, + 0x1800000: 0x4000104, + 0x1900000: 0x4000000, + 0x1a00000: 0x4, + 0x1b00000: 0x10100, + 0x1c00000: 0x4010000, + 0x1d00000: 0x104, + 0x1e00000: 0x10104, + 0x1f00000: 0x4010004, + 0x1080000: 0x4000000, + 0x1180000: 0x104, + 0x1280000: 0x4010100, + 0x1380000: 0x0, + 0x1480000: 0x10004, + 0x1580000: 0x4000100, + 0x1680000: 0x100, + 0x1780000: 0x4010004, + 0x1880000: 0x10000, + 0x1980000: 0x4010104, + 0x1a80000: 0x10104, + 0x1b80000: 0x4000004, + 0x1c80000: 0x4000104, + 0x1d80000: 0x4010000, + 0x1e80000: 0x4, + 0x1f80000: 0x10100 + }, + { + 0x0: 0x80401000, + 0x10000: 0x80001040, + 0x20000: 0x401040, + 0x30000: 0x80400000, + 0x40000: 0x0, + 0x50000: 0x401000, + 0x60000: 0x80000040, + 0x70000: 0x400040, + 0x80000: 0x80000000, + 0x90000: 0x400000, + 0xa0000: 0x40, + 0xb0000: 0x80001000, + 0xc0000: 0x80400040, + 0xd0000: 0x1040, + 0xe0000: 0x1000, + 0xf0000: 0x80401040, + 0x8000: 0x80001040, + 0x18000: 0x40, + 0x28000: 0x80400040, + 0x38000: 0x80001000, + 0x48000: 0x401000, + 0x58000: 0x80401040, + 0x68000: 0x0, + 0x78000: 0x80400000, + 0x88000: 0x1000, + 0x98000: 0x80401000, + 0xa8000: 0x400000, + 0xb8000: 0x1040, + 0xc8000: 0x80000000, + 0xd8000: 0x400040, + 0xe8000: 0x401040, + 0xf8000: 0x80000040, + 0x100000: 0x400040, + 0x110000: 0x401000, + 0x120000: 0x80000040, + 0x130000: 0x0, + 0x140000: 0x1040, + 0x150000: 0x80400040, + 0x160000: 0x80401000, + 0x170000: 0x80001040, + 0x180000: 0x80401040, + 0x190000: 0x80000000, + 0x1a0000: 0x80400000, + 0x1b0000: 0x401040, + 0x1c0000: 0x80001000, + 0x1d0000: 0x400000, + 0x1e0000: 0x40, + 0x1f0000: 0x1000, + 0x108000: 0x80400000, + 0x118000: 0x80401040, + 0x128000: 0x0, + 0x138000: 0x401000, + 0x148000: 0x400040, + 0x158000: 0x80000000, + 0x168000: 0x80001040, + 0x178000: 0x40, + 0x188000: 0x80000040, + 0x198000: 0x1000, + 0x1a8000: 0x80001000, + 0x1b8000: 0x80400040, + 0x1c8000: 0x1040, + 0x1d8000: 0x80401000, + 0x1e8000: 0x400000, + 0x1f8000: 0x401040 + }, + { + 0x0: 0x80, + 0x1000: 0x1040000, + 0x2000: 0x40000, + 0x3000: 0x20000000, + 0x4000: 0x20040080, + 0x5000: 0x1000080, + 0x6000: 0x21000080, + 0x7000: 0x40080, + 0x8000: 0x1000000, + 0x9000: 0x20040000, + 0xa000: 0x20000080, + 0xb000: 0x21040080, + 0xc000: 0x21040000, + 0xd000: 0x0, + 0xe000: 0x1040080, + 0xf000: 0x21000000, + 0x800: 0x1040080, + 0x1800: 0x21000080, + 0x2800: 0x80, + 0x3800: 0x1040000, + 0x4800: 0x40000, + 0x5800: 0x20040080, + 0x6800: 0x21040000, + 0x7800: 0x20000000, + 0x8800: 0x20040000, + 0x9800: 0x0, + 0xa800: 0x21040080, + 0xb800: 0x1000080, + 0xc800: 0x20000080, + 0xd800: 0x21000000, + 0xe800: 0x1000000, + 0xf800: 0x40080, + 0x10000: 0x40000, + 0x11000: 0x80, + 0x12000: 0x20000000, + 0x13000: 0x21000080, + 0x14000: 0x1000080, + 0x15000: 0x21040000, + 0x16000: 0x20040080, + 0x17000: 0x1000000, + 0x18000: 0x21040080, + 0x19000: 0x21000000, + 0x1a000: 0x1040000, + 0x1b000: 0x20040000, + 0x1c000: 0x40080, + 0x1d000: 0x20000080, + 0x1e000: 0x0, + 0x1f000: 0x1040080, + 0x10800: 0x21000080, + 0x11800: 0x1000000, + 0x12800: 0x1040000, + 0x13800: 0x20040080, + 0x14800: 0x20000000, + 0x15800: 0x1040080, + 0x16800: 0x80, + 0x17800: 0x21040000, + 0x18800: 0x40080, + 0x19800: 0x21040080, + 0x1a800: 0x0, + 0x1b800: 0x21000000, + 0x1c800: 0x1000080, + 0x1d800: 0x40000, + 0x1e800: 0x20040000, + 0x1f800: 0x20000080 + }, + { + 0x0: 0x10000008, + 0x100: 0x2000, + 0x200: 0x10200000, + 0x300: 0x10202008, + 0x400: 0x10002000, + 0x500: 0x200000, + 0x600: 0x200008, + 0x700: 0x10000000, + 0x800: 0x0, + 0x900: 0x10002008, + 0xa00: 0x202000, + 0xb00: 0x8, + 0xc00: 0x10200008, + 0xd00: 0x202008, + 0xe00: 0x2008, + 0xf00: 0x10202000, + 0x80: 0x10200000, + 0x180: 0x10202008, + 0x280: 0x8, + 0x380: 0x200000, + 0x480: 0x202008, + 0x580: 0x10000008, + 0x680: 0x10002000, + 0x780: 0x2008, + 0x880: 0x200008, + 0x980: 0x2000, + 0xa80: 0x10002008, + 0xb80: 0x10200008, + 0xc80: 0x0, + 0xd80: 0x10202000, + 0xe80: 0x202000, + 0xf80: 0x10000000, + 0x1000: 0x10002000, + 0x1100: 0x10200008, + 0x1200: 0x10202008, + 0x1300: 0x2008, + 0x1400: 0x200000, + 0x1500: 0x10000000, + 0x1600: 0x10000008, + 0x1700: 0x202000, + 0x1800: 0x202008, + 0x1900: 0x0, + 0x1a00: 0x8, + 0x1b00: 0x10200000, + 0x1c00: 0x2000, + 0x1d00: 0x10002008, + 0x1e00: 0x10202000, + 0x1f00: 0x200008, + 0x1080: 0x8, + 0x1180: 0x202000, + 0x1280: 0x200000, + 0x1380: 0x10000008, + 0x1480: 0x10002000, + 0x1580: 0x2008, + 0x1680: 0x10202008, + 0x1780: 0x10200000, + 0x1880: 0x10202000, + 0x1980: 0x10200008, + 0x1a80: 0x2000, + 0x1b80: 0x202008, + 0x1c80: 0x200008, + 0x1d80: 0x0, + 0x1e80: 0x10000000, + 0x1f80: 0x10002008 + }, + { + 0x0: 0x100000, + 0x10: 0x2000401, + 0x20: 0x400, + 0x30: 0x100401, + 0x40: 0x2100401, + 0x50: 0x0, + 0x60: 0x1, + 0x70: 0x2100001, + 0x80: 0x2000400, + 0x90: 0x100001, + 0xa0: 0x2000001, + 0xb0: 0x2100400, + 0xc0: 0x2100000, + 0xd0: 0x401, + 0xe0: 0x100400, + 0xf0: 0x2000000, + 0x8: 0x2100001, + 0x18: 0x0, + 0x28: 0x2000401, + 0x38: 0x2100400, + 0x48: 0x100000, + 0x58: 0x2000001, + 0x68: 0x2000000, + 0x78: 0x401, + 0x88: 0x100401, + 0x98: 0x2000400, + 0xa8: 0x2100000, + 0xb8: 0x100001, + 0xc8: 0x400, + 0xd8: 0x2100401, + 0xe8: 0x1, + 0xf8: 0x100400, + 0x100: 0x2000000, + 0x110: 0x100000, + 0x120: 0x2000401, + 0x130: 0x2100001, + 0x140: 0x100001, + 0x150: 0x2000400, + 0x160: 0x2100400, + 0x170: 0x100401, + 0x180: 0x401, + 0x190: 0x2100401, + 0x1a0: 0x100400, + 0x1b0: 0x1, + 0x1c0: 0x0, + 0x1d0: 0x2100000, + 0x1e0: 0x2000001, + 0x1f0: 0x400, + 0x108: 0x100400, + 0x118: 0x2000401, + 0x128: 0x2100001, + 0x138: 0x1, + 0x148: 0x2000000, + 0x158: 0x100000, + 0x168: 0x401, + 0x178: 0x2100400, + 0x188: 0x2000001, + 0x198: 0x2100000, + 0x1a8: 0x0, + 0x1b8: 0x2100401, + 0x1c8: 0x100401, + 0x1d8: 0x400, + 0x1e8: 0x2000400, + 0x1f8: 0x100001 + }, + { + 0x0: 0x8000820, + 0x1: 0x20000, + 0x2: 0x8000000, + 0x3: 0x20, + 0x4: 0x20020, + 0x5: 0x8020820, + 0x6: 0x8020800, + 0x7: 0x800, + 0x8: 0x8020000, + 0x9: 0x8000800, + 0xa: 0x20800, + 0xb: 0x8020020, + 0xc: 0x820, + 0xd: 0x0, + 0xe: 0x8000020, + 0xf: 0x20820, + 0x80000000: 0x800, + 0x80000001: 0x8020820, + 0x80000002: 0x8000820, + 0x80000003: 0x8000000, + 0x80000004: 0x8020000, + 0x80000005: 0x20800, + 0x80000006: 0x20820, + 0x80000007: 0x20, + 0x80000008: 0x8000020, + 0x80000009: 0x820, + 0x8000000a: 0x20020, + 0x8000000b: 0x8020800, + 0x8000000c: 0x0, + 0x8000000d: 0x8020020, + 0x8000000e: 0x8000800, + 0x8000000f: 0x20000, + 0x10: 0x20820, + 0x11: 0x8020800, + 0x12: 0x20, + 0x13: 0x800, + 0x14: 0x8000800, + 0x15: 0x8000020, + 0x16: 0x8020020, + 0x17: 0x20000, + 0x18: 0x0, + 0x19: 0x20020, + 0x1a: 0x8020000, + 0x1b: 0x8000820, + 0x1c: 0x8020820, + 0x1d: 0x20800, + 0x1e: 0x820, + 0x1f: 0x8000000, + 0x80000010: 0x20000, + 0x80000011: 0x800, + 0x80000012: 0x8020020, + 0x80000013: 0x20820, + 0x80000014: 0x20, + 0x80000015: 0x8020000, + 0x80000016: 0x8000000, + 0x80000017: 0x8000820, + 0x80000018: 0x8020820, + 0x80000019: 0x8000020, + 0x8000001a: 0x8000800, + 0x8000001b: 0x0, + 0x8000001c: 0x20800, + 0x8000001d: 0x820, + 0x8000001e: 0x20020, + 0x8000001f: 0x8020800 + } + ]; + + // Masks that select the SBOX input + var SBOX_MASK = [ + 0xf8000001, 0x1f800000, 0x01f80000, 0x001f8000, + 0x0001f800, 0x00001f80, 0x000001f8, 0x8000001f + ]; + + /** + * DES block cipher algorithm. + */ + var DES = C_algo.DES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + + // Select 56 bits according to PC1 + var keyBits = []; + for (var i = 0; i < 56; i++) { + var keyBitPos = PC1[i] - 1; + keyBits[i] = (keyWords[keyBitPos >>> 5] >>> (31 - keyBitPos % 32)) & 1; + } + + // Assemble 16 subkeys + var subKeys = this._subKeys = []; + for (var nSubKey = 0; nSubKey < 16; nSubKey++) { + // Create subkey + var subKey = subKeys[nSubKey] = []; + + // Shortcut + var bitShift = BIT_SHIFTS[nSubKey]; + + // Select 48 bits according to PC2 + for (var i = 0; i < 24; i++) { + // Select from the left 28 key bits + subKey[(i / 6) | 0] |= keyBits[((PC2[i] - 1) + bitShift) % 28] << (31 - i % 6); + + // Select from the right 28 key bits + subKey[4 + ((i / 6) | 0)] |= keyBits[28 + (((PC2[i + 24] - 1) + bitShift) % 28)] << (31 - i % 6); + } + + // Since each subkey is applied to an expanded 32-bit input, + // the subkey can be broken into 8 values scaled to 32-bits, + // which allows the key to be used without expansion + subKey[0] = (subKey[0] << 1) | (subKey[0] >>> 31); + for (var i = 1; i < 7; i++) { + subKey[i] = subKey[i] >>> ((i - 1) * 4 + 3); + } + subKey[7] = (subKey[7] << 5) | (subKey[7] >>> 27); + } + + // Compute inverse subkeys + var invSubKeys = this._invSubKeys = []; + for (var i = 0; i < 16; i++) { + invSubKeys[i] = subKeys[15 - i]; + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._subKeys); + }, + + decryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._invSubKeys); + }, + + _doCryptBlock: function (M, offset, subKeys) { + // Get input + this._lBlock = M[offset]; + this._rBlock = M[offset + 1]; + + // Initial permutation + exchangeLR.call(this, 4, 0x0f0f0f0f); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeRL.call(this, 2, 0x33333333); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeLR.call(this, 1, 0x55555555); + + // Rounds + for (var round = 0; round < 16; round++) { + // Shortcuts + var subKey = subKeys[round]; + var lBlock = this._lBlock; + var rBlock = this._rBlock; + + // Feistel function + var f = 0; + for (var i = 0; i < 8; i++) { + f |= SBOX_P[i][((rBlock ^ subKey[i]) & SBOX_MASK[i]) >>> 0]; + } + this._lBlock = rBlock; + this._rBlock = lBlock ^ f; + } + + // Undo swap from last round + var t = this._lBlock; + this._lBlock = this._rBlock; + this._rBlock = t; + + // Final permutation + exchangeLR.call(this, 1, 0x55555555); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeRL.call(this, 2, 0x33333333); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeLR.call(this, 4, 0x0f0f0f0f); + + // Set output + M[offset] = this._lBlock; + M[offset + 1] = this._rBlock; + }, + + keySize: 64/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + // Swap bits across the left and right words + function exchangeLR(offset, mask) { + var t = ((this._lBlock >>> offset) ^ this._rBlock) & mask; + this._rBlock ^= t; + this._lBlock ^= t << offset; + } + + function exchangeRL(offset, mask) { + var t = ((this._rBlock >>> offset) ^ this._lBlock) & mask; + this._lBlock ^= t; + this._rBlock ^= t << offset; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.DES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.DES.decrypt(ciphertext, key, cfg); + */ + C.DES = BlockCipher._createHelper(DES); + + /** + * Triple-DES block cipher algorithm. + */ + var TripleDES = C_algo.TripleDES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + // Make sure the key length is valid (64, 128 or >= 192 bit) + if (keyWords.length !== 2 && keyWords.length !== 4 && keyWords.length < 6) { + throw new Error('Invalid key length - 3DES requires the key length to be 64, 128, 192 or >192.'); + } + + // Extend the key according to the keying options defined in 3DES standard + var key1 = keyWords.slice(0, 2); + var key2 = keyWords.length < 4 ? keyWords.slice(0, 2) : keyWords.slice(2, 4); + var key3 = keyWords.length < 6 ? keyWords.slice(0, 2) : keyWords.slice(4, 6); + + // Create DES instances + this._des1 = DES.createEncryptor(WordArray.create(key1)); + this._des2 = DES.createEncryptor(WordArray.create(key2)); + this._des3 = DES.createEncryptor(WordArray.create(key3)); + }, + + encryptBlock: function (M, offset) { + this._des1.encryptBlock(M, offset); + this._des2.decryptBlock(M, offset); + this._des3.encryptBlock(M, offset); + }, + + decryptBlock: function (M, offset) { + this._des3.decryptBlock(M, offset); + this._des2.encryptBlock(M, offset); + this._des1.decryptBlock(M, offset); + }, + + keySize: 192/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg); + */ + C.TripleDES = BlockCipher._createHelper(TripleDES); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + /** + * RC4 stream cipher algorithm. + */ + var RC4 = C_algo.RC4 = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + var keySigBytes = key.sigBytes; + + // Init sbox + var S = this._S = []; + for (var i = 0; i < 256; i++) { + S[i] = i; + } + + // Key setup + for (var i = 0, j = 0; i < 256; i++) { + var keyByteIndex = i % keySigBytes; + var keyByte = (keyWords[keyByteIndex >>> 2] >>> (24 - (keyByteIndex % 4) * 8)) & 0xff; + + j = (j + S[i] + keyByte) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + } + + // Counters + this._i = this._j = 0; + }, + + _doProcessBlock: function (M, offset) { + M[offset] ^= generateKeystreamWord.call(this); + }, + + keySize: 256/32, + + ivSize: 0 + }); + + function generateKeystreamWord() { + // Shortcuts + var S = this._S; + var i = this._i; + var j = this._j; + + // Generate keystream word + var keystreamWord = 0; + for (var n = 0; n < 4; n++) { + i = (i + 1) % 256; + j = (j + S[i]) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + + keystreamWord |= S[(S[i] + S[j]) % 256] << (24 - n * 8); + } + + // Update counters + this._i = i; + this._j = j; + + return keystreamWord; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4.decrypt(ciphertext, key, cfg); + */ + C.RC4 = StreamCipher._createHelper(RC4); + + /** + * Modified RC4 stream cipher algorithm. + */ + var RC4Drop = C_algo.RC4Drop = RC4.extend({ + /** + * Configuration options. + * + * @property {number} drop The number of keystream words to drop. Default 192 + */ + cfg: RC4.cfg.extend({ + drop: 192 + }), + + _doReset: function () { + RC4._doReset.call(this); + + // Drop + for (var i = this.cfg.drop; i > 0; i--) { + generateKeystreamWord.call(this); + } + } + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg); + */ + C.RC4Drop = StreamCipher._createHelper(RC4Drop); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm + */ + var Rabbit = C_algo.Rabbit = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Swap endian + for (var i = 0; i < 4; i++) { + K[i] = (((K[i] << 8) | (K[i] >>> 24)) & 0x00ff00ff) | + (((K[i] << 24) | (K[i] >>> 8)) & 0xff00ff00); + } + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg); + * var plaintext = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg); + */ + C.Rabbit = StreamCipher._createHelper(Rabbit); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm. + * + * This is a legacy version that neglected to convert the key to little-endian. + * This error doesn't affect the cipher's security, + * but it does affect its compatibility with other implementations. + */ + var RabbitLegacy = C_algo.RabbitLegacy = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg); + */ + C.RabbitLegacy = StreamCipher._createHelper(RabbitLegacy); + }()); + + + return CryptoJS; + +})); \ No newline at end of file diff --git a/server/src/main/resources/static/js/fenye.js b/server/src/main/resources/static/js/fenye.js new file mode 100644 index 00000000..6c8bf6df --- /dev/null +++ b/server/src/main/resources/static/js/fenye.js @@ -0,0 +1,299 @@ + function htmlttt (str){ + var s = ""; + if(str.length == 0) return ""; + s = str.replace(/&/gi,"&"); + s = s.replace(/ /gi," "); + s = s.replace(/'/gi,"\'"); + s = s.replace(/"/gi,"\""); + s = s.replace(/javascript/g,"javascript "); + s = s.replace(/iframe/gi, "iframe "); + return s; + } + + function isEmptyString(str) { + return !str || str.length === 0; +} + + function removeExtraNewlines(str) { + // 替换连续的换行符为单个换行符 + return str.replace(/(?:\r\n|\r|\n){2,}/g, '\n'); +} + + function DHTMLpagenation(content,kkkeyword,Length,page,txt) + { + if(page==0){ + page=1; + } + this.content=content; // 内容 + this.contentLength=s.length; // 内容长度 + this.pageSizeCount; // 总页数 + this.perpageLength= Length; //default perpage byte length. + this.currentPage= page; // 起始页为第1页 + this.regularExp=/\d+/; // 建立正则表达式,匹配数字型字符串。 + this.divDisplayContent; + this.contentStyle=null; + this.strDisplayContent=""; + this.divDisplayPagenation; + this.strDisplayPagenation=""; + + // 把第二个参数赋给perpageLength; + arguments.length==2 ? perpageLength = arguments[1] : ''; + + try { + //创建要显示的DIV + divExecuteTime=document.createElement("DIV"); + document.body.appendChild(divExecuteTime); + } + catch(e) + { + } + + // 得到divPagenation容器。 + if(document.getElementById("divPagenation")) + { + divDisplayPagenation=document.getElementById("divPagenation"); + } + else + { + try + { + //创建分页信息 + divDisplayPagenation=document.createElement("DIV"); + divDisplayPagenation.id="divPagenation"; + document.body.appendChild(divDisplayPagenation); + } + catch(e) + { + return false; + } + } + + // 得到divContent容器 + if(document.getElementById("divContent")) + { + divDisplayContent=document.getElementById("divContent"); + } + else + { + try + { + //创建每页显示内容的消息的DIV + divDisplayContent=document.createElement("DIV"); + divDisplayContent.id="divContent"; + document.body.appendChild(divDisplayContent); + } + catch(e) + { + return false; + } + } + DHTMLpagenation.initialize(); + return this; + + }; + + //初始化分页; + //包括把加入CSS,检查是否需要分页 + DHTMLpagenation.initialize=function() + { + divDisplayContent.className= contentStyle != null ? contentStyle : "divContent"; + + if(contentLength<=perpageLength) + { + if(txt =="code"){ + content = htmlttt(content); + strDisplayContent = '
'+content+'
'; + divDisplayContent.innerHTML=strDisplayContent; + if (!!window.ActiveXObject || "ActiveXObject" in window){ + }else{ + hljs.highlightAll(kkkeyword); + } + }else if(txt =="js"){ + content = htmlttt(content); + var result = js_beautify(content, 1, "\t"); + strDisplayContent = '
'+result+'
'; + divDisplayContent.innerHTML=strDisplayContent; + if (!!window.ActiveXObject || "ActiveXObject" in window){ + }else{ + hljs.highlightAll(kkkeyword); + } + }else{ + content = removeExtraNewlines(content); + let list = content.split('\n') // 换行符分割 + for(let i=0;i" + kkkeyword + ""); + } + divDisplayContent.innerHTML=txt; + } + return null; + } + + pageSizeCount=Math.ceil((contentLength/perpageLength)); + + DHTMLpagenation.goto(currentPage); + + DHTMLpagenation.displayContent(); + }; + + //显示分页栏 + DHTMLpagenation.displayPage=function() + { + strDisplayPagenation=""; + if(currentPage && currentPage !=1) + { + + strDisplayPagenation+=''; + } + else + { + strDisplayPagenation+="上一页"; + } + + if(currentPage && currentPage!=pageSizeCount) + { + strDisplayPagenation+=''; + + strDisplayPagenation+=" "; + } + else + { + strDisplayPagenation+="下一页"; + } + if (isEmptyString(currentPage)) { + currentPage =1; + } + strDisplayPagenation+=+currentPage+"/" + pageSizeCount + "页。
每页 " + perpageLength + " 字符,调整字符数:"; + divDisplayPagenation.innerHTML=strDisplayPagenation; + }; + + //上一页 + DHTMLpagenation.previous=function() + { + DHTMLpagenation.goto(currentPage-1); + }; + + //下一页 + DHTMLpagenation.next=function() + { + + DHTMLpagenation.goto(currentPage+1); + }; + + //跳转至某一页 + DHTMLpagenation.goto=function(iCurrentPage) + { + if (isEmptyString(iCurrentPage)) { + iCurrentPage =1; + } + if(regularExp.test(iCurrentPage)) + { + currentPage=iCurrentPage; + //获取当前的内容 里面包含 ❈ + var currentContent = s.substr((currentPage-1)*perpageLength,perpageLength); + //当前页是否有 ❈ 获取最后一个 ❈ 的位置 + var indexOf = currentContent.indexOf("❈"); + if(indexOf >= 0) + { + //获取从开始位置到当前页位置的内容 + var beginToEndContent = s.substr(0,currentPage*perpageLength); + + //获取开始到当前页位置的内容 中的 * 的最后的下标 + var reCount = beginToEndContent.split("❈").length - 1; + + var contentArray = currentContent.split("❈"); + + currentContent = replaceStr(contentArray,reCount,matchContent); + + } + + strDisplayContent=currentContent; + } + else + { + alert("页面参数错误"); + } + DHTMLpagenation.displayPage(); + DHTMLpagenation.displayContent(); + }; + //显示当前页内容 + DHTMLpagenation.displayContent=function() + { + if(txt =="code"){ + strDisplayContent = htmlttt(strDisplayContent); + strDisplayContent = "
"+strDisplayContent+"
"; + divDisplayContent.innerHTML=strDisplayContent; + if (!!window.ActiveXObject || "ActiveXObject" in window){ + }else{ + hljs.highlightAll(kkkeyword); + } + }else if(txt =="js"){ + strDisplayContent = htmlttt(strDisplayContent); + var result = js_beautify(strDisplayContent, 1, "\t"); + strDisplayContent ='
'+result+'
'; + divDisplayContent.innerHTML=strDisplayContent; + if (!!window.ActiveXObject || "ActiveXObject" in window){ + }else{ + hljs.highlightAll(kkkeyword); + } + }else{ + if (kkkeyword!==""&&kkkeyword!==null&&kkkeyword!=="null") { + + strDisplayContent = strDisplayContent.split(kkkeyword).join("" + kkkeyword + ""); + } + divDisplayContent.innerHTML=strDisplayContent; + } + }; + //改变每页的字节数 + DHTMLpagenation.change=function() + { + + var iPerpageLength = document.getElementById("ctlPerpageLength").value; + if(regularExp.test(iPerpageLength)) + { + + DHTMLpagenation(s,iPerpageLength); + } + else + { + alert("请输入数字"); + } + }; + //改变页码 + DHTMLpagenation.tiaozhuan=function() + { + var yema = document.getElementById("yemaPerpageLength").value; + if(regularExp.test(yema)) + { + DHTMLpagenation.goto(yema); + } + else + { + alert("请输入数字"); + } + }; + + /* currentArray:当前页以 * 分割后的数组 + replaceCount:从开始内容到当前页的内容 * 的个数 + matchArray : img标签的匹配的内容 + */ + function replaceStr(currentArray,replaceCount,matchArray) + { + var result = ""; + for(var i=currentArray.length -1,j = replaceCount-1 ;i>=1; i--) + { + + var temp = (matchArray[j] + currentArray[i]); + + result = temp + result; + + j--; + } + + result = currentArray[0] + result ; + + return result; + } \ No newline at end of file diff --git a/server/src/main/resources/static/js/jsformat.js b/server/src/main/resources/static/js/jsformat.js new file mode 100644 index 00000000..c2afc3e1 --- /dev/null +++ b/server/src/main/resources/static/js/jsformat.js @@ -0,0 +1,72 @@ +function js_beautify(js_source_text,indent_size,indent_character,indent_level) +{var input,output,token_text,last_type,last_text,last_word,current_mode,modes,indent_string;var whitespace,wordchar,punct,parser_pos,line_starters,in_case;var prefix,token_type,do_block_just_closed,var_line,var_line_tainted;function trim_output() +{while(output.length&&(output[output.length-1]===' '||output[output.length-1]===indent_string)){output.pop();}} +function print_newline(ignore_repeated) +{ignore_repeated=typeof ignore_repeated==='undefined'?true:ignore_repeated;trim_output();if(!output.length){return;} +if(output[output.length-1]!=="\n"||!ignore_repeated){output.push("\n");} +for(var i=0;i=input.length){return['','TK_EOF'];} +c=input.charAt(parser_pos);parser_pos+=1;if(c==="\n"){n_newlines+=1;}} +while(in_array(c,whitespace));if(n_newlines>1){for(var i=0;i<2;i++){print_newline(i===0);}} +var wanted_newline=(n_newlines===1);if(in_array(c,wordchar)){if(parser_pos=input.length){break;}}} +parser_pos+=2;return['/*'+comment+'*/','TK_BLOCK_COMMENT'];} +if(input.charAt(parser_pos)==='/'){comment=c;while(input.charAt(parser_pos)!=="\x0d"&&input.charAt(parser_pos)!=="\x0a"){comment+=input.charAt(parser_pos);parser_pos+=1;if(parser_pos>=input.length){break;}} +parser_pos+=1;if(wanted_newline){print_newline();} +return[comment,'TK_COMMENT'];}} +if(c==="'"||c==='"'||(c==='/'&&((last_type==='TK_WORD'&&last_text==='return')||(last_type==='TK_START_EXPR'||last_type==='TK_END_BLOCK'||last_type==='TK_OPERATOR'||last_type==='TK_EOF'||last_type==='TK_END_COMMAND')))){var sep=c;var esc=false;c='';if(parser_pos=input.length){break;}}} +parser_pos+=1;if(last_type==='TK_END_COMMAND'){print_newline();} +return[sep+c+sep,'TK_STRING'];} +if(in_array(c,punct)){while(parser_pos=input.length){break;}} +return[c,'TK_OPERATOR'];} +return[c,'TK_UNKNOWN'];} +indent_character=indent_character||' ';indent_size=indent_size||4;indent_string='';while(indent_size--){indent_string+=indent_character;} +input=js_source_text;last_word='';last_type='TK_START_EXPR';last_text='';output=[];do_block_just_closed=false;var_line=false;var_line_tainted=false;whitespace="\n\r\t ".split('');wordchar='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split('');punct='+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |='.split(' ');line_starters='continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',');current_mode='BLOCK';modes=[current_mode];indent_level=indent_level||0;parser_pos=0;in_case=false;while(true){var t=get_next_token(parser_pos);token_text=t[0];token_type=t[1];if(token_type==='TK_EOF'){break;} +switch(token_type){case'TK_START_EXPR':var_line=false;set_mode('EXPRESSION');if(last_type==='TK_END_EXPR'||last_type==='TK_START_EXPR'){}else if(last_type!=='TK_WORD'&&last_type!=='TK_OPERATOR'){print_space();}else if(in_array(last_word,line_starters)&&last_word!=='function'){print_space();} +print_token();break;case'TK_END_EXPR':print_token();restore_mode();break;case'TK_START_BLOCK':if(last_word==='do'){set_mode('DO_BLOCK');}else{set_mode('BLOCK');} +if(last_type!=='TK_OPERATOR'&&last_type!=='TK_START_EXPR'){if(last_type==='TK_START_BLOCK'){print_newline();}else{print_space();}} +print_token();indent();break;case'TK_END_BLOCK':if(last_type==='TK_START_BLOCK'){trim_output();unindent();}else{unindent();print_newline();} +print_token();restore_mode();break;case'TK_WORD':if(do_block_just_closed){print_space();print_token();print_space();break;} +if(token_text==='case'||token_text==='default'){if(last_text===':'){remove_indent();}else{unindent();print_newline();indent();} +print_token();in_case=true;break;} +prefix='NONE';if(last_type==='TK_END_BLOCK'){if(!in_array(token_text.toLowerCase(),['else','catch','finally'])){prefix='NEWLINE';}else{prefix='SPACE';print_space();}}else if(last_type==='TK_END_COMMAND'&&(current_mode==='BLOCK'||current_mode==='DO_BLOCK')){prefix='NEWLINE';}else if(last_type==='TK_END_COMMAND'&¤t_mode==='EXPRESSION'){prefix='SPACE';}else if(last_type==='TK_WORD'){prefix='SPACE';}else if(last_type==='TK_START_BLOCK'){prefix='NEWLINE';}else if(last_type==='TK_END_EXPR'){print_space();prefix='NEWLINE';} +if(last_type!=='TK_END_BLOCK'&&in_array(token_text.toLowerCase(),['else','catch','finally'])){print_newline();}else if(in_array(token_text,line_starters)||prefix==='NEWLINE'){if(last_text==='else'){print_space();}else if((last_type==='TK_START_EXPR'||last_text==='=')&&token_text==='function'){}else if(last_type==='TK_WORD'&&(last_text==='return'||last_text==='throw')){print_space();}else if(last_type!=='TK_END_EXPR'){if((last_type!=='TK_START_EXPR'||token_text!=='var')&&last_text!==':'){if(token_text==='if'&&last_type==='TK_WORD'&&last_word==='else'){print_space();}else{print_newline();}}}else{if(in_array(token_text,line_starters)&&last_text!==')'){print_newline();}}}else if(prefix==='SPACE'){print_space();} +print_token();last_word=token_text;if(token_text==='var'){var_line=true;var_line_tainted=false;} +break;case'TK_END_COMMAND':print_token();var_line=false;break;case'TK_STRING':if(last_type==='TK_START_BLOCK'||last_type==='TK_END_BLOCK'){print_newline();}else if(last_type==='TK_WORD'){print_space();} +print_token();break;case'TK_OPERATOR':var start_delim=true;var end_delim=true;if(var_line&&token_text!==','){var_line_tainted=true;if(token_text===':'){var_line=false;}} +if(token_text===':'&&in_case){print_token();print_newline();break;} +in_case=false;if(token_text===','){if(var_line){if(var_line_tainted){print_token();print_newline();var_line_tainted=false;}else{print_token();print_space();}}else if(last_type==='TK_END_BLOCK'){print_token();print_newline();}else{if(current_mode==='BLOCK'){print_token();print_newline();}else{print_token();print_space();}} +break;}else if(token_text==='--'||token_text==='++'){if(last_text===';'){start_delim=true;end_delim=false;}else{start_delim=false;end_delim=false;}}else if(token_text==='!'&&last_type==='TK_START_EXPR'){start_delim=false;end_delim=false;}else if(last_type==='TK_OPERATOR'){start_delim=false;end_delim=false;}else if(last_type==='TK_END_EXPR'){start_delim=true;end_delim=true;}else if(token_text==='.'){start_delim=false;end_delim=false;}else if(token_text===':'){if(last_text.match(/^\d+$/)){start_delim=true;}else{start_delim=false;}} +if(start_delim){print_space();} +print_token();if(end_delim){print_space();} +break;case'TK_BLOCK_COMMENT':print_newline();print_token();print_newline();break;case'TK_COMMENT':print_space();print_token();print_newline();break;case'TK_UNKNOWN':print_token();break;} +last_type=token_type;last_text=token_text;} +return output.join('');} \ No newline at end of file diff --git a/server/src/main/resources/static/js/viewer.min.js b/server/src/main/resources/static/js/viewer.min.js index 232bafed..9269322b 100644 --- a/server/src/main/resources/static/js/viewer.min.js +++ b/server/src/main/resources/static/js/viewer.min.js @@ -2612,7 +2612,8 @@ return this.destroy(); } var images = []; - forEach(isImg ? [element] : element.querySelectorAll('img'), function (image) { + //console.log('Hello, World!'); + forEach(isImg ? [element] : element.querySelectorAll('div'), function (image) { if (isFunction(options.filter)) { if (options.filter.call(_this12, image)) { images.push(image); @@ -2989,7 +2990,8 @@ } var isImg = element.localName === 'img'; var images = []; - forEach(isImg ? [element] : element.querySelectorAll('img'), function (image) { + // console.log('Hello, World!'); + forEach(isImg ? [element] : element.querySelectorAll('div'), function (image) { if (isFunction(options.filter)) { if (options.filter.call(_this, image)) { images.push(image); diff --git a/server/src/main/resources/static/pdfjs/build/pdf.mjs b/server/src/main/resources/static/pdfjs/build/pdf.mjs index f2290296..7abbbfc9 100644 --- a/server/src/main/resources/static/pdfjs/build/pdf.mjs +++ b/server/src/main/resources/static/pdfjs/build/pdf.mjs @@ -24,7 +24,23 @@ * pdfjsVersion = 5.4.530 * pdfjsBuild = 50cc4adac */ -/******/ var __webpack_modules__ = ({ +/******/ +var queryStringg = document.location.search.substring(1); +var paramss = parseQueryStringg(queryStringg); +function parseQueryStringg(query) { + const paramss = new Map(); + for (const [key, value] of new URLSearchParams(query)) { + paramss.set(key.toLowerCase(), value); + } + return paramss; +} +var kkSIZE = paramss.get("pdfautofetch"); +if (kkSIZE == "true" ) { + kkSIZE = 65536*16; +} else { + kkSIZE = 65536; +} +var __webpack_modules__ = ({ /***/ 34: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { @@ -20714,7 +20730,7 @@ class TextLayer { - +const DEFAULT_RANGE_CHUNK_SIZE = kkSIZE; const RENDERING_CANCELLED_TIMEOUT = 100; function getDocument(src = {}) { if (typeof src === "string" || src instanceof URL) { @@ -20736,7 +20752,7 @@ function getDocument(src = {}) { const withCredentials = src.withCredentials === true; const password = src.password ?? null; const rangeTransport = src.range instanceof PDFDataRangeTransport ? src.range : null; - const rangeChunkSize = Number.isInteger(src.rangeChunkSize) && src.rangeChunkSize > 0 ? src.rangeChunkSize : 2 ** 16; + const rangeChunkSize = Number.isInteger(src.rangeChunkSize) && src.rangeChunkSize > 0 ? src.rangeChunkSize : DEFAULT_RANGE_CHUNK_SIZE; let worker = src.worker instanceof PDFWorker ? src.worker : null; const verbosity = src.verbosity; const docBaseUrl = typeof src.docBaseUrl === "string" && !isDataScheme(src.docBaseUrl) ? src.docBaseUrl : null; diff --git a/server/src/main/resources/web/bpmn.ftl b/server/src/main/resources/web/bpmn.ftl index 8da95053..fb5263f1 100644 --- a/server/src/main/resources/web/bpmn.ftl +++ b/server/src/main/resources/web/bpmn.ftl @@ -41,9 +41,10 @@ const viewer = new BpmnJS({ container: '#diagram' }); - var url = '${finalUrl}'; + var url = '${finalUrl}'; + var kkagent = '${kkagent}'; var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/'; - if (!url.startsWith(baseUrl)) { + if (kkagent === 'true' || !url.startsWith(baseUrl)) { url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)); } async function showDiagram(diagramXML) { diff --git a/server/src/main/resources/web/code.ftl b/server/src/main/resources/web/code.ftl index 454eb7c1..44479380 100644 --- a/server/src/main/resources/web/code.ftl +++ b/server/src/main/resources/web/code.ftl @@ -3,26 +3,22 @@ - 代码预览 + ${file.name}代码预览 <#include "*/commonHeader.ftl"> + + + <#if "${file.suffix?lower_case}" == "js" > + + - - - - -
@@ -32,39 +28,30 @@
-
-
-
+
+
+
+
+
+
- - - diff --git a/server/src/main/resources/web/dcm.ftl b/server/src/main/resources/web/dcm.ftl index 4d8d24f1..52f54b45 100644 --- a/server/src/main/resources/web/dcm.ftl +++ b/server/src/main/resources/web/dcm.ftl @@ -54,9 +54,10 @@ + + @@ -81,7 +143,7 @@

支持的文件类型

我们一直在扩展支持的文件类型,不断优化预览的效果,如果您有什么建议,欢迎在kk开源社区留意反馈:https://t.zsxq.com/09ZHSXbsQ。 -
+
  1. 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx, xlam, xla, pages 等 Office 办公文档
  2. 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档
  3. @@ -112,28 +174,41 @@

    输入下载地址预览

    + 跨域说明: 跨域是指你接入的URL默认支持跨域 不需要KK在进行反代了
    + <#if "${kkkey}" != "false" > + 程序已经启用秘钥功能接入访问需要输入秘钥,获取秘钥请联系管理员!
    + + <#if isshowkey> + <#if "${kkkey}" != "false" > + 接入秘钥是:${kkkey} + +
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    +
    + + + + + + + + + + + <#if isshowkey> + + - - - + + +
    +
    @@ -143,24 +218,83 @@

    上传本地文件预览

- <#if fileUploadDisable == false> -
- - -
- <#else> -
-

- 文件上传功能默认已禁用。如需开启,请通过以下方式配置: -
- • 配置文件:file.upload.disable=false -
- • 环境变量:KK_FILE_UPLOAD_DISABLE=false -
- 请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。 -

+ <#-- 目录导航 --> + + + <#-- 操作区域 --> +
+
+ <#-- 上传区域 --> + <#if fileUploadDisable == false> +
+
+ + + + + +
+
+ <#else> +
+
+
+ + + + + +
+
+
+ + 文件上传功能已禁用。如需开启,请修改配置文件或联系管理员。 +
+
+
- +
+ <#-- 搜索框 --> +
+ + + + + +
+
+
+ + <#-- 新建文件夹对话框 --> + + + <#-- 文件列表表格 -->
@@ -191,11 +325,15 @@ <#if beian?? && beian != "default"> - + \ No newline at end of file diff --git a/server/src/main/resources/web/ofd.ftl b/server/src/main/resources/web/ofd.ftl index d05ea302..b1097121 100644 --- a/server/src/main/resources/web/ofd.ftl +++ b/server/src/main/resources/web/ofd.ftl @@ -18,14 +18,16 @@ - +
<#list imgUrls as img> -
- loading +
+
+ +
+ + + + +
+
+
<#if "false" == switchDisabled> 使用PDF预览 - + + + + + + + - + \ No newline at end of file diff --git a/server/src/main/resources/web/officeweb.ftl b/server/src/main/resources/web/officeweb.ftl index 4462a7bf..1025b1e3 100644 --- a/server/src/main/resources/web/officeweb.ftl +++ b/server/src/main/resources/web/officeweb.ftl @@ -22,7 +22,6 @@ * 初始化水印 */ function initWaterMark() { - let watermarkTxt = '${watermarkTxt}'; if (watermarkTxt !== '') { watermark.init({ @@ -44,6 +43,10 @@ } } + // 添加加载状态管理 + let isLoading = false; + let loadingTask = null; + + +
+
+
正在加载Excel文件...
+
+
+
+
+ + +
+

加载失败

+

+ +
+ +

-

+ @@ -74,59 +157,218 @@ test = test+'&officePreviewType=html'; window.location.href=test; } + var url = '${finalUrl}'; var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/'; if (!url.startsWith(baseUrl)) { url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)); } + let mask = document.getElementById("lucky-mask-demo"); - function loadText() { - initWaterMark(); // 是否显示水印 - var value = url; - var name = '${file.name}'; - if(value==""){ - return; + let loadingOverlay = document.getElementById("loading-overlay"); + let loadingBar = document.getElementById("loading-bar"); + let errorMessage = document.getElementById("error-message"); + + // 更新加载进度 + function updateProgress(percent) { + if (loadingBar) { + loadingBar.style.width = percent + '%'; } - // mask.style.display = "flex"; - LuckyExcel.transformExcelToLuckyByUrl(value, name, function(exportJson, luckysheetfile){ - if(exportJson.sheets==null || exportJson.sheets.length==0){ - alert("读取excel文件内容失败!"); + } + + // 显示错误信息 + function showError(message) { + hideLoading(); + errorMessage.style.display = 'block'; + document.getElementById('error-detail').textContent = message; + } + + // 隐藏加载动画 + function hideLoading() { + if (loadingOverlay) { + loadingOverlay.style.opacity = '0'; + setTimeout(() => { + loadingOverlay.style.display = 'none'; + document.getElementById('button-area').style.display = 'block'; + }, 300); + } + } + + // 重试加载 + function retryLoad() { + errorMessage.style.display = 'none'; + loadingOverlay.style.display = 'flex'; + loadingOverlay.style.opacity = '1'; + loadTextAsync(); + } + + // 异步加载Excel文件 + async function loadTextAsync() { + if (isLoading) return; + + isLoading = true; + updateProgress(10); + + try { + initWaterMark(); + + const value = url; + const name = '${file.name}'; + + if (!value) { + showError('文件URL为空'); return; } - mask.style.display = "none"; - window.luckysheet.destroy(); - window.luckysheet.create({ - container: 'luckysheet', //luckysheet is the container id - lang: "zh", - showtoolbarConfig:{ - image: true, - print: true, //关闭打印按钮 启用也不能用 等以后看情况而定 - exportXlsx: true, //关闭导出按钮 启用也不能用 等以后看情况而定 - }, - allowCopy: true, // 是否允许拷贝 - showtoolbar: true, // 是否显示工具栏 - showinfobar: false, // 是否显示顶部信息栏 - // myFolderUrl: "/",//作用:左上角<返回按钮的链接 - showsheetbar: true, // 是否显示底部sheet页按钮 - showstatisticBar: true, // 是否显示底部计数栏 - sheetBottomConfig: true, // sheet页下方的添加行按钮和回到顶部按钮配置 - allowEdit: true, // 是否允许前台编辑 - enableAddRow: false, // 允许增加行 - enableAddCol: false, // 允许增加列 - userInfo: false, // 右上角的用户信息展示样式 - showRowBar: true, // 是否显示行号区域 - showColumnBar: false, // 是否显示列号区域 - sheetFormulaBar: false, // 是否显示公式栏 - enableAddBackTop: true,//返回头部按钮 - forceCalculation: false, //下面是导出插件 默认关闭 - data: exportJson.sheets, - title: exportJson.info.name, - userInfo: exportJson.info.name.creator, - }); - }, 1000); + + updateProgress(30); + + // 使用异步方式加载 + await new Promise(resolve => setTimeout(resolve, 100)); // 给UI更新一点时间 + + + + // 或者使用现有的同步方法,但放在setTimeout中避免阻塞 + await transformWithTimeout(value, name); + + updateProgress(100); + + // 延迟隐藏加载界面,让用户看到加载完成 + setTimeout(() => { + hideLoading(); + isLoading = false; + }, 500); + + } catch (error) { + console.error('加载Excel失败:', error); + showError('加载失败: ' + error.message); + isLoading = false; + } } - loadText(); - // 打印时,获取luckysheet指定区域html内容,拼接至div,隐藏luckysheet容器并显示打印区域html + + // 使用setTimeout将同步任务拆分 + function transformWithTimeout(value, name) { + return new Promise((resolve, reject) => { + updateProgress(50); + + // 将转换过程放在setTimeout中,避免阻塞主线程 + setTimeout(() => { + try { + LuckyExcel.transformExcelToLuckyByUrl(value, name, function(exportJson, luckysheetfile){ + if(exportJson.sheets==null || exportJson.sheets.length==0){ + reject(new Error("读取excel文件内容失败!")); + return; + } + + updateProgress(80); + + // 使用requestAnimationFrame来更新UI,避免阻塞 + requestAnimationFrame(() => { + try { + window.luckysheet.destroy(); + window.luckysheet.create({ + container: 'luckysheet', + lang: "zh", + showtoolbarConfig:{ + image: true, + print: true, + exportXlsx: true, + }, + allowCopy: true, + showtoolbar: true, + showinfobar: false, + showsheetbar: true, + showstatisticBar: true, + sheetBottomConfig: true, + allowEdit: true, + enableAddRow: false, + enableAddCol: false, + userInfo: false, + showRowBar: true, + showColumnBar: false, + sheetFormulaBar: false, + enableAddBackTop: true, + forceCalculation: false, + data: exportJson.sheets, + title: exportJson.info.name, + userInfo: exportJson.info.name.creator, + // 添加加载完成的回调 + hook: { + workbookCreateAfter: function() { + resolve(); + } + } + }); + + updateProgress(90); + + } catch (err) { + reject(err); + } + }); + }, 100); + + } catch (error) { + reject(error); + } + }, 100); + }); + } + + + + // 初始化Luckysheet + function initializeLuckysheet(exportJson) { + if (!exportJson.sheets || exportJson.sheets.length === 0) { + throw new Error("读取excel文件内容失败!"); + } + + window.luckysheet.destroy(); + window.luckysheet.create({ + container: 'luckysheet', + lang: "zh", + showtoolbarConfig:{ + image: true, + print: true, + exportXlsx: true, + }, + allowCopy: true, + showtoolbar: true, + showinfobar: false, + showsheetbar: true, + showstatisticBar: true, + sheetBottomConfig: true, + allowEdit: true, + enableAddRow: false, + enableAddCol: false, + userInfo: false, + showRowBar: true, + showColumnBar: false, + sheetFormulaBar: false, + enableAddBackTop: true, + forceCalculation: false, + data: exportJson.sheets, + title: exportJson.info.name, + userInfo: exportJson.info.name.creator, + }); + } + + // 页面加载完成后开始异步加载 + document.addEventListener('DOMContentLoaded', function() { + // 延迟一点时间开始加载,确保DOM完全加载 + setTimeout(() => { + loadTextAsync(); + }, 100); + }); + + // 添加取消加载的功能(按ESC键) + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && isLoading) { + // 可以在这里添加取消加载的逻辑 + console.log('用户取消了加载'); + } + }); + + // 打印时,获取luckysheet指定区域html内容 function to_print() { const html = luckysheet.getRangeHtml(); document.querySelector('#print-html').innerHTML = html; diff --git a/server/src/main/resources/web/pdf.ftl b/server/src/main/resources/web/pdf.ftl index 845ac8ca..89b2e002 100644 --- a/server/src/main/resources/web/pdf.ftl +++ b/server/src/main/resources/web/pdf.ftl @@ -21,9 +21,10 @@ - - <#if currentUrl?contains("http://") || currentUrl?contains("https://") || currentUrl?contains("ftp://")> + + <#if currentUrl?contains("http://") || currentUrl?contains("https://") || currentUrl?contains("file://")|| currentUrl?contains("ftp://")> <#assign finalUrl="${currentUrl}"> <#else> <#assign finalUrl="${baseUrl}${currentUrl}"> @@ -16,38 +105,320 @@
+
+
缩放: 100%
+
正在加载SVG...
+
+ + + + + +
- - + \ No newline at end of file diff --git a/server/src/main/resources/web/tiff.ftl b/server/src/main/resources/web/tiff.ftl index fa1daaff..6eadf179 100644 --- a/server/src/main/resources/web/tiff.ftl +++ b/server/src/main/resources/web/tiff.ftl @@ -4,7 +4,7 @@ Tiff 图片预览 <#include "*/commonHeader.ftl"> - + <#if currentUrl?contains("http://") || currentUrl?contains("https://") || currentUrl?contains("file://")|| currentUrl?contains("ftp://")> @@ -13,13 +13,6 @@ <#assign finalUrl="${baseUrl}${currentUrl}"> - <#if "false" == pdfDownloadDisable> @@ -38,8 +31,9 @@ return reg.test(this); } var url = '${finalUrl}'; - var baseUrl = '${baseUrl}'.endsWithh('/') ? '${baseUrl}' : '${baseUrl}' + '/'; - if (!url.startsWithh(baseUrl)) { + var kkagent = '${kkagent}'; + var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/'; + if (kkagent === 'true' || !url.startsWith(baseUrl)) { url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)); } var myp = document.getElementById('tiff'); @@ -59,11 +53,17 @@ } catch(e){ if (e.message.indexOf("CanvasRenderingContext2D")) { - var imgObjj = new Image(); - imgObjj.src = url; - myp.appendChild(imgObjj); - console.log("错误:" + e); - return; +var html = ""; +html += "
"; +html += "
"; +html += ''; +html += "
"; +html += ""; +html += "
"; +html += "
"; +html += "
"; +myp.innerHTML = html; +return; } console.log("错误:" + e); var html = ""; @@ -109,24 +109,42 @@ html += "

有任何疑问,请加入kk开源社区知识星球咨询:"; +html += "

"; +html += "
"; +const child = document.createElement('div'); +child.innerHTML = html; +myp.appendChild(child); } function imgLoaded(e) { - resp = e.target.response; + resp = e; pages = UTIF.decode(resp); p = 0; loadOne(); @@ -134,8 +152,62 @@ html += ""; const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'arraybuffer'; - xhr.onload = imgLoaded; + xhr.onload = function() { + if (xhr.status === 200) { + imgLoaded(xhr.response); + } else { + console.log(`Error ${xhr.status}: ${xhr.statusText}`) + } +}; xhr.send(); + function rotateImg(imgId, isRotate) { + var img = document.querySelector("#" + imgId); + + if (img.classList.contains("imgT90")) { + img.classList.remove("imgT90"); + if (isRotate) { + img.classList.add("imgT180"); + } + } else if (img.classList.contains("imgT-90")) { + img.classList.remove("imgT-90"); + if (!isRotate) { + img.classList.add("imgT-180"); + } + } else if (img.classList.contains("imgT180")) { + img.classList.remove("imgT180"); + if (isRotate) { + img.classList.add("imgT270"); + } else { + img.classList.add("imgT90"); + } + } else if (img.classList.contains("imgT-180")) { + img.classList.remove("imgT-180"); + if (isRotate) { + img.classList.add("imgT-90"); + } else { + img.classList.add("imgT-270"); + } + } else if (img.classList.contains("imgT270")) { + img.classList.remove("imgT270"); + if (!isRotate) { + img.classList.add("imgT180"); + } + } else if (img.classList.contains("imgT-270")) { + img.classList.remove("imgT-270"); + if (isRotate) { + img.classList.add("imgT-180"); + } + } else { + if (isRotate) { + img.classList.add("imgT90"); + } else { + img.classList.add("imgT-90"); + } + } + } + function recoveryImg(imgId) { + document.querySelector("#" + imgId).classList.remove("imgT90", "imgT180", "imgT270", "imgT-90", "imgT-180", "imgT-270") + } /*初始化水印*/ window.onload = function () { diff --git a/server/src/main/resources/web/txt.ftl b/server/src/main/resources/web/txt.ftl index bb7167af..c7a342a9 100644 --- a/server/src/main/resources/web/txt.ftl +++ b/server/src/main/resources/web/txt.ftl @@ -12,286 +12,89 @@ -<#if "${file.suffix?html}" == "txt" || "${file.suffix?html}" == "log" || "${file.suffix?html}" == "TXT" || "${file.suffix?html}" == "LOG"> - - - -
+<#if "${file.suffix?html}" == "htm" || "${file.suffix?html}" == "html" || "${file.suffix?html}" == "shtml" > + - - <#else/> +<#else/> + +
@@ -301,31 +104,30 @@ background-color:#000000
-
-
+
+
+
+
+
- - - + - + \ No newline at end of file diff --git a/server/src/main/resources/web/xmind.ftl b/server/src/main/resources/web/xmind.ftl index 3912e347..ae1aca29 100644 --- a/server/src/main/resources/web/xmind.ftl +++ b/server/src/main/resources/web/xmind.ftl @@ -20,10 +20,11 @@