Merge branch 'import-export-improvements'

Signed-off-by: binsky <timo@binsky.org>
This commit is contained in:
binsky 2023-07-16 23:29:40 +02:00
commit 0e09b4f579
No known key found for this signature in database
GPG key ID: B438F7FA2E3AC98F
11 changed files with 234 additions and 151 deletions

View file

@ -1276,10 +1276,10 @@ h3 {
.setting-group input[type="text"], .setting-group input[type="password"], .setting-group textarea {
width: 100%; }
.setting-group.margin-bottom-25 {
.setting-group.margin-bottom-25, .margin-bottom-25 {
margin-bottom: 25px; }
.setting-group.margin-bottom-10 {
.setting-group.margin-bottom-10, .margin-bottom-10 {
margin-bottom: 10px; }
.display-grid {
@ -1295,7 +1295,7 @@ h3 {
label[for=confirmVaultPWChange] {
margin-bottom: 10px; }
label[for=confirmVaultDelete] {
label[for=confirmVaultDelete], label[for=skipFirstRow] {
margin-top: 10px;
margin-bottom: 10px; }

File diff suppressed because one or more lines are too long

View file

@ -32,8 +32,8 @@
* Controller of the passmanApp
*/
angular.module('passmanApp')
.controller('GenericCsvImportCtrl', ['$scope', 'CredentialService', 'FileService', 'EncryptService', '$translate', '$q',
function ($scope, CredentialService, FileService, EncryptService, $translate, $q) {
.controller('GenericCsvImportCtrl', ['$scope', '$rootScope', 'CredentialService', 'FileService', 'EncryptService', '$translate', '$q',
function ($scope, $rootScope, CredentialService, FileService, EncryptService, $translate, $q) {
$scope.hello = 'world';
$scope.credentialProperties = [
@ -45,17 +45,27 @@
{
label: 'Username',
prop: 'username',
matching: ['username', 'user', 'login', 'login name']
matching: ['username', 'user', 'login', 'login name', 'login_username']
},
{
label: 'Password',
prop: 'password',
matching: ['password', 'pass', 'pw']
matching: ['password', 'pass', 'pw', 'login_password']
},
{
label: 'TOTP Secret',
label: 'TOTP Secret or Object',
prop: 'otp',
matching: ['totp']
matching: ['otp', 'otp_object', 'totp', 'login_totp']
},
{
label: 'Email',
prop: 'email',
matching: ['email', 'mail']
},
{
label: 'Notes',
prop: 'description',
matching: ['notes', 'description', 'comments']
},
{
label: 'Custom field',
@ -71,24 +81,45 @@
prop: 'files',
matching: ['files']
},
{
label: 'Notes',
prop: 'description',
matching: ['notes', 'description', 'comments']
},
{
label: 'Email',
prop: 'email',
matching: ['email', 'mail']
},
{
label: 'URL',
prop: 'url',
matching: ['website', 'url', 'fulladdress', 'site', 'web site']
matching: ['website', 'url', 'fulladdress', 'site', 'web site', 'login_uri']
},
{
label: 'Tags',
prop: 'tags'
prop: 'tags',
matching: ['tags', 'folder']
},
{
label: 'Created',
prop: 'created',
matching: ['created', 'creation']
},
{
label: 'Changed',
prop: 'changed',
matching: ['changed', 'edited']
},
{
label: 'Expire time',
prop: 'expire_time',
matching: ['expire_time', 'expire', 'expires', 'expires_at']
},
{
label: 'Delete time',
prop: 'delete_time',
matching: ['delete_time', 'delete', 'deleted_at']
},
{
label: 'Icon',
prop: 'icon',
matching: ['icon', 'favicon']
},
{
label: 'Compromised',
prop: 'compromised',
matching: ['compromised']
},
{
label: 'Ignored',
@ -99,14 +130,23 @@
return {text: t};
};
var rowToCredential = async function (row) {
var _credential = PassmanImporter.newCredential();
for(var k = 0; k < $scope.import_fields.length; k++){
var field = $scope.import_fields[k];
let _credential = PassmanImporter.newCredential();
for(let k = 0; k < $scope.import_fields.length; k++){
const field = $scope.import_fields[k];
if(field){
if(field === 'otp'){
_credential.otp.secret = row[k];
if (typeof row[k] === 'object' || row[k].includes('{"')) {
const otpobj = JSON.parse(row[k]);
if (typeof otpobj === 'object' && otpobj.secret !== undefined && otpobj.algorithm !== undefined && otpobj.period !== undefined && otpobj.digits !== undefined) {
_credential.otp = otpobj;
} else if (otpobj.secret !== undefined) {
_credential.otp.secret = otpobj.secret;
}
} else if (row[k] !== '{}') {
_credential.otp.secret = row[k];
}
} else if(field === 'custom_field'){
var key = ($scope.matched) ? $scope.parsed_csv[0][k] : 'Custom field '+ k;
const key = ($scope.matched) ? $scope.parsed_csv[0][k] : 'Custom field '+ k;
_credential.custom_fields.push({
'label': key,
'value': row[k],
@ -116,46 +156,44 @@
if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){
try {
row[k] = JSON.parse(row[k]);
for(let i = 0; k < row[k].length; i++){
_credential.custom_fields.push({
'label': row[k][i].label,
'secret': row[k][i].secret,
'field_type': row[k][i].field_type,
});
}
} catch (e) {
// ignore row[k], it contains no valid json data
// console.error(e);
console.error(e);
continue;
}
} else {
for(let j = 0; j < row[k].length; j++){
if (row[k][j].field_type === 'file'){
var _file = {
filename: row[k][j].value.filename,
size: row[k][j].value.size,
mimetype: row[k][j].value.mimetype,
data: row[k][j].value.file_data
};
row[k][j].value = await FileService.uploadFile(_file).then(FileService.getEmptyFileWithDecryptedFilename);
}
for(let j = 0; j < row[k].length; j++){
if (row[k][j].field_type === 'file'){
const _file = {
filename: row[k][j].value.filename,
size: row[k][j].value.size,
mimetype: row[k][j].value.mimetype,
data: row[k][j].value.file_data ? row[k][j].value.file_data : row[k][j].value.data
};
if (_file.data === undefined) {
console.error('Unable to parse file data from ', row[k][j]);
$scope.log.push('Unable to parse file data from file ' + _file.filename);
continue;
}
_credential.custom_fields.push(row[k][j]);
row[k][j].value = await FileService.uploadFile(_file).then(FileService.getEmptyFileWithDecryptedFilename);
}
_credential.custom_fields.push(row[k][j]);
}
} else if(field === 'files'){
if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){
try {
row[k] = JSON.parse(row[k]);
for(let i = 0; k < row[k].length; i++){
_credential.files.push({
for(let i = 0; i < row[k].length; i++){
_credential.files.push(await FileService.uploadFile({
filename: row[k][i].filename,
size: row[k][i].size,
mimetype: row[k][i].mimetype
});
mimetype: row[k][i].mimetype,
data: row[k][i].file_data ? row[k][i].file_data : row[k][i].data
}).then(FileService.getEmptyFileWithDecryptedFilename));
}
} catch (e) {
// ignore row[k], it contains no valid json data
// console.error(e);
console.error(e);
}
} else {
for(let j = 0; j < row[k].length; j++){
@ -163,15 +201,25 @@
filename: row[k][j].filename,
size: row[k][j].size,
mimetype: row[k][j].mimetype,
data: row[k][j].file_data
data: row[k][j].file_data ? row[k][j].file_data : row[k][j].data
}).then(FileService.getEmptyFileWithDecryptedFilename));
}
}
} else if(field === 'tags'){
if( row[k]) {
var tags = row[k].split(',');
if(row[k] && row[k] !== '' && row[k] !== '[]') {
if (row[k].startsWith('[') && row[k].endsWith(']')) {
row[k] = row[k].substring(1, row[k].length - 1);
}
const tags = row[k].split(',');
_credential.tags = tags.map(tagMapper);
}
} else if(field === 'compromised'){
_credential[field] = (row[k] === 'true' || row[k] === '1');
} else if (field === 'created' || field === 'changed' || field === 'expire_time' || field === 'delete_time') {
const num = parseInt(row[k]);
if (!isNaN(num)) {
_credential[field] = num;
}
} else{
_credential[field] = row[k];
}
@ -181,19 +229,19 @@
};
$scope.inspectCredential = function (row) {
$scope.inspected_credential = rowToCredential(row);
$scope.inspectCredential = async function (row) {
$scope.inspected_credential = await rowToCredential(row);
};
$scope.csvLoaded = function (file) {
$scope.import_fields = [];
$scope.inspected_credential = false;
$scope.matched = false;
$scope.inspected_credential = {};
$scope.atLeastlabelMatched = false;
var file_data = file.data.split(',');
file_data = decodeURIComponent(escape(window.atob(file_data[1])));
/** global: Papa */
Papa.parse(file_data, {
complete: function(results) {
complete: async function(results) {
if(results.data) {
for(var i = 0; i < results.data[0].length; i++){
var propName = results.data[0][i];
@ -203,13 +251,15 @@
if(credentialProperty.matching){
if(credentialProperty.matching.indexOf(propName.toLowerCase()) !== -1){
$scope.import_fields[i] = credentialProperty.prop;
$scope.matched = true;
if (credentialProperty.prop === 'label') {
$scope.atLeastlabelMatched = true;
}
}
}
}
}
if($scope.matched){
$scope.inspectCredential(results.data[1]);
if($scope.atLeastlabelMatched){
await $scope.inspectCredential(results.data[1]);
}
for(var j = 0; j < results.data.length; j++){
@ -242,6 +292,7 @@
};
$scope.log.push($translate.instant('done'));
$scope.importing = false;
$rootScope.refresh();
}
}
@ -274,5 +325,12 @@
var start = ($scope.skipFirstRow) ? 1 : 0;
$scope.inspectCredential($scope.parsed_csv[start]);
};
$scope.fileLoadError = function (file) {
console.error($translate.instant('error.loading.file'), file);
};
$scope.fileSelectProgress = function () {
};
}]);
}());

View file

@ -36,7 +36,7 @@
replace: true,
restrict: 'A',
scope: {
credential: '=credentialTemplate'
credential: '='
},
link: function (scope, element, attrs) {

View file

@ -34,33 +34,50 @@ PassmanExporter.csv.export = function (credentials, FileService, EncryptService,
/** global: C_Promise */
return new C_Promise(function () {
PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){
var headers = ['label', 'username', 'password', 'email', 'description', 'tags', 'url', 'custom_fields', 'files'];
var file_data = '"' + headers.join('","') + '"\n';
for (var i = 0; i < credentials.length; i++) {
var _credential = credentials[i];
var row_data = [];
for (var h = 0; h < headers.length; h++) {
var field = headers[h];
const headers = [
'label',
'description',
'created',
'changed',
'tags',
'email',
'icon',
'username',
'password',
'url',
'renew_interval',
'expire_time',
'delete_time',
'files',
'custom_fields',
'otp',
'compromised',
'hidden'
];
let file_data = '"' + headers.join('","') + '"\n';
for (let i = 0; i < credentials.length; i++) {
const _credential = credentials[i];
let row_data = [];
for (const field of headers) {
if (field === 'tags') {
var _tags = [];
for (var t = 0; t < _credential[field].length; t++) {
_tags.push(_credential[field][t].text);
let _tags = [];
for (const tag_field of _credential[field]) {
_tags.push(tag_field.text);
}
var tag_data = '[' + _tags.join(",") + ']';
const tag_data = '[' + _tags.join(",") + ']';
row_data.push('"' + tag_data.replaceAll('"', '""') + '"');
}
else if (field == 'custom_fields' || field == 'files') {
var _fields = JSON.stringify(_credential[field]);
_fields = _fields.replaceAll('"', '""');
row_data.push('"' + _fields + '"');
}
else {
var data = _credential[field],
value = data === null ? '':data.replaceAll('"', '""');
row_data.push('"' + value + '"');
} else if (field === 'custom_fields' || field === 'files' || field === 'otp' || field === 'icon') {
let _fields = JSON.stringify(_credential[field]);
_fields = _fields.replaceAll('"', '""');
row_data.push('"' + _fields + '"');
} else {
let data = _credential[field];
data = typeof data === 'number' || typeof data === 'boolean' ? "" + data : data;
const value = (data === null || data === undefined) ? '' : data.replaceAll('"', '""');
row_data.push('"' + value + '"');
}
}
var progress = {
let progress = {
percent: i / credentials.length * 100,
loaded: i,
total: credentials.length

View file

@ -34,9 +34,9 @@ PassmanExporter.json.export = function (credentials, FileService, EncryptService
/** global: C_Promise */
return new C_Promise(function () {
PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){
var _output = [];
for (var i = 0; i < credentials.length; i++) {
var _credential = angular.copy(credentials[i]);
let _output = [];
for (let i = 0; i < credentials.length; i++) {
let _credential = angular.copy(credentials[i]);
delete _credential.vault_key;
delete _credential.vault_id;
@ -44,14 +44,14 @@ PassmanExporter.json.export = function (credentials, FileService, EncryptService
_output.push(_credential);
var progress = {
let progress = {
percent: i / credentials.length * 100,
loaded: i,
total: credentials.length
};
this.call_progress(progress);
}
var file_data = JSON.stringify(_output);
let file_data = JSON.stringify(_output);
this.call_then();
download(file_data, 'passman-export.json');
}).bind(this)).progress(function() {

View file

@ -75,44 +75,49 @@ if (!window['PassmanExporter']) {
}
}).bind(this);
for (var i = 0; i < credentials.length; i++) {
var item = credentials[i];
for (let i = 0; i < credentials.length; i++) {
const credential = credentials[i];
// Custom fields
for (c = 0; c < item.custom_fields.length; c++) {
var cf = item.custom_fields[c];
for (let c = 0; c < credential.custom_fields.length; c++) {
const cf = credential.custom_fields[c];
if (cf.field_type === 'file') {
const file = cf.value;
if (file !== "undefined" && file !== undefined && file !== null && file.file_id !== undefined) {
this.parent.total++;
this.parent.fileGUID_cred[file.guid] = {
cred_pos: i,
on: 'custom_fields',
at: c
};
this.parent.FS.getFile(file).then((function (data) {
this.parent.step(data);
}).bind(this), (function (error) {
this.parent.stepFailed(error);
}).bind(this));
}
}
}
// Also get all files
for (let c = 0; c < credential.files.length; c++) {
const file = credential.files[c];
if (file !== "undefined" && file !== undefined && file !== null && file.file_id !== undefined) {
this.parent.total++;
this.parent.fileGUID_cred[cf.value.guid] = {
this.parent.fileGUID_cred[file.guid] = {
cred_pos: i,
on: 'custom_fields',
on: 'files',
at: c
};
this.parent.FS.getFile(cf.value).then((function (data) {
this.parent.FS.getFile(file).then((function (data) {
this.parent.step(data);
}).bind(this), (function (error) {
this.parent.stepFailed(error);
}).bind(this));
}
}
// Also get all files
for (var c = 0; c < item.files.length; c++) {
this.parent.total++;
this.parent.fileGUID_cred[item.files[c].guid] = {
cred_pos: i,
on: 'files',
at: c
};
this.parent.FS.getFile(item.files[c]).then((function (data) {
this.parent.step(data);
}).bind(this), (function (error) {
this.parent.stepFailed(error);
}).bind(this));
}
}
// We have finished downloading everything, so let's hand over job to somewhere else!

File diff suppressed because one or more lines are too long

View file

@ -98,10 +98,10 @@ h3 {
width: 100%;
}
}
.setting-group.margin-bottom-25 {
.setting-group.margin-bottom-25, .margin-bottom-25 {
margin-bottom: 25px;
}
.setting-group.margin-bottom-10 {
.setting-group.margin-bottom-10, .margin-bottom-10 {
margin-bottom: 10px;
}
.display-grid {
@ -117,7 +117,7 @@ h3 {
label[for=confirmVaultPWChange] {
margin-bottom: 10px;
}
label[for=confirmVaultDelete] {
label[for=confirmVaultDelete], label[for=skipFirstRow] {
margin-top: 10px;
margin-bottom: 10px;
}

View file

@ -1,9 +1,9 @@
<div ng-controller="GenericCsvImportCtrl">
<div class="row">
<div class="row margin-bottom-25">
<div class="col-xs-12 col-md-3">
<div>{{ 'select.csv' | translate}}
<input type="file" file-select accept=".csv"
success="csvLoaded">
success="csvLoaded" error="fileLoadError" progress="fileSelectProgress">
</div>
<div ng-show="parsed_csv">
<span translate="parsed.csv.rows" translate-value-rows="{{ parsed_csv.length }}">
@ -11,7 +11,8 @@
</span>
</div>
<div ng-show="parsed_csv">
<input type="checkbox" ng-model="skipFirstRow"> {{ 'skip.first.row' | translate}}
<input id="skipFirstRow" class="checkbox" type="checkbox" ng-model="skipFirstRow">
<label for="skipFirstRow">{{'skip.first.row' | translate}}</label>
</div>
<div ng-show="import_fields.indexOf('label') === -1 && parsed_csv">
<b>{{ 'import.csv.label.req' | translate}}</b>
@ -25,40 +26,42 @@
<div progress-bar="import_progress.progress" index="import_progress.loaded" total="import_progress.total"></div>
</div>
</div>
<div>
<div ng-if="log" class="import_log">
<textarea id="import_log" auto-scroll="log">{{log.join('\n')}}</textarea>
</div>
</div>
</div>
<div class="col-xs-12 col-md-9" ng-show="parsed_csv">
<div class="col-xs-12 col-md-9">
<div ng-if="log" class="import_log">
<textarea id="import_log" auto-scroll="log">{{log.join('\n')}}</textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 display-grid" ng-show="parsed_csv">
<b>{{ 'first.five.lines' | translate }}</b><br />
{{ 'assign.column' | translate }}
<div class="import-table-outter">
<table class="import-table">
<tr ng-repeat="line in parsed_csv | limitTo:5">
<td class="inspect"><i class="fa fa-search"
ng-click="inspectCredential(line)"
ng-if="($index > 0 && matched && import_fields.length > 0) || ($index >= 0 && !matched && import_fields.length > 0)"></i>
</td>
<td ng-repeat="prop in line track by $index">
{{line[$index]}}
</td>
</tr>
<tr ng-repeat="line in parsed_csv | limitTo:1">
<td></td>
<td ng-repeat="prop in line track by $index">
<select ng-model="import_fields[$index]" ng-change="updateExample()"
ng-options="property.prop as property.label for property in credentialProperties">
<div class="import-table-outter margin-bottom-25">
<table class="import-table">
<tr ng-repeat="line in parsed_csv | limitTo:5">
<td class="inspect"><i class="fa fa-search"
ng-click="inspectCredential(line)"
ng-if="($index > 0 && matched && import_fields.length > 0) || ($index >= 0 && !matched && import_fields.length > 0)"></i>
</td>
<td ng-repeat="prop in line track by $index">
{{"" + prop | limitTo: 100}}
</td>
</tr>
<tr ng-repeat="line in parsed_csv | limitTo:1">
<td></td>
<td ng-repeat="prop in line track by $index">
<select ng-model="import_fields[$index]" ng-change="updateExample()"
ng-options="property.prop as property.label for property in credentialProperties">
</select>
</td>
</tr>
</table>
</select>
</td>
</tr>
</table>
</div>
<div ng-show="inspected_credential && import_fields.length > 0">
<b>{{ 'example.credential' | translate}}</b>
<div credential-template="inspected_credential" show-label>
<div credential-template credential="inspected_credential" show-label>
</div>
</div>

View file

@ -158,7 +158,7 @@
<h2 class="sidebar-label">{{selectedCredential.label}}</h2>
</div>
<div credential-template="selectedCredential"></div>
<div credential-template credential="selectedCredential"></div>
<div ng-show="selectedCredential">
<div>