Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mod-box",
"private": true,
"version": "0.2.2",
"version": "0.2.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "ModBox – Modify headers, block requests",
"short_name": "ModBox",
"description": "Modify HTTP headers, block assets or domains & redirect requests. Organise rules by folder and tab, optionally scope to domains.",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"email": "robertjamesphillips@googlemail.com"
},
Expand Down
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function toggleGlobalActive() {
}

function reset() {
data.value = { ...fallbackData };
data.value = structuredClone(fallbackData);
}

function importFolders(folders: FolderType[]) {
Expand Down
99 changes: 79 additions & 20 deletions src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
</label>
</div>

<div v-if="exportError.length > 0" class="error mb20">
{{ exportError }}
</div>

<div class="panel__actions">
<button
class="btn"
Expand Down Expand Up @@ -222,39 +226,94 @@ const fileupload: Ref<HTMLFormElement | null> = ref(null);
const selectedExport: Ref<number[]> = ref([]);
const selectedImport: Ref<number[]> = ref([]);
const importData: Ref<FolderType[]> = ref([]);
const exportError = ref("");
const importError = ref("");
const showDeleteConfirmation = ref(false);

/**
* Export functionality
*
* Uses a DOM anchor-click approach to trigger a file download.
* Hardened with:
* - try/catch for error resilience
* - URL.revokeObjectURL cleanup to prevent memory leaks
* - Data size guard to avoid allocating excessive Blobs
* - Deferred click via setTimeout for popup-lifecycle stability on Linux
*/
const MAX_EXPORT_SIZE = 50_000_000; // 50 MB safety threshold

function download() {
const exportData: FolderType[] = [];
if (!selectedExport.value.length) return;

exportError.value = "";
let url: string | null = null;
let anchor: HTMLAnchorElement | null = null;

try {
const exportData: FolderType[] = [];

props.folders.forEach((folder: FolderType, index: number) => {
if (selectedExport.value.includes(index)) {
exportData.push(folder);
}
});

const payload = {
name: "modbox",
version: 1,
folders: exportData,
};

props.folders.forEach((folder: FolderType, index: number) => {
if (selectedExport.value.includes(index)) {
exportData.push(folder);
const json = JSON.stringify(payload);

if (json.length > MAX_EXPORT_SIZE) {
exportError.value = "Export data is too large. Try selecting fewer folders.";
return;
}
});

const payload = {
name: "modbox",
version: 1,
folders: exportData,
};
const blob = new Blob([json], { type: "application/json" });
url = URL.createObjectURL(blob);
anchor = document.createElement("a");

anchor.href = url;
anchor.download = `modbox-export-${Math.floor(Date.now() / 1000)}.json`;
document.body.appendChild(anchor);

// Defer the click to the next frame so the browser has time to fully
// attach the anchor. This improves reliability in Chrome extension
// popups, especially on Linux where popup teardown can be aggressive.
const currentUrl = url;
const currentAnchor = anchor;

setTimeout(() => {
try {
currentAnchor.click();
} catch {
// Click may fail if popup is closing — non-fatal
} finally {
if (currentAnchor.parentNode) {
document.body.removeChild(currentAnchor);
}
}

const json = JSON.stringify(payload);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
// Give the browser time to initiate the download before revoking
setTimeout(() => {
URL.revokeObjectURL(currentUrl);
}, 3000);
}, 0);

anchor.href = url;
anchor.download = `modbox-export-${Math.floor(Date.now() / 1000)}.json`;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
selectedExport.value = [];
} catch (e) {
exportError.value = "Failed to export rules. Please try again.";

selectedExport.value = [];
// Cleanup on error
if (anchor?.parentNode) {
document.body.removeChild(anchor);
}
if (url) {
URL.revokeObjectURL(url);
}
}
}

/**
Expand Down
Loading