yooper 1 год назад
Родитель
Сommit
f7564f812a
43 измененных файлов с 566 добавлено и 15 удалено
  1. 1 0
      .gitignore
  2. 20 0
      _locales/de/messages.json
  3. 20 0
      _locales/en/messages.json
  4. 20 0
      _locales/es/messages.json
  5. 20 0
      _locales/fr/messages.json
  6. 20 0
      _locales/it/messages.json
  7. 20 0
      _locales/ja/messages.json
  8. 20 0
      _locales/pl/messages.json
  9. 20 0
      _locales/pt_PT/messages.json
  10. 20 0
      _locales/pt_br/messages.json
  11. 21 1
      _locales/ru/messages.json
  12. 20 0
      _locales/tr/messages.json
  13. 20 0
      _locales/uk/messages.json
  14. 20 0
      _locales/zh_CN/messages.json
  15. 20 0
      _locales/zh_TW/messages.json
  16. 0 0
      lib/chrome-browser-polyfill.js
  17. 0 0
      lib/single-file-background.js
  18. 0 0
      lib/single-file-bootstrap.js
  19. 0 0
      lib/single-file-extension-background.js
  20. 0 0
      lib/single-file-extension-bootstrap.js
  21. 0 0
      lib/single-file-extension-core.js
  22. 0 0
      lib/single-file-extension-editor-helper.js
  23. 33 1
      lib/single-file-extension-editor-init.js
  24. 0 0
      lib/single-file-extension-frames.js
  25. 0 0
      lib/single-file-extension.js
  26. 0 0
      lib/single-file-frames.js
  27. 0 0
      lib/single-file-hooks-frames.js
  28. 0 0
      lib/single-file-z-worker.js
  29. 0 0
      lib/single-file-zip.js
  30. 0 0
      lib/single-file-zip.min.js
  31. 0 0
      lib/single-file.js
  32. 36 1
      lib/web-stream.js
  33. 1 1
      manifest.json
  34. 1 1
      package.json
  35. 12 2
      src/core/bg/autosave.js
  36. 6 1
      src/core/bg/config.js
  37. 28 2
      src/core/bg/downloads.js
  38. 7 2
      src/core/common/download.js
  39. 2 2
      src/core/content/content.js
  40. 82 0
      src/lib/rest-form-api/index.js
  41. 40 1
      src/ui/bg/ui-options.js
  42. 16 0
      src/ui/pages/help.html
  43. 20 0
      src/ui/pages/options.html

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@
 **/node_modules/
 **/chromedriver.exe
 **/geckodriver.exe
+.idea/

+ 20 - 0
_locales/de/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/en/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/es/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/fr/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/it/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/ja/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/pl/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "Adres URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/pt_PT/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/pt_br/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 21 - 1
_locales/ru/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
-}
+}

+ 20 - 0
_locales/tr/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/uk/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/zh_CN/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "网址",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

+ 20 - 0
_locales/zh_TW/messages.json

@@ -974,5 +974,25 @@
 	"batchSaveUrlsURLTitle": {
 		"message": "網址",
 		"description": "Title of the column in the table of the URLs"
+	},
+	"optionSaveToRestFormApi": {
+		"message": "Upload to a REST Form Api",
+		"description": "Options page label: 'Upload to a REST Form Api'"
+	},
+	"optionRestFormApiUrl": {
+		"message": "Api's URL",
+		"description": "Specify the Url"
+	},
+	"optionRestFormApiToken": {
+		"message": "Authorization Token",
+		"description": "Provide the auth bearer token"
+	},
+	"optionRestFormApiFileFieldName": {
+		"message": "Field Name of File attribute",
+		"description": "Map the file's field name to your Api's file field name"
+	},
+	"optionRestFormApiUrlFieldName": {
+		"message": "Field Name of Url attribute",
+		"description": "Map the url's field name to your Api's url field name"
 	}
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/chrome-browser-polyfill.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-background.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-bootstrap.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension-background.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension-bootstrap.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension-core.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension-editor-helper.js


+ 33 - 1
lib/single-file-extension-editor-init.js

@@ -1 +1,33 @@
-!function(){"use strict";document.currentScript.remove(),function e(t){t.querySelectorAll("template[shadowrootmode]").forEach((t=>{let o=t.parentElement.shadowRoot;if(!o){try{o=t.parentElement.attachShadow({mode:t.getAttribute("shadowrootmode")}),o.innerHTML=t.innerHTML,t.remove()}catch(e){}o&&e(o)}}))}(document)}();
+(function () {
+	'use strict';
+
+	/* global document */
+
+	(() => {
+
+		document.currentScript.remove();
+		processNode(document);
+
+		function processNode(node) {
+			node.querySelectorAll("template[shadowrootmode]").forEach(element => {
+				let shadowRoot = element.parentElement.shadowRoot;
+				if (!shadowRoot) {
+					try {
+						shadowRoot = element.parentElement.attachShadow({
+							mode: element.getAttribute("shadowrootmode")
+						});
+						shadowRoot.innerHTML = element.innerHTML;
+						element.remove();
+					} catch (error) {
+						// ignored
+					}
+					if (shadowRoot) {
+						processNode(shadowRoot);
+					}
+				}
+			});
+		}
+
+	})();
+
+})();

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension-frames.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-extension.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-frames.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-hooks-frames.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-z-worker.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-zip.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file-zip.min.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/single-file.js


+ 36 - 1
lib/web-stream.js

@@ -1 +1,36 @@
-!function(){"use strict";void 0===globalThis.TransformStream&&(globalThis.TransformStream=class{}),void 0===globalThis.WritableStream&&(globalThis.WritableStream=class{})}();
+(function () {
+	'use strict';
+
+	/*
+	 * Copyright 2010-2020 Gildas Lormeau
+	 * contact : gildas.lormeau <at> gmail.com
+	 * 
+	 * This file is part of SingleFile.
+	 *
+	 *   The code in this file is free software: you can redistribute it and/or 
+	 *   modify it under the terms of the GNU Affero General Public License 
+	 *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+	 *   of the License, or (at your option) any later version.
+	 * 
+	 *   The code in this file is distributed in the hope that it will be useful, 
+	 *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+	 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+	 *   General Public License for more details.
+	 *
+	 *   As additional permission under GNU AGPL version 3 section 7, you may 
+	 *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+	 *   AGPL normally required by section 4, provided you include this license 
+	 *   notice and a URL through which recipients can access the Corresponding 
+	 *   Source.
+	 */
+
+	/* global globalThis */
+
+	if (typeof globalThis.TransformStream === "undefined") {
+		globalThis.TransformStream = class TransformStream { };
+	}
+	if (typeof globalThis.WritableStream === "undefined") {
+		globalThis.WritableStream = class WritableStream { };
+	}
+
+})();

+ 1 - 1
manifest.json

@@ -8,7 +8,7 @@
 		"64": "src/ui/resources/icon_64.png",
 		"128": "src/ui/resources/icon_128.png"
 	},
-	"version": "1.22.47",
+	"version": "1.22.48",
 	"description": "__MSG_extensionDescription__",
 	"content_scripts": [
 		{

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "single-file",
-	"version": "1.2.2",
+	"version": "1.2.3",
 	"description": "SingleFile",
 	"author": "Gildas Lormeau",
 	"license": "AGPL-3.0-or-later",

+ 12 - 2
src/core/bg/autosave.js

@@ -151,7 +151,7 @@ async function saveContent(message, tab) {
 		options.incognito = tab.incognito;
 		options.tabId = tabId;
 		options.tabIndex = tab.index;
-		options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox;
+		options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi;
 		let pageData;
 		try {
 			if (options.autoSaveExternalSave) {
@@ -163,7 +163,7 @@ async function saveContent(message, tab) {
 				options.tabId = tabId;
 				pageData = await getPageData(options, null, null, { fetch });
 				let skipped;
-				if (!options.saveToGDrive && !options.saveWithWebDAV && !options.saveToGitHub && !options.saveToDropbox && !options.saveWithCompanion) {
+				if (!options.saveToGDrive && !options.saveWithWebDAV && !options.saveToGitHub && !options.saveToDropbox && !options.saveWithCompanion && !options.saveToRestFormApi) {
 					const testSkip = await downloads.testSkipSave(pageData.filename, options);
 					skipped = testSkip.skipped;
 					options.filenameConflictAction = testSkip.filenameConflictAction;
@@ -203,6 +203,16 @@ async function saveContent(message, tab) {
 							content: pageData.content,
 							filenameConflictAction: options.filenameConflictAction
 						});
+					} else if (options.saveToRestFormApi) {
+						await downloads.saveToRestFormApi(
+							message.taskId,
+							content,
+							pageData.url,
+							options.restFormApiToken,
+							options.restFormApiUrl,
+							options.restFormApiFileFieldName,
+							options.restFormApiUrlFieldName
+						);
 					} else {
 						if (!(content instanceof Blob)) {
 							content = new Blob([content], { type });

+ 6 - 1
src/core/bg/config.js

@@ -109,6 +109,7 @@ const DEFAULT_CONFIG = {
 	webDAVUser: "",
 	webDAVPassword: "",
 	saveToGitHub: false,
+	saveToRestFormApi: false,
 	githubToken: "",
 	githubUser: "",
 	githubRepository: "SingleFile-Archives",
@@ -165,7 +166,11 @@ const DEFAULT_CONFIG = {
 	blockVideos: true,
 	blockAudios: true,
 	delayBeforeProcessing: 0,
-	_migratedTemplateFormat: true
+	_migratedTemplateFormat: true,
+	saveToRestFormApiUrl: "",
+	saveToRestFormApiFileFieldName: "",
+	saveToRestFormApiUrlFieldName: "",
+	saveToRestFormApiToken: "",
 };
 
 const DEFAULT_RULES = [{

+ 28 - 2
src/core/bg/downloads.js

@@ -37,6 +37,7 @@ import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { GitHub } from "./../../lib/github/github.js";
 import { download } from "./download-util.js";
 import * as yabson from "./../../lib/yabson/yabson.js";
+import {RestFormApi} from "../../lib/../lib/rest-form-api/index";
 
 const partialContents = new Map();
 const tabData = new Map();
@@ -65,6 +66,7 @@ export {
 	saveToGitHub,
 	saveToDropbox,
 	saveWithWebDAV,
+	saveToRestFormApi,
 	encodeSharpCharacter
 };
 
@@ -166,7 +168,7 @@ async function downloadContent(contents, tab, incognito, message) {
 	const tabId = tab.id;
 	try {
 		let skipped;
-		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub) {
+		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub && !message.saveToRestFormApi) {
 			const testSkip = await testSkipSave(message.filename, message);
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			skipped = testSkip.skipped;
@@ -210,6 +212,16 @@ async function downloadContent(contents, tab, incognito, message) {
 					content: message.content,
 					filenameConflictAction: message.filenameConflictAction
 				});
+			} else if (message.saveToRestFormApi) {
+				response = await saveToRestFormApi(
+					message.taskId,
+					contents.join(""),
+					tab.url,
+					message.saveToRestFormApiToken,
+					message.saveToRestFormApiUrl,
+					message.saveToRestFormApiFileFieldName,
+					message.saveToRestFormApiUrlFieldName
+				);
 			} else {
 				message.url = URL.createObjectURL(new Blob(contents, { type: message.mimeType }));
 				response = await downloadPage(message, {
@@ -546,6 +558,19 @@ function saveToClipboard(pageData) {
 	}
 }
 
+async function saveToRestFormApi(taskId, content, url, token, restApiUrl, fileFieldName, urlFieldName) {
+	try {
+		const taskInfo = business.getTaskInfo(taskId);
+		if (!taskInfo || !taskInfo.cancelled) {
+			const client = new RestFormApi(token, restApiUrl, fileFieldName, urlFieldName);
+			business.setCancelCallback(taskId, () => client.abort());
+			return await client.upload(content, url);
+		}
+	} catch (error) {
+		throw new Error(error.message + " (RestFormApi)");
+	}
+}
+
 async function downloadPageForeground(taskId, filename, content, mimeType, tabId, { foregroundSave, sharePage }) {
 	const serializer = yabson.getSerializer({
 		filename,
@@ -562,4 +587,5 @@ async function downloadPageForeground(taskId, filename, content, mimeType, tabId
 		});
 	}
 	return browser.tabs.sendMessage(tabId, { method: "content.download" });
-}
+}
+

+ 7 - 2
src/core/common/download.js

@@ -101,7 +101,12 @@ async function downloadPage(pageData, options) {
 		password: options.password,
 		compressContent: options.compressContent,
 		foregroundSave: options.foregroundSave,
-		sharePage: options.sharePage
+		sharePage: options.sharePage,
+		saveToRestFormApi: options.saveToRestFormApi,
+		saveToRestFormApiUrl: options.saveToRestFormApiUrl,
+		saveToRestFormApiFileFieldName: options.saveToRestFormApiFileFieldName,
+		saveToRestFormApiUrlFieldName: options.saveToRestFormApiUrlFieldName,
+		saveToRestFormApiToken: options.saveToRestFormApiToken,
 	};
 	if (options.compressContent) {
 		const blob = new Blob([await yabson.serialize(pageData)], { type: pageData.mimeType });
@@ -134,7 +139,7 @@ async function downloadPage(pageData, options) {
 			await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
 		}
 	} else {
-		if ((options.backgroundSave && !options.sharePage) || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) {
+		if ((options.backgroundSave && !options.sharePage) || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi) {
 			const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: pageData.mimeType }));
 			message.blobURL = blobURL;
 			const result = await browser.runtime.sendMessage(message);

+ 2 - 2
src/core/content/content.js

@@ -168,7 +168,7 @@ async function savePage(message) {
 			try {
 				const pageData = await processPage(options);
 				if (pageData) {
-					if (((!options.backgroundSave && !options.saveToClipboard) || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) && options.confirmFilename) {
+					if (((!options.backgroundSave && !options.saveToClipboard) || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi) && options.confirmFilename) {
 						pageData.filename = ui.prompt("Save as", pageData.filename) || pageData.filename;
 					}
 					await download.downloadPage(pageData, options);
@@ -194,7 +194,7 @@ async function savePage(message) {
 async function processPage(options) {
 	const frames = singlefile.processors.frameTree;
 	let framesSessionId;
-	options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox;
+	options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi;
 	singlefile.helper.initDoc(document);
 	ui.onStartPage(options);
 	processor = new singlefile.SingleFile(options);

+ 82 - 0
src/lib/rest-form-api/index.js

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2024 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * author: gildas.lormeau <at> gmail.com
+ * author: dcardin2007 <at> gmail.com
+ *
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or
+ *   modify it under the terms of the GNU Affero General Public License
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ *
+ *   The code in this file is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
+ *   AGPL normally required by section 4, provided you include this license
+ *   notice and a URL through which recipients can access the Corresponding
+ *   Source.
+ */
+
+/* global fetch, btoa, Blob, FileReader, AbortController */
+
+const AUTHORIZATION_HEADER = "Authorization";
+const BEARER_PREFIX_AUTHORIZATION = "Bearer ";
+const ACCEPT_HEADER = "Accept";
+const CONTENT_TYPE = "multipart/form-data";
+
+export { RestFormApi };
+
+class RestFormApi {
+	constructor(token, restApiUrl, fileFieldName, urlFieldName) {
+		this.headers = new Map([
+			[AUTHORIZATION_HEADER, BEARER_PREFIX_AUTHORIZATION + token],
+			[ACCEPT_HEADER, CONTENT_TYPE]
+		]);
+		this.restApiUrl = restApiUrl;
+		this.fileFieldName = fileFieldName;
+		this.urlFieldName = urlFieldName;
+	}
+
+	async upload(content, url) {
+
+		this.controller = new AbortController();
+		try{
+			const blob = new Blob([content], { type: 'text/html'})
+			const file = new File([blob], "SingleFile.html", { type: 'text/html' });
+			let formData = new FormData();
+			if(this.fileFieldName){
+				formData.append(this.fileFieldName, file)
+			}
+			if(this.urlFieldName){
+				formData.append(this.urlFieldName, url)
+			}
+			const response = await fetch(this.restApiUrl, {
+				method: 'POST',
+				body: formData,
+				headers: this.headers,
+				signal: this.controller.signal
+			});
+			if ([200,201].includes(response.status)) {
+				// do something with the data?
+				const data = await response.json();
+			} else {
+				throw new Error(await response.text());
+			}
+		}
+		catch(e){
+			throw new Error(e);
+		}
+	}
+
+	abort() {
+		if (this.controller) {
+			this.controller.abort();
+		}
+	}
+}

+ 40 - 1
src/ui/bg/ui-options.js

@@ -331,6 +331,20 @@ const promptCancelButton = document.getElementById("promptCancelButton");
 const promptConfirmButton = document.getElementById("promptConfirmButton");
 const manifest = browser.runtime.getManifest();
 const requestPermissionIdentity = manifest.optional_permissions && manifest.optional_permissions.includes("identity");
+/**
+ * Save to Rest Form Api functionality
+ */
+const saveToRestFormApiLabel = document.getElementById("saveToRestFormApiLabel");
+const saveToRestFormApiUrlLabel = document.getElementById("saveToRestFormApiUrlLabel");
+const saveToRestFormApiFileFieldNameLabel = document.getElementById("saveToRestFormApiFileFieldNameLabel");
+const saveToRestFormApiUrlFieldNameLabel = document.getElementById("saveToRestFormApiUrlFieldNameLabel");
+const saveToRestFormApiTokenLabel = document.getElementById("saveToRestFormApiTokenLabel");
+const saveToRestFormApiInput = document.getElementById("saveToRestFormApiInput");
+const saveToRestFormApiUrlInput = document.getElementById("saveToRestFormApiUrlInput");
+const saveToRestFormApiFileFieldNameInput = document.getElementById("saveToRestFormApiFileFieldNameInput");
+const saveToRestFormApiUrlFieldNameInput = document.getElementById("saveToRestFormApiUrlFieldNameInput");
+const saveToRestFormApiTokenInput = document.getElementById("saveToRestFormApiTokenInput");
+
 
 let sidePanelDisplay;
 if (location.href.endsWith("#side-panel")) {
@@ -536,6 +550,7 @@ saveWithCompanionInput.addEventListener("click", () => disableDestinationPermiss
 saveToGDriveInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], false), false);
 saveToDropboxInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], true, false), false);
 saveWithWebDAVInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
+saveToRestFormApiInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
 sharePageInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
 saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
 passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
@@ -767,6 +782,13 @@ resetCurrentButton.textContent = browser.i18n.getMessage("optionsResetCurrentBut
 resetCancelButton.textContent = promptCancelButton.textContent = cancelButton.textContent = browser.i18n.getMessage("optionsCancelButton");
 confirmButton.textContent = promptConfirmButton.textContent = browser.i18n.getMessage("optionsOKButton");
 document.getElementById("resetConfirmLabel").textContent = browser.i18n.getMessage("optionsResetConfirm");
+saveToRestFormApiLabel.textContent = browser.i18n.getMessage("optionSaveToRestFormApi");
+saveToRestFormApiUrlLabel.textContent = browser.i18n.getMessage("optionRestFormApiUrl");
+saveToRestFormApiFileFieldNameLabel.textContent = browser.i18n.getMessage("optionRestFormApiFileFieldName");
+saveToRestFormApiUrlFieldNameLabel.textContent = browser.i18n.getMessage("optionRestFormApiUrlFieldName");
+saveToRestFormApiTokenLabel.textContent = browser.i18n.getMessage("optionRestFormApiToken");
+
+
 if (location.href.endsWith("#")) {
 	document.querySelector(".new-window-link").remove();
 	document.documentElement.classList.add("maximized");
@@ -1038,6 +1060,18 @@ async function refresh(profileName) {
 	applySystemThemeInput.checked = profileOptions.applySystemTheme;
 	warnUnsavedPageInput.checked = profileOptions.warnUnsavedPage;
 	displayInfobarInEditorInput.checked = profileOptions.displayInfobarInEditor;
+	/**
+	 *	Rest Form Api Feature
+	 */
+    saveToRestFormApiInput.checked = profileOptions.saveToRestFormApi;
+    saveToRestFormApiUrlInput.value = profileOptions.saveToRestFormApiUrl;
+    saveToRestFormApiUrlInput.disabled = !profileOptions.saveToRestFormApi;
+    saveToRestFormApiTokenInput.value = profileOptions.saveToRestFormApiToken;
+    saveToRestFormApiTokenInput.disabled = !profileOptions.saveToRestFormApi;
+    saveToRestFormApiFileFieldNameInput.value = profileOptions.saveToRestFormApiFileFieldName;
+    saveToRestFormApiFileFieldNameInput.disabled = !profileOptions.saveToRestFormApi;
+    saveToRestFormApiUrlFieldNameInput.value = profileOptions.saveToRestFormApiUrlFieldName;
+    saveToRestFormApiUrlFieldNameInput.disabled = !profileOptions.saveToRestFormApi;
 }
 
 function getProfileText(profileName) {
@@ -1155,7 +1189,12 @@ async function update() {
 			defaultEditorMode: defaultEditorModeInput.value,
 			applySystemTheme: applySystemThemeInput.checked,
 			warnUnsavedPage: warnUnsavedPageInput.checked,
-			displayInfobarInEditor: displayInfobarInEditorInput.checked
+			displayInfobarInEditor: displayInfobarInEditorInput.checked,
+			saveToRestFormApi: saveToRestFormApiInput.checked,
+			saveToRestFormApiUrl: saveToRestFormApiUrlInput.value,
+			saveToRestFormApiToken: saveToRestFormApiTokenInput.value,
+			saveToRestFormApiFileFieldName: saveToRestFormApiFileFieldNameInput.value,
+			saveToRestFormApiUrlFieldName: saveToRestFormApiUrlFieldNameInput.value,
 		}
 	});
 	try {

+ 16 - 0
src/ui/pages/help.html

@@ -526,6 +526,22 @@
 								target="_blank">here</a></p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
+					<li data-options-label="saveToRestFormApiLabel"> <span class="option">Option: POST the content as a file to an Api endpoint</span>
+						<p>Check this option to save the page on an Api endpoint.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
+					<li data-options-label="saveToRestFormApiUrlLabel"> <span class="option">Option: URL</span>
+						<p>Enter the URL of the Api endpoint.</p>
+					</li>
+					<li data-options-label="saveToRestFormApiTokenLabel"> <span class="option">Option: user auth token</span>
+						<p>Provide the Authorization bearer token</p>
+					</li>
+					<li data-options-label="saveToRestFormApiFileFieldNameLabel"> <span class="option">Option: mapped file field name</span>
+						<p>Map the name of the file field that is posted to the endpoint</p>
+					</li>
+					<li data-options-label="saveToRestFormApiUrlFieldNameLabel"> <span class="option">Option: mapped url field name</span>
+						<p>Map the name of the url field that is to the endpoint</p>
+					</li>
 				</ul>
 				<p>Network</p>
 				<ul>

+ 20 - 0
src/ui/pages/options.html

@@ -304,6 +304,26 @@
 				<label for="saveWithCompanionInput" id="saveWithCompanionLabel"></label>
 				<input type="radio" id="saveWithCompanionInput" name="destinationInput">
 			</div>
+			<div class="option">
+				<label for="saveToRestFormApiInput" id="saveToRestFormApiLabel"></label>
+				<input type="radio" id="saveToRestFormApiInput" name="destinationInput">
+			</div>
+			<div class="option second-level">
+				<label for="saveToRestFormApiUrlInput" id="saveToRestFormApiUrlLabel"></label>
+				<input type="text" id="saveToRestFormApiUrlInput" class="medium-input">
+			</div>
+			<div class="option second-level">
+				<label for="saveToRestFormApiTokenInput" id="saveToRestFormApiTokenLabel"></label>
+				<input type="password" id="saveToRestFormApiTokenInput" class="medium-input">
+			</div>
+			<div class="option second-level">
+				<label for="saveToRestFormApiFileFieldNameInput" id="saveToRestFormApiFileFieldNameLabel"></label>
+				<input type="input" id="saveToRestFormApiFileFieldNameInput" class="medium-input">
+			</div>
+			<div class="option second-level">
+				<label for="saveToRestFormApiUrlFieldNameInput" id="saveToRestFormApiUrlFieldNameLabel"></label>
+				<input type="input" id="saveToRestFormApiUrlFieldNameInput" class="medium-input">
+			</div>
 		</details>
 		<details>
 			<summary id="networkLabel"></summary>

Некоторые файлы не были показаны из-за большого количества измененных файлов