/*jslint indent: 2, maxlen: 80, nomen: true */ /*global define, jIO, btoa, b64_hmac_sha1, jQuery, XMLHttpRequest, XHRwrapper, FormData*/ /** * JIO S3 Storage. Type = "s3". * Amazon S3 "database" storage. */ // define([module_name], [dependencies], module); (function (dependencies, module) { "use strict"; if (typeof define === 'function' && define.amd) { return define(dependencies, module); } module(jIO, jQuery, {b64_hmac_sha1: b64_hmac_sha1}); }(['jio', 'jquery', 'sha1'], function (jIO, $, sha1) { "use strict"; var b64_hmac_sha1 = sha1.b64_hmac_sha1; jIO.addStorageType("s3", function (spec, my) { 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.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, "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.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 () { //generates the policy //enables the choice for the http response code var s3_policy; s3_policy = { "expiration": "2020-01-01T00:00:00Z", "conditions": [ {"bucket": priv.server }, ["starts-with", "$key", ""], {"acl": priv.acl }, {"success_action_redirect": ""}, {"success_action_status": undefined }, // 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 //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 () { var response, err = ''; 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') { 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 { 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) { /*jslint unparam: true */ doc._attachments = doc._attachments || {}; switch (action) { case "add": doc._attachments[attachid] = data; //nothing happens doc = JSON.stringify(doc); break; case "remove": if (doc._attachments !== undefined) { delete doc._attachments[attachid]; } doc = JSON.stringify(doc); break; case "update": doc._attachments[attachid] = data; //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) { //GET oriented method var requestUTC, httpVerb, StringToSign, Signature; requestUTC = new Date().toUTCString(); httpVerb = "GET"; StringToSign = priv.buildStringToSign( httpVerb, '', 'application/json', '', 'x-amz-date:' + requestUTC, '/' + priv.server + '/' + key ); Signature = b64_hmac_sha1(priv.password, StringToSign); return Signature; }; that.XHRwrapper = function (command, docId, attachId, http, mime, data, jio, is_attachment, callback) { var docFile, requestUTC, StringToSign, url, Signature, xhr; docFile = priv.secureName(priv.idsToFileName(docId, attachId || undefined)); requestUTC = new Date().toUTCString(); StringToSign = priv.buildStringToSign( http, '', mime, '', 'x-amz-date:' + requestUTC, '/' + priv.server + '/' + docFile ); url = 'http://s3.amazonaws.com/' + priv.server + '/' + docFile; Signature = b64_hmac_sha1(priv.password, StringToSign); 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, mime; doc = command.cloneDoc(); doc_id = command.getDocId(); function postDocument() { var http_response, fd, Signature, xhr; 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); http_response = ''; 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 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); 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; } mime = 'text/plain; charset=UTF-8'; that.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, attachId, isJIO, mime; docId = command.getDocId(); attachId = command.getAttachmentId() || ''; isJIO = true; mime = 'text/plain; charset=UTF-8'; that.XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, false); }; that.getAttachment = function (command) { var docId, attachId, isJIO, mime; docId = command.getDocId(); attachId = command.getAttachmentId(); isJIO = true; mime = 'text/plain; charset=UTF-8'; that.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, docId, mime; doc = command.cloneDoc(); docId = command.getDocId(); mime = 'text/plain; charset=UTF-8'; //pas d'attachment dans un put simple function putDocument() { var attachId, data, isJIO; attachId = ''; data = JSON.stringify(doc); isJIO = true; that.XHRwrapper(command, docId, attachId, 'PUT', mime, data, isJIO, false); } that.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, docId, attachId, mime, attachment_data, attachment_md5, attachment_mimetype, attachment_length; mon_document = null; docId = command.getDocId(); attachId = command.getAttachmentId() || ''; mime = 'text/plain; charset=UTF-8'; //récupération des variables de l'attachement //attachment_id = command.getAttachmentId(); attachment_data = command.getAttachmentData(); attachment_md5 = command.md5SumAttachmentData(); attachment_mimetype = command.getAttachmentMimeType(); attachment_length = command.getAttachmentLength(); function putAttachment() { that.XHRwrapper(command, docId, attachId, 'PUT', mime, attachment_data, false, true, function () { that.success({ // response "ok": true, "id": docId, "attachment": attachId //"rev": current_revision }); } ); } function putDocument() { var attachment_obj, data, doc; attachment_obj = { //"revpos": 3, // optional "digest": attachment_md5, "content_type": attachment_mimetype, "length": attachment_length }; data = JSON.parse(mon_document); doc = priv.updateMeta(data, docId, attachId, "add", attachment_obj); that.XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false, function () { putAttachment(); } ); } function getDocument() { //XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true); that.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, mime; docId = command.getDocId(); mime = 'text/plain; charset=UTF-8'; function deleteDocument() { that.XHRwrapper(command, docId, '', 'DELETE', mime, '', true, false, function () { that.success({ // response "ok": true, "id": docId //"rev": current_revision }); } ); } function myCallback() { return; } that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (response) { var attachKeys, keys; attachKeys = (JSON.parse(response))._attachments; for (keys in attachKeys) { if (attachKeys.hasOwnProperty(keys)) { that.XHRwrapper(command, docId, keys, 'DELETE', mime, '', false, false, myCallback ); } } deleteDocument(); } ); }; that.removeAttachment = function (command) { var mon_document, docId, attachId, mime; mon_document = null; docId = command.getDocId(); attachId = command.getAttachmentId() || ''; mime = 'text/plain; charset=UTF-8'; //récupération des variables de l'attachement // attachment_id = command.getAttachmentId(); // attachment_data = command.getAttachmentData(); // attachment_md5 = command.md5SumAttachmentData(); // attachment_mimetype = command.getAttachmentMimeType(); // attachment_length = command.getAttachmentLength(); function removeAttachment() { that.XHRwrapper(command, docId, attachId, 'DELETE', mime, '', true, true, function () { return; }); } function putDocument() { var data, doc; data = JSON.parse(mon_document); doc = priv.updateMeta(data, docId, attachId, "remove", ''); that.XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false, function () { removeAttachment(); }); } function getDocument() { that.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, mime; mon_document = null; mime = 'text/plain; charset=UTF-8'; function makeJSON() { var keys, resultTable, counter, allDocResponse, count, countB, dealCallback, errCallback, i, keyId, Signature, callURL, requestUTC; keys = $(mon_document).find('Key'); resultTable = []; counter = 0; keys.each(function () { var $this, filename, docId; $this = $(this); filename = $this.context.textContent; 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); } }); 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 count = resultTable.length - 1; countB = 0; dealCallback = function (i, countB, allDoc) { /*jslint unparam: true */ return function (doc, statustext, response) { allDoc.rows[i].doc = response.responseText; if (count === 0) { that.success(allDoc); } else { count -= 1; } }; }; errCallback = 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); } }; i = resultTable.length - 1; if (command.getOption("include_docs") === true) { for (i; i >= 0; i -= 1) { keyId = resultTable[i]; Signature = that.encodeAuthorization(keyId); callURL = 'http://' + priv.server + '.s3.amazonaws.com/' + keyId; requestUTC = new Date().toUTCString(); allDocResponse.rows[i] = { "id": priv.fileNameToIds(keyId).join(), "key": keyId, "value": {} }; $.ajax({ contentType : '', crossdomain : true, url : callURL, type : 'GET', headers : { 'Authorization' : "AWS" + " " + priv.AWSIdentifier + ":" + Signature, 'x-amz-date' : requestUTC, 'Content-Type' : 'application/json' //'Content-MD5' : '' //'Content-Length' : , //'Expect' : , //'x-amz-security-token' : , }, success : dealCallback(i, countB, allDocResponse), error : errCallback(that.error) }); countB += 1; } } else { for (i; i >= 0; i -= 1) { keyId = resultTable[i]; allDocResponse.rows[i] = { "id": priv.fileNameToIds(keyId).join(), "key": keyId, "value": {} }; } that.success(allDocResponse); } } function getXML() { //XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true); that.XHRwrapper(command, '', '', 'GET', mime, '', false, false, function (reponse) { mon_document = reponse; makeJSON(); } ); } getXML(); //fin alldocs }; return that; }); }));