Avoid Pass importer failing during line parsing
[ProtonMail-WebClient.git] / packages / pass / lib / import / providers / roboform / roboform.reader.ts
blobca5066a0877ba3b41b0c639ddfb76abeead1e428
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';
5 import {
6     getEmailOrUsername,
7     getImportedVaultName,
8     importLoginItem,
9     importNoteItem,
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[] = [];
36     try {
37         const result = await readCSV<RoboformVariadicItem>({
38             data,
39             onError: (error) => warnings.push(error),
40         });
42         /*
43          * Skips the first row (headers) and maps results to an array of RoboformItem's.
44          */
45         const items: RoboformItem[] = result.items
46             .slice(1)
47             .map(([Name, Url, MatchUrl, Login, Pwd, Note, Folder, ...RfFieldsV2]) => ({
48                 Name,
49                 Url,
50                 MatchUrl,
51                 Login: unescapeFieldValue(Login),
52                 Pwd: unescapeFieldValue(Pwd),
53                 Note,
54                 Folder: lastItem(Folder.split('/')),
55                 RfFieldsV2,
56             }));
58         const groupedByVault = groupByKey(items, 'Folder');
60         const vaults: ImportVault[] = groupedByVault.map((roboformItems) => {
61             const vaultName = roboformItems[0].Folder;
63             const items = roboformItems
64                 .map((item) => {
65                     try {
66                         /* Bookmark */
67                         if (item.Url && !item.MatchUrl && !item.Login && !item.Pwd) {
68                             return importLoginItem({
69                                 name: item.Name,
70                                 urls: [item.Url],
71                                 note: item.Note,
72                             });
73                         }
75                         /* Note */
76                         if (!item.Url && !item.MatchUrl && !item.Login && !item.Pwd) {
77                             return importNoteItem({
78                                 name: item.Name,
79                                 note: item.Note,
80                             });
81                         }
83                         /* Login */
84                         return importLoginItem({
85                             name: item.Name,
86                             urls: [item.MatchUrl],
87                             note: item.Note,
88                             ...getEmailOrUsername(item.Login),
89                             password: item.Pwd,
90                             totp: formatOtpAuthUri(item),
91                         });
92                     } catch (err) {
93                         const title = item?.Name ?? '';
94                         ignored.push(`[${c('Label').t`Unknown`}] ${title}`);
95                         logger.warn('[Importer::Roboform]', err);
96                     }
97                 })
98                 .filter(truthy);
100             return {
101                 shareId: null,
102                 name: getImportedVaultName(vaultName),
103                 items,
104             };
105         });
107         return { vaults, ignored, warnings };
108     } catch (e) {
109         logger.warn('[Importer::Roboform]', e);
110         throw new ImportProviderError('Roboform', e);
111     }