browser-commander 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd-baseline.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +7 -4
- package/package.json +4 -1
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +610 -25
- package/src/browser/launcher.js +14 -0
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +18 -1
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-launch.js +42 -0
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/safari-support.js +98 -0
- package/src/browser/storage-state.js +28 -8
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +64 -19
- package/src/cli/args.js +2 -0
- package/src/cli/commands.js +2 -0
- package/src/cli/high-level-methods.js +2 -0
- package/src/cli/sessions.js +5 -0
- package/src/core/webdriver-page.js +62 -0
- package/src/exports.js +6 -0
- package/src/traces/recorder.js +5 -0
- package/tests/e2e/safari.e2e.test.js +104 -0
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/browser/safari.test.js +192 -0
- package/tests/unit/browser/webdriver.test.js +48 -3
- package/tests/unit/cli/dispatcher.test.js +5 -1
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
|
@@ -3,6 +3,156 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathExists } from './fs-utils.js';
|
|
4
4
|
|
|
5
5
|
import { withDatabaseSnapshot } from './sqlite-snapshot.js';
|
|
6
|
+
import { matchesDomains } from './domains.js';
|
|
7
|
+
import { openSqliteDatabase } from '../browser-cookie-database.js';
|
|
8
|
+
|
|
9
|
+
async function filterHistory(targetPath, domains, warnings) {
|
|
10
|
+
if (!domains?.length) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const db = await openSqliteDatabase(targetPath);
|
|
14
|
+
try {
|
|
15
|
+
const tables = new Set(
|
|
16
|
+
db
|
|
17
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
18
|
+
.all()
|
|
19
|
+
.map((row) => row.name)
|
|
20
|
+
);
|
|
21
|
+
if (tables.has('urls')) {
|
|
22
|
+
const remove = db.prepare('DELETE FROM urls WHERE id = ?');
|
|
23
|
+
for (const row of db.prepare('SELECT id,url FROM urls').all()) {
|
|
24
|
+
if (!matchesDomains(row.url, domains)) {
|
|
25
|
+
remove.run(row.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const [table, column] of [
|
|
29
|
+
['visits', 'url'],
|
|
30
|
+
['segments', 'url_id'],
|
|
31
|
+
['keyword_search_terms', 'url_id'],
|
|
32
|
+
]) {
|
|
33
|
+
if (tables.has(table)) {
|
|
34
|
+
db.exec(
|
|
35
|
+
`DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM urls)`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (tables.has('visits')) {
|
|
40
|
+
for (const [table, column] of [
|
|
41
|
+
['visit_source', 'id'],
|
|
42
|
+
['content_annotations', 'visit_id'],
|
|
43
|
+
['context_annotations', 'visit_id'],
|
|
44
|
+
]) {
|
|
45
|
+
if (tables.has(table)) {
|
|
46
|
+
db.exec(
|
|
47
|
+
`DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM visits)`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (tables.has('segments') && tables.has('segment_usage')) {
|
|
53
|
+
db.exec(
|
|
54
|
+
'DELETE FROM segment_usage WHERE segment_id NOT IN (SELECT id FROM segments)'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
filterDownloads(db, tables, domains);
|
|
59
|
+
if (tables.has('top_sites')) {
|
|
60
|
+
const remove = db.prepare('DELETE FROM top_sites WHERE url = ?');
|
|
61
|
+
for (const row of db.prepare('SELECT url FROM top_sites').all()) {
|
|
62
|
+
if (!matchesDomains(row.url, domains)) {
|
|
63
|
+
remove.run(row.url);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
omitUnfilteredMetadata(db, tables, warnings);
|
|
68
|
+
db.exec('VACUUM');
|
|
69
|
+
} finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function omitUnfilteredMetadata(db, tables, warnings) {
|
|
75
|
+
const filtered = new Set([
|
|
76
|
+
'urls',
|
|
77
|
+
'downloads',
|
|
78
|
+
'top_sites',
|
|
79
|
+
'meta',
|
|
80
|
+
'sqlite_sequence',
|
|
81
|
+
]);
|
|
82
|
+
if (tables.has('urls')) {
|
|
83
|
+
for (const table of ['visits', 'segments', 'keyword_search_terms']) {
|
|
84
|
+
filtered.add(table);
|
|
85
|
+
}
|
|
86
|
+
if (tables.has('visits')) {
|
|
87
|
+
for (const table of [
|
|
88
|
+
'visit_source',
|
|
89
|
+
'content_annotations',
|
|
90
|
+
'context_annotations',
|
|
91
|
+
]) {
|
|
92
|
+
filtered.add(table);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (tables.has('segments')) {
|
|
96
|
+
filtered.add('segment_usage');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (tables.has('downloads')) {
|
|
100
|
+
filtered.add('downloads_url_chains');
|
|
101
|
+
filtered.add('downloads_slices');
|
|
102
|
+
}
|
|
103
|
+
// Cluster labels and keywords can describe excluded visits even when a
|
|
104
|
+
// cluster also contains selected visits. Unknown schemas have no safe join.
|
|
105
|
+
for (const table of [...tables].sort()) {
|
|
106
|
+
if (filtered.has(table)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const identifier = `"${table.replaceAll('"', '""')}"`;
|
|
110
|
+
const count = db.prepare(`SELECT COUNT(*) AS c FROM ${identifier}`).get().c;
|
|
111
|
+
db.exec(`DELETE FROM ${identifier}`);
|
|
112
|
+
if (count > 0) {
|
|
113
|
+
warnings.push({
|
|
114
|
+
type: 'history',
|
|
115
|
+
item: table,
|
|
116
|
+
reason: 'unsupported-history-metadata',
|
|
117
|
+
detail: `${count} copied metadata rows removed`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function filterDownloads(db, tables, domains) {
|
|
124
|
+
if (!tables.has('downloads')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const remove = db.prepare('DELETE FROM downloads WHERE id = ?');
|
|
128
|
+
for (const row of db.prepare('SELECT * FROM downloads').all()) {
|
|
129
|
+
const urls = ['url', 'site_url', 'tab_url', 'referrer', 'tab_referrer_url']
|
|
130
|
+
.map((column) => row[column])
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
if (tables.has('downloads_url_chains')) {
|
|
133
|
+
urls.push(
|
|
134
|
+
...db
|
|
135
|
+
.prepare('SELECT url FROM downloads_url_chains WHERE id = ?')
|
|
136
|
+
.all(row.id)
|
|
137
|
+
.map((chain) => chain.url)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
// A redirect outside the selected sites is still unrelated browsing data.
|
|
141
|
+
if (!urls.length || urls.some((url) => !matchesDomains(url, domains))) {
|
|
142
|
+
remove.run(row.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const [table, column] of [
|
|
146
|
+
['downloads_url_chains', 'id'],
|
|
147
|
+
['downloads_slices', 'download_id'],
|
|
148
|
+
]) {
|
|
149
|
+
if (tables.has(table)) {
|
|
150
|
+
db.exec(
|
|
151
|
+
`DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM downloads)`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
6
156
|
|
|
7
157
|
/**
|
|
8
158
|
* History and Top Sites migration.
|
|
@@ -20,12 +170,13 @@ import { withDatabaseSnapshot } from './sqlite-snapshot.js';
|
|
|
20
170
|
* already guarantees a point-in-time consistent copy.
|
|
21
171
|
*/
|
|
22
172
|
|
|
23
|
-
function snapshotInto({ sourcePath, targetPath }) {
|
|
173
|
+
function snapshotInto({ sourcePath, targetPath, domains, warnings }) {
|
|
24
174
|
return withDatabaseSnapshot({
|
|
25
175
|
sourcePath,
|
|
26
176
|
read: async (snapshotPath) => {
|
|
27
177
|
const { copyFile } = await import('node:fs/promises');
|
|
28
178
|
await copyFile(snapshotPath, targetPath);
|
|
179
|
+
await filterHistory(targetPath, domains, warnings);
|
|
29
180
|
// Best-effort URL count for the report.
|
|
30
181
|
try {
|
|
31
182
|
const { openSqliteDatabase } =
|
|
@@ -52,7 +203,11 @@ function snapshotInto({ sourcePath, targetPath }) {
|
|
|
52
203
|
* @param {string} options.targetProfileDir
|
|
53
204
|
* @returns {Promise<{migrated: number, skipped: Array, warnings: Array}>}
|
|
54
205
|
*/
|
|
55
|
-
export async function migrateHistory({
|
|
206
|
+
export async function migrateHistory({
|
|
207
|
+
sourceProfileDir,
|
|
208
|
+
targetProfileDir,
|
|
209
|
+
domains,
|
|
210
|
+
}) {
|
|
56
211
|
const skipped = [];
|
|
57
212
|
const warnings = [];
|
|
58
213
|
await mkdir(targetProfileDir, { recursive: true });
|
|
@@ -64,6 +219,8 @@ export async function migrateHistory({ sourceProfileDir, targetProfileDir }) {
|
|
|
64
219
|
const count = await snapshotInto({
|
|
65
220
|
sourcePath: historySource,
|
|
66
221
|
targetPath: path.join(targetProfileDir, 'History'),
|
|
222
|
+
domains,
|
|
223
|
+
warnings,
|
|
67
224
|
});
|
|
68
225
|
urlCount = count ?? 0;
|
|
69
226
|
migratedDatabases += 1;
|
|
@@ -80,6 +237,8 @@ export async function migrateHistory({ sourceProfileDir, targetProfileDir }) {
|
|
|
80
237
|
await snapshotInto({
|
|
81
238
|
sourcePath: topSitesSource,
|
|
82
239
|
targetPath: path.join(targetProfileDir, 'Top Sites'),
|
|
240
|
+
domains,
|
|
241
|
+
warnings,
|
|
83
242
|
});
|
|
84
243
|
migratedDatabases += 1;
|
|
85
244
|
}
|
|
@@ -7,15 +7,25 @@ import {
|
|
|
7
7
|
browserProfileRoot,
|
|
8
8
|
resolveBrowserProfile,
|
|
9
9
|
} from '../browser-profiles.js';
|
|
10
|
+
import { migrateSafariClass } from './safari-import.js';
|
|
11
|
+
import {
|
|
12
|
+
validateMigrationOptions,
|
|
13
|
+
validateMigrationPaths,
|
|
14
|
+
} from './validation.js';
|
|
10
15
|
import { migrateBookmarks } from './bookmarks.js';
|
|
16
|
+
import { pathExists } from './fs-utils.js';
|
|
11
17
|
import { migrateCookies } from './cookies.js';
|
|
18
|
+
import {
|
|
19
|
+
ADDITIONAL_DATA_CLASSES,
|
|
20
|
+
reportAdditionalClasses,
|
|
21
|
+
} from './data-classes.js';
|
|
12
22
|
import { migrateExtensions } from './extensions.js';
|
|
13
23
|
import {
|
|
14
24
|
migrateFirefoxBookmarks,
|
|
15
25
|
migrateFirefoxPasswords,
|
|
16
26
|
readFirefoxCookies,
|
|
17
|
-
reportFirefoxHistory,
|
|
18
27
|
} from './firefox.js';
|
|
28
|
+
import { migrateFirefoxHistory } from './firefox-history.js';
|
|
19
29
|
import { migrateHistory } from './history.js';
|
|
20
30
|
import { migratePasswords } from './passwords.js';
|
|
21
31
|
import { migratePreferences } from './preferences.js';
|
|
@@ -36,8 +46,8 @@ import {
|
|
|
36
46
|
*
|
|
37
47
|
* Cookies are returned (not written): a running Chromium re-derives its own
|
|
38
48
|
* cookie encryption, so the launcher seeds them over CDP with the existing
|
|
39
|
-
* `seedCookies` path.
|
|
40
|
-
*
|
|
49
|
+
* `seedCookies` path. Supported file imports are written into the target profile;
|
|
50
|
+
* classes without a native writer receive explicit skipped reports.
|
|
41
51
|
*/
|
|
42
52
|
|
|
43
53
|
export const ALL_DATA_CLASSES = Object.freeze([
|
|
@@ -47,6 +57,7 @@ export const ALL_DATA_CLASSES = Object.freeze([
|
|
|
47
57
|
'passwords',
|
|
48
58
|
'preferences',
|
|
49
59
|
'extensions',
|
|
60
|
+
...ADDITIONAL_DATA_CLASSES,
|
|
50
61
|
]);
|
|
51
62
|
|
|
52
63
|
// Classification is driven by the shared catalogue so every Chromium variant
|
|
@@ -62,14 +73,7 @@ function isFirefoxBrowser(browser) {
|
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
function emptyMigrated() {
|
|
65
|
-
return
|
|
66
|
-
cookies: 0,
|
|
67
|
-
bookmarks: 0,
|
|
68
|
-
history: 0,
|
|
69
|
-
passwords: 0,
|
|
70
|
-
preferences: 0,
|
|
71
|
-
extensions: 0,
|
|
72
|
-
};
|
|
76
|
+
return Object.fromEntries(ALL_DATA_CLASSES.map((type) => [type, 0]));
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
async function resolveSourceProfileDir({
|
|
@@ -114,34 +118,6 @@ function mergeReport(report, className, fragment) {
|
|
|
114
118
|
report.warnings.push(...(fragment.warnings ?? []));
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
function reportSafariClasses(report, selected) {
|
|
118
|
-
for (const type of ALL_DATA_CLASSES.filter(
|
|
119
|
-
(name) => name !== 'cookies' && selected.has(name)
|
|
120
|
-
)) {
|
|
121
|
-
report.skipped.push({
|
|
122
|
-
type,
|
|
123
|
-
item: report.source.browser,
|
|
124
|
-
reason:
|
|
125
|
-
type === 'passwords'
|
|
126
|
-
? 'safari-password-export-required'
|
|
127
|
-
: 'safari-class-not-supported',
|
|
128
|
-
detail:
|
|
129
|
-
type === 'passwords'
|
|
130
|
-
? 'Safari passwords live in the Keychain. Export Passwords from Safari or the Passwords app to CSV; CSV import is tracked separately and is not supported yet.'
|
|
131
|
-
: 'Safari currently supports cookie import only; this data class has not been translated.',
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (report.cookies.length > 0) {
|
|
135
|
-
report.warnings.push({
|
|
136
|
-
type: 'cookies',
|
|
137
|
-
item: report.source.browser,
|
|
138
|
-
reason: 'safari-samesite-unavailable',
|
|
139
|
-
detail:
|
|
140
|
-
'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
121
|
async function resolvePasswordKeys({
|
|
146
122
|
keys,
|
|
147
123
|
browser,
|
|
@@ -179,7 +155,8 @@ async function resolvePasswordKeys({
|
|
|
179
155
|
* @param {{browser: string, profile: (string|undefined), userDataDir: (string|undefined)}} options.from
|
|
180
156
|
* @param {string} options.to - Target profile directory
|
|
181
157
|
* @param {string[]} [options.include=ALL_DATA_CLASSES]
|
|
182
|
-
* @param {string[]} [options.domains] -
|
|
158
|
+
* @param {string[]} [options.domains] - Per-site host/subdomain filter
|
|
159
|
+
* @param {boolean} [options.includePaymentCards=false] - Separate explicit payment-card consent
|
|
183
160
|
* @param {string} [options.platform=process.platform]
|
|
184
161
|
* @param {string} [options.targetBrowser] - Launching browser channel (key derivation)
|
|
185
162
|
* @param {Object} [options.keys] - Injected password keys {resolveSourceKey, targetKey, targetPrefix, primaryPassword}
|
|
@@ -198,6 +175,8 @@ export async function migrateProfile({
|
|
|
198
175
|
homeDir = os.homedir(),
|
|
199
176
|
environment = process.env,
|
|
200
177
|
runCommand,
|
|
178
|
+
passwordCsv,
|
|
179
|
+
includePaymentCards = false,
|
|
201
180
|
}) {
|
|
202
181
|
if (!from?.browser) {
|
|
203
182
|
throw new TypeError('migrateProfile requires from.browser');
|
|
@@ -205,6 +184,17 @@ export async function migrateProfile({
|
|
|
205
184
|
if (!to) {
|
|
206
185
|
throw new TypeError('migrateProfile requires a target directory (to)');
|
|
207
186
|
}
|
|
187
|
+
validateMigrationOptions({
|
|
188
|
+
include,
|
|
189
|
+
domains,
|
|
190
|
+
targetBrowser,
|
|
191
|
+
to,
|
|
192
|
+
platform,
|
|
193
|
+
homeDir,
|
|
194
|
+
environment,
|
|
195
|
+
classes: ALL_DATA_CLASSES,
|
|
196
|
+
includePaymentCards,
|
|
197
|
+
});
|
|
208
198
|
// An explicit userDataDir names the source, so only an installed-browser
|
|
209
199
|
// import is steered towards the profile holding the requested domains.
|
|
210
200
|
const source = await resolveImportSource({
|
|
@@ -216,6 +206,11 @@ export async function migrateProfile({
|
|
|
216
206
|
runCommand,
|
|
217
207
|
});
|
|
218
208
|
const { browser } = source;
|
|
209
|
+
if (!['chromium', 'firefox', 'safari'].includes(browserFamily(browser))) {
|
|
210
|
+
throw new TypeError(
|
|
211
|
+
`Browser ${browser} supports detection only; its profile format is not supported for migration`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
219
214
|
const profile = from.profile ?? source.profile ?? 'Default';
|
|
220
215
|
const isFirefox = isFirefoxBrowser(browser);
|
|
221
216
|
const sourceProfileDir = await resolveSourceProfileDir({
|
|
@@ -227,6 +222,7 @@ export async function migrateProfile({
|
|
|
227
222
|
environment,
|
|
228
223
|
});
|
|
229
224
|
const selected = new Set(include);
|
|
225
|
+
validateMigrationPaths(sourceProfileDir, to);
|
|
230
226
|
const report = {
|
|
231
227
|
source: { browser, profile, userDataDir: from.userDataDir ?? null },
|
|
232
228
|
target: to,
|
|
@@ -238,6 +234,13 @@ export async function migrateProfile({
|
|
|
238
234
|
if (source.warning) {
|
|
239
235
|
report.warnings.push(source.warning);
|
|
240
236
|
}
|
|
237
|
+
report.skipped.push(
|
|
238
|
+
...reportAdditionalClasses({
|
|
239
|
+
selected,
|
|
240
|
+
profileDir: sourceProfileDir,
|
|
241
|
+
includePaymentCards,
|
|
242
|
+
})
|
|
243
|
+
);
|
|
241
244
|
|
|
242
245
|
if (selected.has('cookies')) {
|
|
243
246
|
if (isFirefox) {
|
|
@@ -263,8 +266,21 @@ export async function migrateProfile({
|
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
if (browserFamily(browser) === 'safari') {
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
return migrateSafariProfile({
|
|
270
|
+
selected,
|
|
271
|
+
passwordCsv,
|
|
272
|
+
keys,
|
|
273
|
+
browser,
|
|
274
|
+
targetBrowser,
|
|
275
|
+
platform,
|
|
276
|
+
sourceProfileDir,
|
|
277
|
+
environment,
|
|
278
|
+
from,
|
|
279
|
+
to,
|
|
280
|
+
domains,
|
|
281
|
+
homeDir,
|
|
282
|
+
report,
|
|
283
|
+
});
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
if (selected.has('bookmarks')) {
|
|
@@ -288,10 +304,15 @@ export async function migrateProfile({
|
|
|
288
304
|
report,
|
|
289
305
|
'history',
|
|
290
306
|
isFirefox
|
|
291
|
-
? await
|
|
307
|
+
? await migrateFirefoxHistory({
|
|
308
|
+
profileDir: sourceProfileDir,
|
|
309
|
+
targetProfileDir: to,
|
|
310
|
+
domains,
|
|
311
|
+
})
|
|
292
312
|
: await migrateHistory({
|
|
293
313
|
sourceProfileDir,
|
|
294
314
|
targetProfileDir: to,
|
|
315
|
+
domains,
|
|
295
316
|
})
|
|
296
317
|
);
|
|
297
318
|
}
|
|
@@ -312,56 +333,181 @@ export async function migrateProfile({
|
|
|
312
333
|
);
|
|
313
334
|
}
|
|
314
335
|
|
|
336
|
+
if (isFirefox) {
|
|
337
|
+
for (const type of ['preferences', 'extensions']) {
|
|
338
|
+
if (selected.has(type)) {
|
|
339
|
+
report.skipped.push({
|
|
340
|
+
type,
|
|
341
|
+
item: sourceProfileDir,
|
|
342
|
+
reason: 'firefox-class-not-supported',
|
|
343
|
+
detail:
|
|
344
|
+
'Firefox preferences and extensions cannot be copied into a Chromium profile.',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
315
350
|
if (selected.has('passwords')) {
|
|
316
|
-
|
|
351
|
+
await migratePasswordClass(report, {
|
|
317
352
|
keys,
|
|
318
353
|
browser,
|
|
319
|
-
targetBrowser
|
|
354
|
+
targetBrowser,
|
|
320
355
|
platform,
|
|
321
356
|
sourceProfileDir,
|
|
322
357
|
environment,
|
|
358
|
+
to,
|
|
359
|
+
isFirefox,
|
|
360
|
+
domains,
|
|
323
361
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return report;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function migrateSafariProfile({
|
|
368
|
+
report,
|
|
369
|
+
browser,
|
|
370
|
+
domains,
|
|
371
|
+
environment,
|
|
372
|
+
from,
|
|
373
|
+
homeDir,
|
|
374
|
+
keys,
|
|
375
|
+
passwordCsv,
|
|
376
|
+
platform,
|
|
377
|
+
selected,
|
|
378
|
+
sourceProfileDir,
|
|
379
|
+
targetBrowser,
|
|
380
|
+
to,
|
|
381
|
+
}) {
|
|
382
|
+
const passwordKeys =
|
|
383
|
+
selected.has('passwords') && passwordCsv
|
|
384
|
+
? await resolvePasswordKeys({
|
|
385
|
+
keys,
|
|
386
|
+
browser,
|
|
387
|
+
targetBrowser: targetBrowser ?? 'chrome',
|
|
344
388
|
platform,
|
|
345
|
-
targetKey: passwordKeys.targetKey,
|
|
346
|
-
targetPrefix: passwordKeys.targetPrefix,
|
|
347
|
-
primaryPassword: passwordKeys.primaryPassword,
|
|
348
|
-
})
|
|
349
|
-
);
|
|
350
|
-
} else {
|
|
351
|
-
mergeReport(
|
|
352
|
-
report,
|
|
353
|
-
'passwords',
|
|
354
|
-
await migratePasswords({
|
|
355
389
|
sourceProfileDir,
|
|
356
|
-
|
|
357
|
-
platform,
|
|
358
|
-
resolveSourceKey: passwordKeys.resolveSourceKey,
|
|
359
|
-
targetKey: passwordKeys.targetKey,
|
|
360
|
-
targetPrefix: passwordKeys.targetPrefix,
|
|
390
|
+
environment,
|
|
361
391
|
})
|
|
362
|
-
|
|
363
|
-
|
|
392
|
+
: null;
|
|
393
|
+
for (const type of ALL_DATA_CLASSES.filter(
|
|
394
|
+
(type) =>
|
|
395
|
+
type !== 'cookies' &&
|
|
396
|
+
!ADDITIONAL_DATA_CLASSES.includes(type) &&
|
|
397
|
+
selected.has(type)
|
|
398
|
+
)) {
|
|
399
|
+
mergeReport(
|
|
400
|
+
report,
|
|
401
|
+
type,
|
|
402
|
+
await migrateSafariClass({
|
|
403
|
+
type,
|
|
404
|
+
profileDir: sourceProfileDir,
|
|
405
|
+
targetProfileDir: to,
|
|
406
|
+
domains,
|
|
407
|
+
passwordCsv,
|
|
408
|
+
passwordKeys,
|
|
409
|
+
platform,
|
|
410
|
+
legacyDir:
|
|
411
|
+
from.userDataDir ||
|
|
412
|
+
path.basename(path.dirname(sourceProfileDir)) === 'Profiles'
|
|
413
|
+
? undefined
|
|
414
|
+
: path.join(
|
|
415
|
+
homeDir,
|
|
416
|
+
'Library',
|
|
417
|
+
browser === 'safari' ? 'Safari' : 'Safari Technology Preview'
|
|
418
|
+
),
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (report.cookies.length) {
|
|
423
|
+
report.warnings.push({
|
|
424
|
+
type: 'cookies',
|
|
425
|
+
item: browser,
|
|
426
|
+
reason: 'safari-samesite-unavailable',
|
|
427
|
+
detail:
|
|
428
|
+
'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
|
|
429
|
+
});
|
|
364
430
|
}
|
|
365
|
-
|
|
366
431
|
return report;
|
|
367
432
|
}
|
|
433
|
+
|
|
434
|
+
async function migratePasswordClass(
|
|
435
|
+
report,
|
|
436
|
+
{
|
|
437
|
+
browser,
|
|
438
|
+
domains,
|
|
439
|
+
environment,
|
|
440
|
+
isFirefox,
|
|
441
|
+
keys,
|
|
442
|
+
platform,
|
|
443
|
+
sourceProfileDir,
|
|
444
|
+
targetBrowser,
|
|
445
|
+
to,
|
|
446
|
+
}
|
|
447
|
+
) {
|
|
448
|
+
if (
|
|
449
|
+
browser === 'yandex' &&
|
|
450
|
+
(await pathExists(path.join(sourceProfileDir, 'Ya Passman Data')))
|
|
451
|
+
) {
|
|
452
|
+
report.skipped.push({
|
|
453
|
+
type: 'passwords',
|
|
454
|
+
item: 'Ya Passman Data',
|
|
455
|
+
reason: 'yandex-passman-encryption-unsupported',
|
|
456
|
+
detail:
|
|
457
|
+
'Ya Passman Data uses local_encryptor_data and may require a Yandex master password; this extra encryption layer is not supported. Export passwords to a supported format instead.',
|
|
458
|
+
});
|
|
459
|
+
if (!(await pathExists(path.join(sourceProfileDir, 'Login Data')))) {
|
|
460
|
+
return report;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const passwordKeys = await resolvePasswordKeys({
|
|
464
|
+
keys,
|
|
465
|
+
browser,
|
|
466
|
+
targetBrowser: targetBrowser ?? (isFirefox ? 'chrome' : browser),
|
|
467
|
+
platform,
|
|
468
|
+
sourceProfileDir,
|
|
469
|
+
environment,
|
|
470
|
+
});
|
|
471
|
+
if (!passwordKeys?.targetKey) {
|
|
472
|
+
report.skipped.push({
|
|
473
|
+
type: 'passwords',
|
|
474
|
+
item: 'Login Data',
|
|
475
|
+
reason: 'target-key-unavailable',
|
|
476
|
+
});
|
|
477
|
+
report.warnings.push({
|
|
478
|
+
type: 'passwords',
|
|
479
|
+
item: 'Login Data',
|
|
480
|
+
reason: 'target-key-unavailable',
|
|
481
|
+
detail:
|
|
482
|
+
'A target encryption key was not available (on Windows the launcher must generate one and write it into the target Local State); passwords were not migrated.',
|
|
483
|
+
});
|
|
484
|
+
} else if (isFirefox) {
|
|
485
|
+
mergeReport(
|
|
486
|
+
report,
|
|
487
|
+
'passwords',
|
|
488
|
+
await migrateFirefoxPasswords({
|
|
489
|
+
profileDir: sourceProfileDir,
|
|
490
|
+
targetProfileDir: to,
|
|
491
|
+
platform,
|
|
492
|
+
targetKey: passwordKeys.targetKey,
|
|
493
|
+
targetPrefix: passwordKeys.targetPrefix,
|
|
494
|
+
primaryPassword: passwordKeys.primaryPassword,
|
|
495
|
+
domains,
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
mergeReport(
|
|
500
|
+
report,
|
|
501
|
+
'passwords',
|
|
502
|
+
await migratePasswords({
|
|
503
|
+
sourceProfileDir,
|
|
504
|
+
targetProfileDir: to,
|
|
505
|
+
platform,
|
|
506
|
+
resolveSourceKey: passwordKeys.resolveSourceKey,
|
|
507
|
+
targetKey: passwordKeys.targetKey,
|
|
508
|
+
targetPrefix: passwordKeys.targetPrefix,
|
|
509
|
+
domains,
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
1
|
import { deriveChromiumCookieKey } from '../browser-cookie-crypto.js';
|
|
4
2
|
import {
|
|
5
3
|
decryptWindowsDpapi,
|
|
@@ -101,6 +99,4 @@ export async function resolveTargetKey({
|
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
/** Convenience: the Local State path next to a profile directory. */
|
|
104
|
-
export
|
|
105
|
-
return path.join(path.dirname(profileDir), 'Local State');
|
|
106
|
-
}
|
|
102
|
+
export { localStatePathForProfile } from '../browser-profile-files.js';
|