"use strict"; /*jslint indent: 2, maxlen: 80, nomen: true */ /*global jIO: true, btoa: true, b64_hmac_sha1: true */ /** * JIO S3 Storage. Type = "s3". * Amazon S3 "database" storage. */ jIO.addStorageType("s3", function (spec, my) { var evt; var that, priv = {}; spec = spec || {}; that = my.basicStorage(spec, my); // attributes priv.username = spec.username || ''; priv.AWSIdentifier = spec.AWSIdentifier || ''; priv.password = spec.password || ''; priv.server = spec.server || ''; /*|| jiobucket ||*/ priv.url = spec.url || ''; /*||> https://s3-eu-west-1.amazonaws.com <||*/ priv.acl = spec.acl || ''; /*||> "private, public-read, public-read-write, authenticated-read, bucket-owner-read, bucket-owner-full-control" <||*/ priv.actionStatus = spec.actionStatus || ''; priv.contenTType = spec.contenTType || ''; /** * Update [doc] the document object and remove [doc] keys * which are not in [new_doc]. It only changes [doc] keys not starting * with an underscore. * ex: doc: {key:value1,_key:value2} with * new_doc: {key:value3,_key:value4} updates * doc: {key:value3,_key:value2}. * @param {object} doc The original document object. * @param {object} new_doc The new document object **/ priv.secureDocId = function (string) { var split = string.split('/'), i; if (split[0] === '') { split = split.slice(1); } for (i = 0; i < split.length; i += 1) { if (split[i] === '') { return ''; } } return split.join('%2F'); }; /** * Replace substrings to another strings * @method recursiveReplace * @param {string} string The string to do replacement * @param {array} list_of_replacement An array of couple * ["substring to select", "selected substring replaced by this string"]. * @return {string} The replaced string */ priv.recursiveReplace = function (string, list_of_replacement) { var i, split_string = string.split(list_of_replacement[0][0]); if (list_of_replacement[1]) { for (i = 0; i < split_string.length; i += 1) { split_string[i] = priv.recursiveReplace( split_string[i], list_of_replacement.slice(1) ); } } return split_string.join(list_of_replacement[0][1]); }; /** * Changes / to %2F, % to %25 and . to _. * @method secureName * @param {string} name The name to secure * @return {string} The secured name */ priv.secureName = function (name) { return priv.recursiveReplace(name, [["/", "%2F"], ["%", "%25"]]); }; /** * Restores the original name from a secured name * @method restoreName * @param {string} secured_name The secured name to restore * @return {string} The original name */ priv.restoreName = function (secured_name) { return priv.recursiveReplace(secured_name, [["%2F", "/"], ["%25", "%"]]); }; /** * Convert document id and attachment id to a file name * @method idsToFileName * @param {string} doc_id The document id * @param {string} attachment_id The attachment id (optional) * @return {string} The file name */ priv.idsToFileName = function (doc_id, attachment_id) { doc_id = priv.secureName(doc_id).split(".").join("_."); if (typeof attachment_id === "string") { attachment_id = priv.secureName(attachment_id).split(".").join("_."); return doc_id + "." + attachment_id; } return doc_id; }; /** * Convert a file name to a document id (and attachment id if there) * @method fileNameToIds * @param {string} file_name The file name to convert * @return {array} ["document id", "attachment id"] or ["document id"] */ priv.fileNameToIds = function (file_name) { var separator_index = -1, split = file_name.split("."); split.slice(0, -1).forEach(function (file_name_part, index) { if (file_name_part.slice(-1) !== "_") { separator_index = index; } }); if (separator_index === -1) { return [priv.restoreName(priv.restoreName( file_name ).split("_.").join("."))]; } return [ priv.restoreName(priv.restoreName( split.slice(0, separator_index + 1).join(".") ).split("_.").join(".")), priv.restoreName(priv.restoreName( split.slice(separator_index + 1).join(".") ).split("_.").join(".")) ]; }; /** * Removes the last character if it is a "/". "/a/b/c/" become "/a/b/c" * @method removeSlashIfLast * @param {string} string The string to modify * @return {string} The modified string */ priv.removeSlashIfLast = function (string) { if (string[string.length - 1] === "/") { return string.slice(0, -1); } return string; }; that.documentObjectUpdate = function (doc, new_doc) { var k; for (k in doc) { if (doc.hasOwnProperty(k)) { if (k[0] !== '_') { delete doc[k]; } } } for (k in new_doc) { if (new_doc.hasOwnProperty(k)) { if (k[0] !== '_') { doc[k] = new_doc[k]; } } } }; /** * Checks if an object has no enumerable keys * @method objectIsEmpty * @param {object} obj The object * @return {boolean} true if empty, else false */ that.objectIsEmpty = function (obj) { var k; for (k in obj) { if (obj.hasOwnProperty(k)) { return false; } } return true; }; // ===================== overrides ====================== that.specToStore = function () { return { "username": priv.username, "password": priv.password, "url": priv.url, "server": priv.server, "acl": priv.acl }; }; that.validateState = function () { // xxx complete error message // jjj completion below if (typeof priv.AWSIdentifier === "string" && priv.AWSIdentifier === '') { return 'Need at least one parameter "Aws login".'; } if (typeof priv.password === "string" && priv.password === '') { return 'Need at least one parameter "password".'; } if (typeof priv.url === "string" && priv.url === '') { return 'Need at least one parameter "url".'; } if (typeof priv.server === "string" && priv.server === '') { return 'Need at least one parameter "server".'; } return ''; }; // =================== S3 Specifics ================= /** * Encoding the signature using a stringToSign * Encoding the policy * @method buildStringToSign * @param {string} http_verb The HTTP method * @param {string} content_md5 The md5 content * @param {string} content_type The content type * @param {number} expires The expires time * @param {string} x_amz_headers The specific amazon headers * @param {string} path_key The path of the document * @return {string} The generated signature */ // xxx no need to make it public, use private -> "priv" (not "that") priv.buildStringToSign = function (http_verb, content_md5, content_type, expires, x_amz_headers, path_key) { //example : // var StringToSign = S3.buildStringToSign(S3.httpVerb,'','','', // 'x-amz-date:'+S3.requestUTC,'/jio1st/prive.json'); var StringToSign = http_verb + '\n' + content_md5 + '\n'//content-md5 + content_type + '\n'//content-type + expires + '\n'//expires + x_amz_headers + '\n'//x-amz headers + path_key;//path key return StringToSign; }; that.encodePolicy = function (form) { //generates the policy //enables the choice for the http response code var http_code = ''; var s3_policy = { "expiration": "2020-01-01T00:00:00Z", "conditions": [ {"bucket": priv.server }, ["starts-with", "$key", ""], {"acl": priv.acl }, {"success_action_redirect": ""}, {"success_action_status": http_code }, ["starts-with", "$Content-Type", ""], ["content-length-range", 0, 524288000] ] }; //base64 encoding of the policy (native base64 js >> // .btoa() = encode, .atob() = decode) priv.b64_policy = btoa(JSON.stringify(s3_policy)); //generates the signature value using the policy and the secret access key //use of sha1.js to generate the signature var Signature = that.signature(priv.b64_policy); }; that.signature = function (string) { var Signature = b64_hmac_sha1(priv.password, string); return Signature; }; function xhr_onreadystatechange(docId, command, obj, http, jio, isAttachment, callback) { obj.onreadystatechange = function () { if (obj.readyState === 4) { if (this.status === 204 || this.status === 201 || this.status === 200) { switch (http) { case "POST": that.success({ ok: true, id: docId }); break; case 'PUT': if (jio === true) { that.success({ ok: true, id: command.getDocId() }); } else { callback(this.responseText); } break; case 'GET': if (jio === true) { if (typeof this.responseText !== 'string') { var response = JSON.parse(this.responseText); response._attachments = response._attachments || {}; delete response._attachments; that.success(JSON.stringify(response)); } else { if (isAttachment === true) { that.success(this.responseText); } else { that.success(JSON.parse(this.responseText)); } } } else { callback(this.responseText); } break; case 'DELETE': if (jio === true) { if (isAttachment === false) { that.success({ ok: true, id: command.getDocId() }); } else { that.success({ ok: true, id: command.getDocId(), attachment: command.getAttachmentId() }); } } else { callback(this.responseText); } break; } } else { var err = this; if (this.status === 405) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" err.error = "not_allowed"; that.error(err); } if (this.status === 404) { if (http === 'GET') { if (jio === true) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" err.statustext = "not_foud"; err.reason = "file does not exist"; err.error = "not_found"; that.error(err); } else { callback('404'); } } else { //status //statustext "Not Found" //error //reason "reason" //message "did not work" err.error = "not_found"; that.error(err); } } if (this.status === 409) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" err.error = "already_exists"; that.error(err); } } } }; } priv.updateMeta = function (doc, docid, attachid, action, data) { doc._attachments = doc._attachments || {}; switch (action) { case "add": doc._attachments[attachid] = data; //nothing happens doc = JSON.stringify(doc); break; case "remove": //if (typeof doc._attachments !== 'undefined') { if (doc._attachments !== 'undefined') { delete doc._attachments[attachid]; } doc = JSON.stringify(doc); break; case "update": console.log(doc._attachments); doc._attachments[attachid] = data; console.log(doc._attachments); //update happened in the put request doc = JSON.stringify(doc); break; } return doc; }; priv.createError = function (status, message, reason) { var error = { "status": status, "message": message, "reason": reason }; switch (status) { case 404: error.statusText = "Not found"; break; case 405: error.statusText = "Method Not Allowed"; break; case 409: error.statusText = "Conflicts"; break; case 24: error.statusText = "Corrupted Document"; break; } error.error = error.statusText.toLowerCase().split(" ").join("_"); return error; }; that.encodeAuthorization = function (key, mime) { //GET oriented method var requestUTC = new Date().toUTCString(); var httpVerb = "GET"; var StringToSign = priv.buildStringToSign( httpVerb, '', 'application/json', '', 'x-amz-date:' + requestUTC, '/' + priv.server + '/' + key ); var Signature = b64_hmac_sha1(priv.password, StringToSign); return Signature; }; function XHRwrapper(command, docId, attachId, http, mime, data, jio, is_attachment, callback) { var docFile = priv.secureName(priv.idsToFileName(docId, attachId || undefined)); var requestUTC = new Date().toUTCString(); var StringToSign = priv.buildStringToSign( http, '', mime, '', 'x-amz-date:' + requestUTC, '/' + priv.server + '/' + docFile ); var url = 'http://s3.amazonaws.com/' + priv.server + '/' + docFile; var Signature = b64_hmac_sha1(priv.password, StringToSign); var xhr, XMLHttpRequest; xhr = new XMLHttpRequest(); xhr.open(http, url, true); xhr.setRequestHeader("HTTP-status-code", "100"); xhr.setRequestHeader("x-amz-date", requestUTC); xhr.setRequestHeader("Authorization", "AWS " + priv.AWSIdentifier + ":" + Signature); xhr.setRequestHeader("Content-Type", mime); xhr.responseType = 'text'; xhr_onreadystatechange(docId, command, xhr, http, jio, is_attachment, callback); if (http === 'PUT') { xhr.send(data); } else { xhr.send(null); } } // ==================== commands ==================== /** * Create a document in local storage. * @method post * @param {object} command The JIO command **/ that.post = function (command) { //as S3 encoding key are directly inserted within the FormData(), //use of XHRwrapper function ain't pertinent var doc, doc_id; doc = command.cloneDoc(); doc_id = command.getDocId(); function postDocument() { doc_id = priv.secureName(priv.idsToFileName(doc_id)); //Meant to deep-serialize in order to avoid //conflicts due to the multipart enctype doc = JSON.stringify(doc); var http_response = ''; var fd, FormData; fd = new FormData(); //virtually builds the form fields //filename fd.append('key', doc_id); //file access authorizations priv.acl = ""; fd.append('acl', priv.acl); //content-type priv.contenTType = "text/plain"; fd.append('Content-Type', priv.contenTType); //allows specification of a success url redirection fd.append('success_action_redirect', ''); //allows to specify the http code response if the request is successful fd.append('success_action_status', http_response); //login AWS fd.append('AWSAccessKeyId', priv.AWSIdentifier); //exchange policy with the amazon s3 service //can be common to all uploads or specific that.encodePolicy(fd); //priv.b64_policy = that.encodePolicy(fd); fd.append('policy', priv.b64_policy); //signature through the base64.hmac.sha1(secret key, policy) method var Signature = b64_hmac_sha1(priv.password, priv.b64_policy); fd.append('signature', Signature); //uploaded content !!may must be a string rather than an object fd.append('file', doc); var xhr, XMLHttpRequest; xhr = new XMLHttpRequest(); xhr_onreadystatechange(doc_id, command, xhr, 'POST', true, false, ''); xhr.open('POST', 'https://' + priv.server + '.s3.amazonaws.com/', true); xhr.send(fd); } if (doc_id === '' || doc_id === undefined) { doc_id = 'no_document_id_' + ((Math.random() * 10).toString().split('.'))[1]; doc._id = doc_id; } var mime = 'text/plain; charset=UTF-8'; XHRwrapper(command, doc_id, '', 'GET', mime, '', false, false, function (response) { if (response === '404') { postDocument(); } else { //si ce n'est pas une 404, //alors on renvoit une erreur 405 return that.error(priv.createError( 409, "Cannot create document", "Document already exists" )); } } ); }; /** * Get a document or attachment * @method get * @param {object} command The JIO command **/ that.get = function (command) { var docId = command.getDocId(); var attachId = command.getAttachmentId() || ''; var isJIO = true; var mime = 'text/plain; charset=UTF-8'; XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, false); }; that.getAttachment = function (command) { var docId = command.getDocId(); var attachId = command.getAttachmentId(); var isJIO = true; var mime = 'text/plain; charset=UTF-8'; XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, true); }; /** * Create or update a document in local storage. * @method put * @param {object} command The JIO command **/ that.put = function (command) { var doc = command.cloneDoc(); var docId = command.getDocId(); var mime = 'text/plain; charset=UTF-8'; //pas d'attachment dans un put simple function putDocument() { var attachId = ''; var data = JSON.stringify(doc); var isJIO = true; var mime = 'text/plain; charset=UTF-8'; XHRwrapper(command, docId, attachId, 'PUT', mime, data, isJIO, false); } XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (response) { //if (response === '404') {} if (response._attachments !== undefined) { doc._attachments = response._attachments; } putDocument(); } ); }; that.putAttachment = function (command) { var mon_document = null; var docId = command.getDocId(); var attachId = command.getAttachmentId() || ''; var mime = 'text/plain; charset=UTF-8'; //récupération des variables de l'attachement var attachment_id, attachment_data, attachment_md5, attachment_mimetype, attachment_length; attachment_id = command.getAttachmentId(); attachment_data = command.getAttachmentData(); attachment_md5 = command.md5SumAttachmentData(); attachment_mimetype = command.getAttachmentMimeType(); attachment_length = command.getAttachmentLength(); function putAttachment() { XHRwrapper(command, docId, attachId, 'PUT', mime, attachment_data, false, true, function (reponse) { that.success({ // response "ok": true, "id": docId, "attachment": attachId //"rev": current_revision }); } ); } function putDocument() { var attachment_obj = { //"revpos": 3, // optional "digest": attachment_md5, "content_type": attachment_mimetype, "length": attachment_length }; var data = JSON.parse(mon_document); var doc = priv.updateMeta(data, docId, attachId, "add", attachment_obj); XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false, function (reponse) { putAttachment(); } ); } function getDocument() { //XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true); XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (reponse) { if (reponse === '404') { return that.error(priv.createError( 404, "Cannot find document", "Document does not exist" )); } mon_document = reponse; putDocument(); } ); } getDocument(); }; /** * Remove a document or attachment * @method remove * @param {object} command The JIO command */ that.remove = function (command) { var docId = command.getDocId(); var mime = 'text/plain; charset=UTF-8'; function deleteDocument() { XHRwrapper(command, docId, '', 'DELETE', mime, '', true, false, function (reponse) { that.success({ // response "ok": true, "id": docId //"rev": current_revision }); } ); } XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (response) { console.log(response); var attachKeys = (JSON.parse(response))._attachments; var keys; for (keys in attachKeys) { XHRwrapper(command, docId, keys, 'DELETE', mime, '', false, false, function (response) { //console.log('this key got deleted : ' + keys); } ); } deleteDocument(); } ); }; that.removeAttachment = function (command) { var mon_document = null; var docId = command.getDocId(); var attachId = command.getAttachmentId() || ''; var mime = 'text/plain; charset=UTF-8'; //récupération des variables de l'attachement var attachment_id, attachment_data, attachment_md5, attachment_mimetype, attachment_length; attachment_id = command.getAttachmentId(); attachment_data = command.getAttachmentData(); attachment_md5 = command.md5SumAttachmentData(); attachment_mimetype = command.getAttachmentMimeType(); attachment_length = command.getAttachmentLength(); function removeAttachment() { XHRwrapper(command, docId, attachId, 'DELETE', mime, '', true, true, function (reponse) { } ); } function putDocument() { var data = JSON.parse(mon_document); var doc = priv.updateMeta(data, docId, attachId, "remove", ''); XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false, function (reponse) { removeAttachment(); } ); } function getDocument() { XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (reponse) { mon_document = reponse; putDocument(); } ); } getDocument(); }; /** * Get all filenames belonging to a user from the document index * @method allDocs * @param {object} command The JIO command **/ that.allDocs = function (command) { var mon_document = null; var mime = 'text/plain; charset=UTF-8'; function makeJSON() { var $; var keys = $(mon_document).find('Key'); var resultTable = []; var counter = 0; keys.each(function (index) { var that = $(this); var filename = that.context.textContent; var docId = priv.idsToFileName(priv.fileNameToIds(filename)[0]); if (counter === 0) { counter += 1; resultTable.push(docId); } else if (docId !== resultTable[counter - 1]) { counter += 1; resultTable.push(docId); } }); var allDocResponse = { // document content will be added to response "total_rows": resultTable.length, "offset": 0, "rows": [] }; //needed to save the index within the $.ajax.success() callback var count = resultTable.length - 1; var countB = 0; var dealCallback = function (i, countB, allDoc) { return function (doc, statustext, response) { allDoc.rows[i].doc = response.responseText; if (count === 0) { that.success(allDoc); } else { count -= 1; } }; }; var i = resultTable.length - 1; if (command.getOption("include_docs") === true) { for (i; i >= 0; i--) { docId = resultTable[i]; var Signature = that.encodeAuthorization(docId); var callURL = priv.url + docId; var requestUTC = new Date().toUTCString(); var parse = true; allDocResponse.rows[i] = { "id": priv.fileNameToIds(docId) + '', "key": docId, "value": {} }; var checkCounter = i; $.ajax({ contentType : '', crossdomain : true, url : callURL, type : 'GET', headers : { 'Authorization' : "AWS" + " " + priv.AWSIdentifier + ":" + Signature, //'Host' : priv.url, 'x-amz-date' : requestUTC, 'Content-Type' : 'application/json' //'Content-MD5' : '' //'Content-Length' : , //'Expect' : , //'x-amz-security-token' : , }, success : dealCallback(i, countB, allDocResponse), error : function (err) { if (err.status === 404) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" err.error = "not_found"; that.error(err); } else { return that.retry(err); } } }); countB += 1; } } else { for (i; i >= 0; i--) { docId = resultTable[i]; allDocResponse.rows[i] = { "id": priv.fileNameToIds(docId) + '', "key": docId, "value": {} }; } that.success(allDocResponse); } } function getXML() { //XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true); XHRwrapper(command, '', '', 'GET', mime, '', false, false, function (reponse) { mon_document = reponse; makeJSON(); } ); } getXML(); //fin alldocs }; return that; }); /* // It is not possible to attach listeners to xhr level 2 events // AND validate the Qunit tests through sinon.js // therefore, below methods are deprecated var S3specifics = {}; S3specifics.uploadProgress = function(evt){ if (evt.lengthComputable) { var percentComplete = Math.round(evt.loaded * 100 / evt.total); console.log(percentComplete.toString() + '%'); } else { console.log('Unable to compute.'); } }; S3specifics.uploadComplete = function(evt){ var evt_txt = evt.target.responseText; var parser = new DOMParser(); var xmlDoc = parser.parseFromString(evt_txt, "text/xml"); var responseURL = $(xmlDoc.getElementsByTagName('Location'))[0].text(); console.log(responseURL); }; S3specifics.uploadFailed = function(evt){ var evt_txt = evt.target.responseText; console.log("Erreur lors de la tentative d'upload : " + evt_txt); }; S3specifics.uploadCanceled = function(evt){ console.log("Upload annulé par l'utilisateur ou le navigateur."); }; S3specifics.onReadyStateChange = function(req, those, that) { if (req.readyState === 4 && those.status === 200){ that.success({ ok: true, id: command.getDocId() }); } }; */