1 import { c } from 'ttag';
3 import { readCSV } from '@proton/pass/lib/import/helpers/csv.reader';
4 import { ImportProviderError } from '@proton/pass/lib/import/helpers/error';
10 } from '@proton/pass/lib/import/helpers/transformers';
11 import type { ImportPayload, ImportVault } from '@proton/pass/lib/import/types';
12 import type { Maybe } from '@proton/pass/types';
13 import { groupByKey } from '@proton/pass/utils/array/group-by-key';
14 import { truthy } from '@proton/pass/utils/fp/predicates';
15 import { logger } from '@proton/pass/utils/logger';
16 import lastItem from '@proton/utils/lastItem';
18 import type { RoboformItem, RoboformVariadicItem } from './roboform.types';
20 /** In Roboform exports, fields beginning with `@` or `'` have a single quote (`'`) prepended.
21 * To account for this, occurences of `'@` and `''` are replaced with their actual values,
22 * ie. `@` and `'` respectively. */
23 const unescapeFieldValue = (value: string) => value.replace(/^'(?=@|')/, '');
25 const formatOtpAuthUri = (item: RoboformItem): Maybe<string> => {
26 const totpDefinition = item.RfFieldsV2?.find((e) => e.startsWith('TOTP KEY$'));
27 if (!totpDefinition) return;
28 const secret = lastItem(totpDefinition.split(','));
29 return `otpauth://totp/${item.Name}:none?secret=${secret}`;
32 export const readRoboformData = async ({ data }: { data: string }): Promise<ImportPayload> => {
33 const ignored: string[] = [];
34 const warnings: string[] = [];
37 const result = await readCSV<RoboformVariadicItem>({
39 onError: (error) => warnings.push(error),
43 * Skips the first row (headers) and maps results to an array of RoboformItem's.
45 const items: RoboformItem[] = result.items
47 .map(([Name, Url, MatchUrl, Login, Pwd, Note, Folder, ...RfFieldsV2]) => ({
51 Login: unescapeFieldValue(Login),
52 Pwd: unescapeFieldValue(Pwd),
54 Folder: lastItem(Folder.split('/')),
58 const groupedByVault = groupByKey(items, 'Folder');
60 const vaults: ImportVault[] = groupedByVault.map((roboformItems) => {
61 const vaultName = roboformItems[0].Folder;
63 const items = roboformItems
67 if (item.Url && !item.MatchUrl && !item.Login && !item.Pwd) {
68 return importLoginItem({
76 if (!item.Url && !item.MatchUrl && !item.Login && !item.Pwd) {
77 return importNoteItem({
84 return importLoginItem({
86 urls: [item.MatchUrl],
88 ...getEmailOrUsername(item.Login),
90 totp: formatOtpAuthUri(item),
93 const title = item?.Name ?? '';
94 ignored.push(`[${c('Label').t`Unknown`}] ${title}`);
95 logger.warn('[Importer::Roboform]', err);
102 name: getImportedVaultName(vaultName),
107 return { vaults, ignored, warnings };
109 logger.warn('[Importer::Roboform]', e);
110 throw new ImportProviderError('Roboform', e);