password-manager/frontend/delta/js/Clipperz/PM/UI/AttachmentController.js
2016-11-27 15:05:10 +01:00

606 lines
20 KiB
JavaScript

/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI');
Clipperz.PM.UI.AttachmentController = function(someParameters) {
this.MAX_SIMULTANEOUS_READ = 1;
this.MAX_SIMULTANEOUS_UPLOAD = 1;
this.MAX_SIMULTANEOUS_DOWNLOAD = 1;
this.MAX_SIMULTANEOUS_ENCRYPT = 1;
this.MAX_SIMULTANEOUS_DECRYPT = 1;
this.LATEST_ENCRYPTION_VERSION = '1.0'; // Versions aren't handled completely yet!
this.fileQueue = [];
// this.notifications = [];
this.operationsCount = null;
this.encryptedDocument = null;
this.uploadMessageCallback = someParameters['uploadMessageCallback'];
this.downloadMessageCallback = someParameters['downloadMessageCallback'];
this.reloadServerStatusCallback = someParameters['reloadServerStatusCallback'];
// this.cryptoObject = window.crypto || window.msCrypto; // Not needed anymore because of polyfill
this.cryptoObject = window.crypto
return this;
}
MochiKit.Base.update(Clipperz.PM.UI.AttachmentController.prototype, {
toString: function () {
return "Clipperz.PM.UI.AttachmentController";
},
//-------------------------------------------------------------------------
/*
notifyUpdate: function() {
// MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateAttachmentQueueInfo', this.getQueueInfo(), this.getNotificationsInfo());
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateNotifications');
},
*/
getQueueInfo: function() {
return this.fileQueue;
},
/*
getNotificationsInfo: function() {
return this.notifications;
},
*/
//=========================================================================
// Entry points
//=========================================================================
addAttachment: function (anAttachment) {
var deferredResult;
var actualDeferredResult;
actualDeferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.uploadAttachment-result", {trace:false});
actualDeferredResult.addMethod(anAttachment, 'setValue', 'hash');
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.uploadAttachment", {trace:false});
deferredResult.collectResults({
'_attachment': MochiKit.Base.partial(MochiKit.Async.succeed, anAttachment),
'_record': MochiKit.Base.method(anAttachment, 'record'),
'reference': MochiKit.Base.method(anAttachment, 'reference'),
'meta': MochiKit.Base.method(anAttachment, 'metadata'),
'key': MochiKit.Base.method(anAttachment, 'key'),
'nonce': MochiKit.Base.method(anAttachment, 'nonce'),
'status': MochiKit.Base.partial(MochiKit.Async.succeed, 'WAITING_READ'),
'file': MochiKit.Base.method(anAttachment, 'file'),
'recordReference': MochiKit.Base.method(anAttachment.record(), 'reference'),
'process': MochiKit.Base.partial(MochiKit.Async.succeed, 'UPLOAD'), // Used only to differentiate notifications
}, {trace: false});
deferredResult.addCallback(function (someInfo) { someInfo['deferredResult'] = actualDeferredResult; return someInfo; });
deferredResult.addMethod(this, 'addFileToQueue');
deferredResult.callback();
// return deferredResult;
return actualDeferredResult;
},
getAttachment: function(anAttachment, aMessageCallback) {
if (this.getQueuePosition(anAttachment.reference()) >= 0) {
this.removeFileFromQueue(anAttachment.reference());
}
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.downloadAttachment", {trace:false});
deferredResult.collectResults({
'_attachment': MochiKit.Base.partial(MochiKit.Async.succeed, anAttachment),
'_record': MochiKit.Base.method(anAttachment, 'record'),
'reference': MochiKit.Base.method(anAttachment, 'reference'),
'meta': MochiKit.Base.method(anAttachment, 'metadata'),
'key': MochiKit.Base.method(anAttachment, 'key'),
'nonce': MochiKit.Base.method(anAttachment, 'nonce'),
'status': MochiKit.Base.partial(MochiKit.Async.succeed, 'WAITING_DOWNLOAD'),
'messageCallback': MochiKit.Async.succeed(aMessageCallback),
'process': MochiKit.Base.partial(MochiKit.Async.succeed, 'DOWNLOAD'), // Used only to differentiate notifications
}, {trace: false});
deferredResult.addCallback(function(aResult){
MochiKit.Base.update(aResult, {'messageCallback': aMessageCallback});
return aResult;
});
deferredResult.addMethod(this, 'addFileToQueue');
deferredResult.callback();
return deferredResult;
},
cancelAttachment: function(anAttachment) {
var deferredResult;
var reference = anAttachment.reference()
var queueElement = this.getQueueElement(reference);
var isElementInProgress = (queueElement && queueElement['status'] != 'DONE' && queueElement['status'] != 'CANCELED' && queueElement['status'] != 'FAILED');
if (isElementInProgress) {
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.cancelAttachment", {trace:false});
deferredResult.addMethod(this, 'updateFileInQueue', reference, {'status': 'CANCELED'});
if (queueElement['deferredRequest']) {
deferredResult.addMethod(queueElement['deferredRequest'], 'cancel');
}
deferredResult.callback();
// TODO: We may also want do delete stuff in the queue element
} else {
deferredResult = MochiKit.Async.succeed();
}
return deferredResult;
},
//=========================================================================
// Queue management
//=========================================================================
dispatchQueueOperations: function() {
var currentElement;
var processNextElements;
var count = this.updateOperationsCount();
// this.notifyUpdate();
processNextElements = true;
for (i in this.fileQueue) {
if (processNextElements) {
currentElement = this.fileQueue[i];
switch (currentElement['status']) {
case 'WAITING_READ':
if ((count['READING']) < this.MAX_SIMULTANEOUS_READ) {
this.readFile(currentElement['reference'], currentElement['file']);
processNextElements = false;
}
break;
case 'WAITING_ENCRYPT':
if (count['ENCRYPTING'] < this.MAX_SIMULTANEOUS_ENCRYPT) {
this.encryptFile(currentElement['reference'], currentElement['originalArray'], currentElement['key'], currentElement['nonce']);
processNextElements = false;
}
break;
case 'WAITING_UPLOAD':
if (count['UPLOADING'] < this.MAX_SIMULTANEOUS_UPLOAD) {
this.uploadFile(currentElement['reference'], currentElement['encryptedArray']);
processNextElements = false;
}
break;
case 'WAITING_DOWNLOAD':
if (count['DOWNLOADING'] < this.MAX_SIMULTANEOUS_DOWNLOAD) {
this.downloadFile(currentElement['reference'], currentElement['messageCallback']);
processNextElements = false;
}
break;
case 'WAITING_DECRYPT':
if (count['DECRYPTING'] < this.MAX_SIMULTANEOUS_DECRYPT) {
this.decryptFile(currentElement['reference'], currentElement['encryptedArray'], currentElement['key'], currentElement['nonce']);
processNextElements = false;
}
break;
case 'WAITING_SAVE':
this.saveFile(currentElement['reference'], currentElement['decryptedArray'], currentElement['meta']['name'], currentElement['meta']['type']);
processNextElements = false;
// Clipperz.Sound.beep();
break;
}
}
}
},
updateOperationsCount: function() {
var count;
count = {
'WAITING_READ': 0,
'READING': 0,
'WAITING_ENCRYPT': 0,
'ENCRYPTING': 0,
'WAITING_UPLOAD': 0,
'UPLOADING': 0,
'WAITING_DOWNLOAD': 0,
'DOWNLOADING': 0,
'WAITING_DECRYPT': 0,
'DECRYPTING': 0,
'WAITING_SAVE': 0,
'DONE': 0,
'CANCELED': 0,
'FAILED': 0,
};
for (var i in this.fileQueue) {
count[this.fileQueue[i]['status']]++;
}
this.operationsCount = count;
return this.operationsCount;
},
addFileToQueue: function(someParameters) {
this.fileQueue.push(someParameters);
this.dispatchQueueOperations();
},
removeFileFromQueue: function(aFileReference) {
this.fileQueue.splice(this.getQueuePosition(aFileReference), 1);
this.dispatchQueueOperations();
},
getQueueElement: function(aFileReference) {
var i = this.getQueuePosition(aFileReference);
return this.fileQueue[i];
},
updateFileInQueue: function(aFileReference, someParameters) {
var queuePosition = this.getQueuePosition(aFileReference);
MochiKit.Base.update(this.fileQueue[queuePosition], someParameters);
if ((someParameters['status'] == 'DONE') || (someParameters['status'] == 'FAILED')) {
this.addNotification(this.fileQueue[queuePosition], someParameters['status']);
}
this.dispatchQueueOperations();
},
appendResult: function(aFileReference, anArray) {
var queueElement = this.getQueueElement(aFileReference);
queueElement['result'].set(anArray, queueElement['currentByte']);
},
getQueuePosition: function(aFileReference) {
var result;
result = -1;
for (var i in this.fileQueue) {
if (this.fileQueue[i].reference == aFileReference) {
result = i;
}
}
return result;
},
//=========================================================================
// Notifications
//=========================================================================
addNotification: function(aQueueElement, aStatus) {
//console.log("ADD ATTACHMENT NOTIFICATION", aQueueElement);
var messagePrefix = "";
var message;
var filename;
var level;
if (aStatus == 'DONE') {
level = 'info';
if (aQueueElement['process'] == 'DOWNLOAD') {
messagePrefix = "downloaded attachment ";
} else if (aQueueElement['process'] == 'UPLOAD') {
messagePrefix = "uploaded attachment ";
}
} else if (aStatus == 'FAILED') {
level = 'error';
if (aQueueElement['process'] == 'DOWNLOAD') {
messagePrefix = "error downloading attachment ";
} else if (aQueueElement['process'] == 'UPLOAD') {
messagePrefix = "error uploading attachment ";
}
} else {
level = 'warning';
messagePrefix = "???";
}
filename = aQueueElement['meta']['name'];
message = messagePrefix + filename;
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'addNotification', {'message':message, 'level':level});
},
/*
removeNotification: function(aNotificationId) {
var i, position;
position = -1;
for (i in this.notifications) {
if (this.notifications[i]['id'] == aNotificationId) {
position = i;
}
}
if (position >= 0) {
this.notifications.splice(position, 1);
}
// this.notifyUpdate();
},
*/
/*
randomId: function() {
return Clipperz.Crypto.randomKey();
// return Clipperz.Crypto.PRNG.defaultRandomGenerator().getRandomBytes(32).toHexString().substring(2);
},
*/
//=========================================================================
// Queue Processing: READ
//=========================================================================
readFile: function(aFileReference, aFile) {
var reader = new FileReader();
this.updateFileInQueue(aFileReference, {
'status': 'READING',
});
reader.onload = MochiKit.Base.method(this, 'readFileOnload', aFileReference);
reader.readAsArrayBuffer(aFile);
},
readFileOnload: function(aFileReference, anEvent) {
var fileContent;
var fileContentSha256Hash;
fileContent = new Uint8Array(anEvent.target.result);
fileContentSha256Hash = npm.bitcoin.crypto.sha256(fileContent).toString('hex');
this.getQueueElement(aFileReference)['deferredResult'].callback(fileContentSha256Hash);
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_ENCRYPT',
'originalArray': fileContent,
})
},
//=========================================================================
// Queue Processing: ENCRYPT
//=========================================================================
encryptFile: function(aFileReference, anArrayBuffer, aKey, aNonce) {
this.updateFileInQueue(aFileReference, {
'status': 'ENCRYPTING',
});
this.cryptoObject.subtle.importKey(
"raw",
aKey, //this is an example jwk key, "raw" would be an ArrayBuffer
{ name: "AES-CBC" }, //this is the algorithm options
false, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
// .then(MochiKit.Base.method(this, 'doEncrypt', aFileReference, anArrayBuffer, aNonce))
.then(this.doEncrypt.bind(this,aFileReference, anArrayBuffer, aNonce))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'encryptFile(): encryption failed'));
},
doEncrypt: function(aFileReference, anArrayBuffer, anIV, aWebcryptoKey) {
this.cryptoObject.subtle.encrypt(
{
name: "AES-CBC",
iv: anIV,
},
aWebcryptoKey,
anArrayBuffer
)
.then(MochiKit.Base.method(this, 'doneEncrypt', aFileReference))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'doEncrypt(): encryption failed'));
},
doneEncrypt: function(aFileReference, anArrayBuffer) {
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_UPLOAD',
'encryptedArray': new Uint8Array(anArrayBuffer),
});
},
//=========================================================================
// Queue Processing: UPLOAD
//=========================================================================
uploadFile: function(aFileReference, anEncryptedArray) {
this.updateFileInQueue(aFileReference, {
'status': 'UPLOADING',
'deferredRequest': this.uploadFileRequest(aFileReference, anEncryptedArray),
'requestProgress': 0,
});
},
uploadFileRequest: function(aFileReference, anEncryptedArray) {
var deferredResult;
var queueElement = this.getQueueElement(aFileReference);
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.uploadFileRequest", {trace:false});
deferredResult.addCallback(this.uploadMessageCallback, {
'attachmentReference': queueElement['_attachment'].reference(),
'recordReference': queueElement['_attachment'].record().reference(),
'arrayBufferData': anEncryptedArray,
'version': this.LATEST_ENCRYPTION_VERSION,
}, MochiKit.Base.method(this, 'uploadFileProgress', aFileReference));
deferredResult.addMethod(this, 'uploadFileDone', aFileReference);
deferredResult.addErrback(MochiKit.Base.method(this, 'handleException', aFileReference, 'uploadFileRequest(): request failed or canceled'));
deferredResult.callback();
return deferredResult;
},
uploadFileDone: function(aFileReference, aResult){
var record = this.getQueueElement(aFileReference)['_record'];
return Clipperz.Async.callbacks("AttachmentController.uploadFileDone", [
MochiKit.Base.partial(this.reloadServerStatusCallback, record),
MochiKit.Base.method(this, 'updateFileInQueue', aFileReference, {
'status': 'DONE',
'requestProgress': 1,
}),
// add notification
], {trace:false});
},
uploadFileProgress: function(aFileReference, anEvent) {
var newProgress = (anEvent.lengthComputable) ? (anEvent.loaded / anEvent.total) : -1;
this.updateFileInQueue(aFileReference, {
'requestProgress': newProgress,
});
},
//=========================================================================
// Queue Processing: DOWNLOAD
//=========================================================================
downloadFile: function(aFileReference) {
var deferredRequest;
var queueElement = this.getQueueElement(aFileReference);
deferredRequest = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.downloadFile", {trace:false});
deferredRequest.addCallback(this.downloadMessageCallback, queueElement['_attachment'], MochiKit.Base.method(this, 'downloadFileProgress', aFileReference));
deferredRequest.addMethod(this, 'downloadFileDone', aFileReference);
deferredRequest.addErrback(MochiKit.Base.method(this, 'handleException', aFileReference, 'downloadFile(): download filed or canceled'));
deferredRequest.callback();
this.updateFileInQueue(aFileReference, {
'status': 'DOWNLOADING',
'deferredRequest': deferredRequest,
'requestProgress': 0,
});
},
downloadFileDone: function(aFileReference, aResult){
var queueElement = this.getQueueElement(aFileReference);
var encryptedArray = new Uint8Array(aResult);
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_DECRYPT',
'key': queueElement['key'],
'nonce': queueElement['nonce'],
'encryptedArray': encryptedArray,
'requestProgress': 1,
});
},
downloadFileProgress: function(aFileReference, anEvent) {
var newProgress = (anEvent.lengthComputable) ? (anEvent.loaded / anEvent.total) : -1;
this.updateFileInQueue(aFileReference, {
'requestProgress': newProgress,
});
},
//=========================================================================
// Queue Processing: DECRYPT
//=========================================================================
decryptFile: function(aFileReference, anArrayBuffer, aKey, aNonce) {
this.updateFileInQueue(aFileReference, {
'status': 'DECRYPTING',
});
this.cryptoObject.subtle.importKey(
"raw",
aKey, //this is an example jwk key, "raw" would be an ArrayBuffer
{name: "AES-CBC"}, //this is the algorithm options
false, //whether the key is extractable (i.e. can be used in exportKey)
["decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
.then(MochiKit.Base.method(this, 'doDecrypt', aFileReference, anArrayBuffer, aNonce))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'decryptFile(): decryption failed'));
},
doDecrypt: function(aFileReference, anArrayBuffer, anIV, aWebcryptoKey) {
this.cryptoObject.subtle.decrypt(
{name: "AES-CBC", iv: anIV},
aWebcryptoKey,
anArrayBuffer
)
.then(MochiKit.Base.method(this, 'doneDecrypt', aFileReference))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'doDecrypt(): decryption failed'));
},
doneDecrypt: function(aFileReference, anArrayBuffer) {
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_SAVE',
'decryptedArray': new Uint8Array(anArrayBuffer),
});
},
//=========================================================================
// Queue Processing: SAVE
//=========================================================================
saveFile: function(aFileReference, anArray, aFileName, aFileType) {
var blob = new Blob([anArray], {type: aFileType});
saveAs(blob, aFileName);
this.updateFileInQueue(aFileReference, {
'status': 'DONE',
});
},
//=========================================================================
// Exceptions
//=========================================================================
/** Handles exceptions for upload/download and encrypt/decrypt. Note that
* an exception is thrown also when the user manually cancels the file
* processing. In this case the status remains 'CANCELED'.
*/
handleException: function(aFileReference, aMessage, anException) {
var queueElement = this.getQueueElement(aFileReference);
var messageString = aMessage ? " (" + aMessage + ")" : "";
try {
if (Clipperz.Base.evalJSON(anException['req']['response'])['message'] == "not enough space available for uploading the attachment") {
console.log("NOT ENOUGH ATTACHMENT QUOTA EXCEPTION");
}
} catch (exception) {
}
if (queueElement['status'] != 'CANCELED') {
this.updateFileInQueue(aFileReference, {
'status': 'FAILED',
});
}
if (aMessage) {
console.log("AttachmentController: caught exception" + messageString + ":", anException);
}
},
//=========================================================================
__syntaxFix__: "syntax fix"
});