Skip to content

Commit

Permalink
export: allow customizing the list of exported fields
Browse files Browse the repository at this point in the history
Add a new --fields=FIELDLIST argument which can be used to
customize the list of output fields instead of printing
just the few defaults that we can import.

Signed-off-by: Bob Copeland <copeland@lastpass.com>
  • Loading branch information
Bob Copeland committed Jun 7, 2017
1 parent 7e9c202 commit aabc642
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 54 deletions.
141 changes: 90 additions & 51 deletions cmd-export.c
Expand Up @@ -46,6 +46,21 @@
#include <unistd.h>
#include <string.h>

struct field_selection {
const char *name;
struct list_head list;
};

static void parse_field_arg(char *arg, struct list_head *head)
{
char *token;
for (token = strtok(arg, ","); token; token = strtok(NULL, ",")) {
struct field_selection *sel = new0(struct field_selection, 1);
sel->name = token;
list_add_tail(&sel->list, head);
}
}

static void print_csv_cell(const char *cell, bool is_last)
{
const char *ptr;
Expand Down Expand Up @@ -79,52 +94,72 @@ static void print_csv_cell(const char *cell, bool is_last)
printf(",");
}

void print_account_to_csv_standard(struct account* account, const char* groupname)
void print_csv_field(struct account *account, const char *field_name,
bool is_last)
{
print_csv_cell(account->url, false);
print_csv_cell(account->username, false);
print_csv_cell(account->password, false);
print_csv_cell(account->note, false);
print_csv_cell(account->name, false);
print_csv_cell(groupname, false);
print_csv_cell(bool_str(account->fav), true);
}
_cleanup_free_ char *share_group = NULL;
char *groupname = account->group;

#define OUTPUT_FIELD(name, value, is_last) \
do { \
if (!strcmp(field_name, name)) { \
print_csv_cell(value, is_last); \
return; \
} \
} while(0)

OUTPUT_FIELD("url", account->url, is_last);
OUTPUT_FIELD("username", account->username, is_last);
OUTPUT_FIELD("password", account->password, is_last);
OUTPUT_FIELD("extra", account->note, is_last);
OUTPUT_FIELD("name", account->name, is_last);
OUTPUT_FIELD("fav", bool_str(account->fav), is_last);
OUTPUT_FIELD("id", account->id, is_last);
OUTPUT_FIELD("group", account->group, is_last);
OUTPUT_FIELD("fullname", account->fullname, is_last);
OUTPUT_FIELD("last_touch", account->last_touch, is_last);
OUTPUT_FIELD("last_modified_gmt", account->last_modified_gmt, is_last);
OUTPUT_FIELD("attachpresent", bool_str(account->attachpresent), is_last);

if (!strcmp(field_name, "grouping")) {
if (account->share) {
xasprintf(&share_group, "%s\\%s",
account->share->name, account->group);

void print_account_to_csv_full(struct account* account, const char* groupname)
{
print_csv_cell(account->id, false);
print_csv_cell(account->name, false);
print_csv_cell(account->group, false);
print_csv_cell(groupname, false);
print_csv_cell(account->fullname, false);
print_csv_cell(account->url, false);
print_csv_cell(account->username, false);
print_csv_cell(account->password, false);
print_csv_cell(account->note, false);
print_csv_cell(account->last_touch, false);
print_csv_cell(account->last_modified_gmt, false);
print_csv_cell(bool_str(account->fav), false);
print_csv_cell(bool_str(account->attachpresent), true);
/* trim trailing backslash if no subfolder */
if (!strlen(account->group))
share_group[strlen(share_group)-1] = '\0';

groupname = share_group;
}
print_csv_cell(groupname, is_last);
return;
}

/* unknown field, just return empty string */
print_csv_cell("", is_last);
}

int cmd_export(int argc, char **argv)
{
static struct option long_options[] = {
{"sync", required_argument, NULL, 'S'},
{"color", required_argument, NULL, 'C'},
{"full", no_argument, NULL, 'f'},
{"fields", required_argument, NULL, 'f'},
{0, 0, 0, 0}
};
int option;
int option_index;
enum blobsync sync = BLOB_SYNC_AUTO;
struct account *account;
static const char *csv_header_standard = "url,username,password,extra,name,grouping,fav\r\n";
static const char *csv_header_full = "id,name,group,groupname,fullname,url,username,password,note,last_touch,last_modified_gmt,fav,attachpresent\r\n";
const char *csv_header = csv_header_standard;
int is_standard_format = 1;
const char *default_fields[] = {
"url", "username", "password", "extra",
"name", "grouping", "fav"
};

LIST_HEAD(field_list);

while ((option = getopt_long(argc, argv, "c", long_options, &option_index)) != -1) {
while ((option = getopt_long(argc, argv, "", long_options, &option_index)) != -1) {
switch (option) {
case 'S':
sync = parse_sync_string(optarg);
Expand All @@ -134,18 +169,27 @@ int cmd_export(int argc, char **argv)
parse_color_mode_string(optarg));
break;
case 'f':
csv_header = csv_header_full;
is_standard_format = 0;
parse_field_arg(optarg, &field_list);
break;
case '?':
default:
die_usage(cmd_export_usage);
}
}

if (list_empty(&field_list)) {
for (unsigned int i = 0; i < ARRAY_SIZE(default_fields); i++) {
struct field_selection *sel = new0(struct field_selection, 1);
sel->name = default_fields[i];
list_add_tail(&sel->list, &field_list);
}
}

unsigned char key[KDF_HASH_LEN];
struct session *session = NULL;
struct blob *blob = NULL;
struct field_selection *field_sel, *tmp;

init_all(sync, key, &session, &blob);

/* reprompt once if any one account is password protected */
Expand All @@ -160,35 +204,30 @@ int cmd_export(int argc, char **argv)
}
}

printf("%s", csv_header);
struct field_selection *last_entry =
list_last_entry_or_null(&field_list, struct field_selection, list);

list_for_each_entry(account, &blob->account_head, list) {
/* header */
list_for_each_entry(field_sel, &field_list, list) {
print_csv_cell(field_sel->name, field_sel == last_entry);
}

_cleanup_free_ char *share_group = NULL;
char *groupname = account->group;
/* entries */
list_for_each_entry(account, &blob->account_head, list) {

/* skip groups */
if (!strcmp(account->url, "http://group"))
continue;

if (account->share) {
xasprintf(&share_group, "%s\\%s",
account->share->name, account->group);

/* trim trailing backslash if no subfolder */
if (!strlen(account->group))
share_group[strlen(share_group)-1] = '\0';

groupname = share_group;
list_for_each_entry(field_sel, &field_list, list) {
print_csv_field(account, field_sel->name,
field_sel == last_entry);
}

lastpass_log_access(sync, session, key, account);
if (is_standard_format) {
print_account_to_csv_standard(account, groupname);
continue;
}
}

print_account_to_csv_full(account, groupname);
list_for_each_entry_safe(field_sel, tmp, &field_list, list) {
free(field_sel);
}

session_free(session);
Expand Down
2 changes: 1 addition & 1 deletion cmd.h
Expand Up @@ -108,7 +108,7 @@ int cmd_sync(int argc, char **argv);
#define cmd_sync_usage "sync [--background, -b] " color_usage

int cmd_export(int argc, char **argv);
#define cmd_export_usage "export [--sync=auto|now|no] " color_usage
#define cmd_export_usage "export [--sync=auto|now|no] " color_usage " [--fields=FIELDLIST]"

int cmd_share(int argc, char **argv);
#define cmd_share_usage "share subcommand sharename ..."
Expand Down
11 changes: 11 additions & 0 deletions list.h
Expand Up @@ -162,6 +162,17 @@ static inline void list_del(struct list_head *entry)
#define list_first_entry_or_null(ptr, type, member) \
(!list_empty(ptr) ? list_first_entry(ptr, type, member) : NULL)

/**
* list_last_entry_or_null - get the last element from a list
* @ptr: the list head to take the element from.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*
* Note that if the list is empty, it returns NULL.
*/
#define list_last_entry_or_null(ptr, type, member) \
(!list_empty(ptr) ? list_last_entry(ptr, type, member) : NULL)

/**
* list_next_entry - get the next element in list
* @pos: the type * to cursor
Expand Down
9 changes: 7 additions & 2 deletions lpass.1.txt
Expand Up @@ -35,7 +35,7 @@ several subcommands:
lpass *status* [--quiet, -q] [--color=auto|never|always]
lpass *sync* [--background, -b] [--color=auto|never|always]
lpass *import* [--sync=auto|now|no] [FILENAME]
lpass *export* [--sync=auto|now|no] [--color=auto|never|always]
lpass *export* [--sync=auto|now|no] [--color=auto|never|always] [--fields=FIELDLIST]
lpass *share* *userls* SHARE
lpass *share* *useradd* [--read-only=[true|false]] [--hidden=[true|false]] [--admin=[true|false]] SHARE USERNAME
lpass *share* *usermod* [--read-only=[true|false]] [--hidden=[true|false]] [--admin=[true|false]] SHARE USERNAME
Expand Down Expand Up @@ -185,7 +185,12 @@ will create a duplicate entry of the one specified, but with a different 'ID'.
Backup
~~~~~~
The 'export' subcommand will dump all account information including
passwords to stdout (unencrypted) in CSV format.
passwords to stdout (unencrypted) in CSV format. The optional
'--fields=FIELDLIST' argument may contain a comma-separated subset of the
following fields:

id, url, username, password, extra, name, fav, id, grouping, group,
fullname, last_touch, last_modified_gmt, attachpresent

The 'import' subcommand does the reverse: accounts from an unencrypted
CSV file are uploaded to the server.
Expand Down

0 comments on commit aabc642

Please sign in to comment.