Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON import and copy-to-clipboard on click #120

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions css/toastr.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
<link rel="stylesheet" href="css/jquery.mobile-1.4.5.min.css" />
<link rel="stylesheet" href="css/jquery.mobile-custom.min.css" />
<link rel="stylesheet" href="css/styling.css" />
<link rel="stylesheet" href="css/toastr.min.css" />
<!-- purposely at the top -->
<script src="lib/jquery-2.1.3.min.js"></script>
<script src="js/init.js"></script>
<script src="lib/jquery.mobile-1.4.5.min.js"></script>
<script src="lib/jssha-1.31.min.js"></script>
<script src="lib/FileSaver.js"></script>
<script src="lib/toastr.min.js"></script>
<script src="js/gauth.js"></script>
<script src="js/main.js"></script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon.ico" />
Expand Down Expand Up @@ -80,6 +82,10 @@ <h1 data-l10n-id="title-settings">Settings</h1>
<p>
<a id="export" data-l10n-id="settings-export" data-role="button" data-theme="a">Export keys</a>
</p>
<p>
<a id="import" data-l10n-id="settings-import" data-role="button" data-theme="a">Import keys</a>
<input id="import-files" multiple type="file" style="display: none">
</p>
</div>
</section>

Expand Down
109 changes: 107 additions & 2 deletions js/gauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
$.mobile.navigate('#main');
} else {
$('#keySecret').focus();
}
}
});

$('#addKeyCancel').click(function() {
Expand All @@ -162,11 +162,20 @@

var clearAddFields = function() {
$('#keyAccount').val('');
$('#keySecret').val('');
$('#keySecret').val('');
};

$('#edit').click(function() { toggleEdit(); });
$('#export').click(function() { exportAccounts(); });
$('#import').click(function() {
// Sneakily "delegate" the click to an invisible INPUT
// element. That lets us use the input's `files`
// while maintaining a consistent button UI.
$('#import-files').click();
});
$('#import-files').change(function(evt) {
importAccounts(evt);
});
};

var updateKeys = function() {
Expand All @@ -188,6 +197,15 @@
deleteAccount(index);
});
accElem.append(delLink);
} else {
// If not selecting for deletion, copy the key on click.
accElem.click(function () {
navigator.clipboard.writeText(key).then(function () {
toastr.success(`Copied key ${key} for ${account.name}`)
}).catch(function (e) {
toastr.error('Unable to copy key: missing clipboard access permission');
});
});
}

// Add HTML element
Expand All @@ -213,6 +231,93 @@
saveAs(blob, 'gauth-export.json');
};

// Helper to ensure that parsed objects are arrays of
// name/secret keyed objects.
const validateAccountData = function(data) {
if (!(data instanceof Array)) {
throw new SyntaxError("Account list is not an array");
}
if (data.length === 0) {
throw new SyntaxError("Account list is empty");
}
// Helper to check if its argument is a string.
const isNonemptyString = function (val) {
return (typeof val === "string" || val instanceof String) &&
val.length > 0;
}

for (let index = 0; index < data.length; ++index) {
const element = data[index];
const keys = Object.keys(element).sort();
if (keys.length !== 2) {
throw new SyntaxError(`Account data at index ${index} has unexpected entries: ${keys}`);
}
if (keys[0] !== "name") {
throw new SyntaxError(`Account data at index ${index} has no account name`);
} else if(!isNonemptyString(element.name)) {
throw new SyntaxError(`Account data at index ${index} has invalid account name: '${element.name}'`);
}
if (keys[1] !== "secret") {
throw new SyntaxError(`Account data at index ${index} has no account secret`);
} else if(!isNonemptyString(element.secret)) {
throw new SyntaxError(`Account data at index ${index} has invalid account secret: '${element.secret}'`);
}
}
};

// Merge new_data into dest, skipping duplicate secrets.
// This requires both to be valid account lists.
var mergeAccountData = function(dest, new_data) {
const existing = {};
for (const element of dest) {
existing[element.secret] = element.name;
}
for (const element of new_data) {
const prior_name = existing[element.secret];
if (prior_name !== undefined) {
toastr.warning(`Skipping account "${element.name}"; duplicate of "${prior_name}"`);
} else {
dest.push({name: element.name, secret: element.secret});
}
}
};

var importAccounts = async function(evt) {
const files = evt.target.files;
if (files.length === 0) {
toastr.warning("Please select one or more files to upload.");
return;
}
const new_accounts = [];
var valid_files = 0;
for (const file of files) {
const text = await file.text();
try {
const data = JSON.parse(text)
validateAccountData(data);
valid_files += 1;
let len = new_accounts.length;
mergeAccountData(new_accounts, data);
console.log(`Read ${data.length} accounts from ${file.name}, ${new_accounts.length - len} unique`);
} catch (e) {
console.warn(`Error processing ${file.name}`, e);
toastr.error(e, `Import failed on ${file.name}: invalid JSON data.`);
}
}
if (new_accounts.length > 0) {
const accounts = storageService.getObject('accounts');
var len = accounts.length;
mergeAccountData(accounts, new_accounts);
len = accounts.length - len;
console.log(`Added total of new ${len} new accounts`);
storageService.setObject('accounts', accounts);
updateKeys();
toastr.success(`Imported ${new_accounts.length} accounts (${len} new) from ${valid_files} file${valid_files > 1 ? "s" : ""}`);
} else {
toastr.warning('No accounts imported');
}
};

var deleteAccount = function(index) {
// Remove object by index
var accounts = storageService.getObject('accounts');
Expand Down
1 change: 1 addition & 0 deletions lib/toastr.js.map

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions lib/toastr.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.