diff --git a/gulpfile.js b/gulpfile.js index b56d022aa85..a12dbf96fb1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -123,6 +123,10 @@ gulp.task('lib', ['clean:lib'], function () { { src: paths.npmDir + 'ng-infinite-scroll/build/ng-infinite-scroll.js', dest: paths.libDir + 'ng-infinite-scroll' + }, + { + src: paths.npmDir + 'papaparse/papaparse*.js', + dest: paths.libDir + 'papaparse' } ]; diff --git a/package.json b/package.json index 57f4feb7334..93b755be65f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "ng-infinite-scroll": "1.3.0", "node-forge": "0.7.0", "webpack-stream": "3.2.0", - "gulp-json-editor": "2.2.1" + "gulp-json-editor": "2.2.1", + "papaparse": "4.2.0" } } diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index e185cc33b2c..a59ec73beb0 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -654,5 +654,25 @@ "disableContextMenuItemDesc": { "message": "Context menu options provide quick access to password generation and logins for the website in your current tab.", "desription": "Context menu options provide quick access to password generation and logins for the website in your current tab." + }, + "exportVault": { + "message": "Export Vault", + "desription": "Export Vault" + }, + "warning": { + "message": "WARNING", + "desription": "WARNING (should stay in capitalized letters if the language permits)" + }, + "exportWarning": { + "message": "This export contains your unencrypted data in .csv format. You should not store or send it over unsecure channels (such as email). Delete it immediately after your are done using it.", + "desription": "This export contains your unencrypted data in .csv format. You should not store or send it over unsecure channels (such as email). Delete it immediately after your are done using it." + }, + "exportMasterPassword": { + "message": "Enter your master password to export your vault data.", + "desription": "Enter your master password to export your vault data." + }, + "exportVaultInfo": { + "message": "Export your vault data in .csv format so that you can easily modify it or move it elsewhere.", + "desription": "Export your vault data in .csv format so that you can easily modify it or move it elsewhere." } } diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json index ff9ee0530e3..b1b1440aa3c 100644 --- a/src/_locales/fr/messages.json +++ b/src/_locales/fr/messages.json @@ -652,7 +652,7 @@ "description": "Disable Context Menu Options" }, "disableContextMenuItemDesc": { - "message": "Les options de menu contextuelles permettent un accès rapide à la génération de mots de passe et d\'identifiants pour le site web de l\'onglet actuel", + "message": "Les options de menu contextuelles permettent un accès rapide à la génération de mots de passe et d'identifiants pour le site web de l'onglet actuel", "desription": "Context menu options provide quick access to password generation and logins for the website in your current tab." } } diff --git a/src/popup/app/config.js b/src/popup/app/config.js index c4340a907d9..39af96b6345 100644 --- a/src/popup/app/config.js +++ b/src/popup/app/config.js @@ -141,6 +141,13 @@ data: { authorize: true }, params: { animation: null, addState: null, editState: null } }) + .state('export', { + url: '/export', + templateUrl: 'app/tools/views/toolsExport.html', + controller: 'toolsExportController', + data: { authorize: true }, + params: { animation: null } + }) .state('about', { url: '/about', diff --git a/src/popup/app/tools/toolsExportController.js b/src/popup/app/tools/toolsExportController.js new file mode 100644 index 00000000000..4658779bfe9 --- /dev/null +++ b/src/popup/app/tools/toolsExportController.js @@ -0,0 +1,116 @@ +angular + .module('bit.tools') + + .controller('toolsExportController', function ($scope, $state, toastr, $q, $analytics, + i18nService, cryptoService, userService, folderService, loginService) { + $scope.i18n = i18nService; + + $scope.submitPromise = null; + $scope.submit = function () { + $scope.submitPromise = checkPassword().then(function () { + return getCsv(); + }).then(function (csv) { + downloadFile(csv); + }, function () { + toastr.error(i18nService.invalidMasterPassword, i18nService.errorsOccurred); + }); + }; + + function checkPassword() { + var deferred = $q.defer(); + + userService.getEmail(function (email) { + var key = cryptoService.makeKey($scope.masterPassword, email); + cryptoService.hashPassword($scope.masterPassword, key, function (keyHash) { + cryptoService.getKeyHash(true, function (storedKeyHash) { + if (storedKeyHash && keyHash && storedKeyHash === keyHash) { + deferred.resolve(); + } + else { + deferred.reject(); + } + }); + }); + }); + + return deferred.promise; + } + + function getCsv() { + var deferred = $q.defer(); + var decFolders = []; + var decLogins = []; + var promises = []; + + var folderPromise = $q.when(folderService.getAllDecrypted()); + folderPromise.then(function (folders) { + decFolders = folders; + }); + promises.push(folderPromise); + + var loginPromise = $q.when(loginService.getAllDecrypted()); + loginPromise.then(function (logins) { + decLogins = logins; + }); + promises.push(loginPromise); + + $q.all(promises).then(function () { + var exportLogins = []; + for (var i = 0; i < decLogins.length; i++) { + var login = { + name: decLogins[i].name, + uri: decLogins[i].uri, + username: decLogins[i].username, + password: decLogins[i].password, + notes: decLogins[i].notes, + folder: null + }; + + for (var j = 0; j < decFolders.length; j++) { + if (decFolders[j].id === decLogins[i].folderId && decFolders[j].name !== i18nService.noneFolder) { + login.folder = decFolders[j].name; + break; + } + } + + exportLogins.push(login); + } + + var csv = Papa.unparse(exportLogins); + deferred.resolve(csv); + }); + + return deferred.promise; + } + + function downloadFile(csvString) { + var csvBlob = new Blob([csvString]); + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(csvBlob, makeFileName()); + } + else { + var a = window.document.createElement('a'); + a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); + a.download = makeFileName(); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } + + function makeFileName() { + var now = new Date(); + var dateString = + now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + + padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + function padNumber(number, width, paddingCharacter) { + paddingCharacter = paddingCharacter || '0'; + number = number + ''; + return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; + } + }); diff --git a/src/popup/app/tools/views/tools.html b/src/popup/app/tools/views/tools.html index d63768696bf..136aed4642e 100644 --- a/src/popup/app/tools/views/tools.html +++ b/src/popup/app/tools/views/tools.html @@ -30,6 +30,11 @@ {{i18n.importLogins}} {{i18n.importLoginsInfo}} + + + {{i18n.exportVault}} + {{i18n.exportVaultInfo}} + diff --git a/src/popup/app/tools/views/toolsExport.html b/src/popup/app/tools/views/toolsExport.html new file mode 100644 index 00000000000..f05232e6b7e --- /dev/null +++ b/src/popup/app/tools/views/toolsExport.html @@ -0,0 +1,29 @@ +
diff --git a/src/popup/index.html b/src/popup/index.html index 8c860045cb1..7285483ab85 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -14,6 +14,7 @@ + @@ -82,6 +83,7 @@ + diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js index 81bed737d66..b521bf24dc8 100644 --- a/src/services/cryptoService.js +++ b/src/services/cryptoService.js @@ -131,6 +131,7 @@ function initCryptoService() { if (b64 && b64 === true && _b64KeyHash) { callback(_b64KeyHash); + return; } else if (!b64 && _keyHash) { callback(_keyHash);