/*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.addStorage("s3", function (spec) { var that, priv = {}, lastDigest, isDelete; that = this; //nomenclature param // param._id, // ._attachment, // ._blob // attributes priv.username = spec.username || ''; priv.AWSIdentifier = spec.AWSIdentifier || ''; priv.password = spec.password || ''; priv.server = spec.server || ''; /*||> "private, public-read, public-read-write, authenticated-read, bucket-owner-read, bucket-owner-full-control" <||*/ priv.acl = spec.acl || ''; 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'); }; priv.fileNameToIds = function (resourcename) { var split, el, id = "", attmt = "", last; split = resourcename.split('.'); function replaceAndNotLast() { last = false; return '.'; } /*jslint ass: true */ while ((el = split.shift()) !== undefined) { last = true; el = el.replace(/__/g, '%2595'); el = el.replace(/_$/, replaceAndNotLast); id += el.replace(/%2595/g, '_'); if (last) { break; } } attmt = split.join('.'); return [id, attmt]; }; priv.idsToFileName = function (document_id, attachment_id) { document_id = encodeURI(document_id). replace(/\//g, "%2F"). replace(/\?/g, "%3F"); document_id = encodeURI(document_id). replace(/_/g, "__"). replace(/\./g, "_."); if (attachment_id) { attachment_id = encodeURI(attachment_id). replace(/\//g, "%2F"). replace(/\?/g, "%3F"); return document_id + "." + attachment_id; } return document_id; }; /** * 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; }; /** * Generate a new uuid * * @method generateUuid * @private * @return {String} The new uuid */ function generateUuid() { function S4() { /* 65536 */ var i, string = Math.floor( Math.random() * 0x10000 ).toString(16); for (i = string.length; i < 4; i += 1) { string = '0' + string; } return string; } return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); } 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 }, ["starts-with", "$Content-Type", ""] ] }; //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": command.success(this.status, {id: docId}); break; case 'PUT': if (jio === true) { command.success(this.status); } else { callback(this.responseText); } break; case 'GET': if (jio === true) { if (isAttachment === true) { //méthode that.getAttachment response = this.response; command.success( this.status, {'data': response, 'digest': lastDigest} ); } else { // this is not an attachment // that.get method response = JSON.parse(this.responseText); command.success(this.status, {'data': response}); } } else { callback(this.responseText); } break; case 'DELETE': if (jio === true) { if (isAttachment === false) { command.success(this.status); } else { command.success(this.status); } } 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"; command.error(err); } if (this.status === 404) { if (http === 'GET') { if (jio === true) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" return command.error( 404, "Not Found", "File does not exist" ); } //not jio if (isDelete === true) { isDelete = false; return command.error( 404, "Not Found", "File does not exist" ); } callback('404'); } else { //status //statustext "Not Found" //error //reason "reason" //message "did not work" return command.error( 404, "Not Found", "File does not exist" ); } //fin 404 } if (this.status === 409) { //status //statustext "Not Found" //error //reason "reason" //message "did not work" return command.error( 409, "Already Exists", "File does exist" ); } } } }; } 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; }; 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; if (command.method === "alldocs") { docFile = ''; } else { docFile = 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); if (http === 'GET' && jio === true && is_attachment === true) { xhr.responseType = 'blob'; } else { //défaut 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, metadata) { //as S3 encoding key are directly inserted within the FormData(), //use of XHRwrapper function ain't pertinent var doc, doc_id, mime; doc = metadata; //doc_id = (!doc._id) ? generateUuid() : doc._id; doc._id = doc._id || generateUuid(); doc_id = doc._id; function postDocument() { var fd, Signature, xhr; doc_id = priv.idsToFileName(doc_id); //Meant to deep-serialize in order to avoid //conflicts due to the multipart enctype doc = JSON.stringify(doc); 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); 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 = generateUuid(); } 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 command.error( 409, "Document already exists", "Cannot create document" ); } }); }; /** * Get a document or attachment * @method get * @param {object} command The JIO command **/ that.get = function (command, metadata) { var docId, isJIO, mime; docId = metadata._id; isJIO = true; mime = 'text/plain; charset=UTF-8'; that.XHRwrapper(command, docId, '', 'GET', mime, '', isJIO, false); }; that.getAttachment = function (command, param) { var docId, attachId, isJIO, mime; function getTheAttachment() { docId = param._id; attachId = param._attachment; isJIO = true; mime = 'text/plain; charset=UTF-8'; that.XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, true); } function getDoc() { docId = param._id; isJIO = false; mime = 'text/plain; charset=UTF-8'; that.XHRwrapper(command, docId, '', 'GET', mime, '', isJIO, false, function (response) { var responseObj = JSON.parse(response)._attachments; if (responseObj !== undefined) { if (responseObj[param._attachment] !== undefined) { lastDigest = JSON.parse(response). _attachments[param._attachment].digest; } } getTheAttachment(); }); } getDoc(); //docId = param._id; //attachId = param._attachment; //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, metadata) { var doc, docId, mime; doc = metadata; docId = doc._id; 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) { var responseObj = JSON.parse(response); if (responseObj._attachments !== undefined) { doc._attachments = responseObj._attachments; } putDocument(); // XXX control non existing document to throw a 201 http code }); }; that.putAttachment = function (command, param) { var my_document, docId, attachId, mime, attachment_data, attachment_digest, attachment_mimetype, attachment_length; my_document = null; docId = param._id; attachId = param._attachment; mime = param._blob.type; attachment_data = param._blob; jIO.util.readBlobAsBinaryString(param._blob).then(function (e) { var binary_string = e.target.result; attachment_digest = jIO.util.makeBinaryStringDigest(binary_string); function putAttachment() { that.XHRwrapper( command, docId, attachId, 'PUT', mime, attachment_data, false, true, function () { command.success({ // response "digest": attachment_digest }); } ); } function putDocument() { var attachment_obj, data, doc; attachment_mimetype = param._blob.type; attachment_length = param._blob.size; attachment_obj = { //"revpos": 3, // optional "digest": attachment_digest, "content_type": attachment_mimetype, "length": attachment_length }; data = JSON.parse(my_document); doc = priv.updateMeta(data, docId, attachId, "add", attachment_obj); that.XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false, function () { putAttachment(); }); } function getDocument() { that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false, function (response) { if (response === '404') { return command.error( 404, "Document does not exist", "Cannot find document" ); } my_document = response; putDocument(); }); } getDocument(); }); }; /** * Remove a document or attachment * @method remove * @param {object} command The JIO command */ that.remove = function (command, param) { var docId, mime; docId = param._id; mime = 'text/plain; charset=UTF-8'; isDelete = true; function deleteDocument() { isDelete = false; that.XHRwrapper(command, docId, '', 'DELETE', mime, '', true, false, function () { command.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, param) { var my_document, docId, attachId, mime; my_document = null; docId = param._id; attachId = param._attachment; 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(my_document); data = my_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 (response) { if (response === '404') { return command.error( 404, "missing document", "This Document does not exist" ); } my_document = JSON.parse(response); if (my_document._attachments === undefined) { return command.error( 404, "missing attachment", "This Document has no attachments" ); } if (my_document._attachments[attachId] !== undefined) { putDocument(); } else { return command.error( 404, "missing attachment", "This Attachment does not exist" ); } }); } getDocument(); }; /** * Get all filenames belonging to a user from the document index * @method allDocs * @param {object} command The JIO command **/ that.allDocs = function (command, param, options) { /*jslint unparam: true */ var _succ, my_document, mime; _succ = command.success; command.success = function () { _succ.apply(this, arguments); }; my_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 = $($.parseXML(my_document)).find('Key'); if (keys.length === 0) { return command.success({"data": { "total_rows": 0, "rows": [] } }); } 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, "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 = JSON.parse(response.responseText); if (count === 0) { return command.success({ "data": allDoc }); } count -= 1; }; }; errCallback = function (jQxhr) { command.error( jQxhr.status, jQxhr.statusText, "S3 Alldocs failed." ); }; i = resultTable.length - 1; if (options.include_docs) { 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)[0], "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 }); countB += 1; } } else { for (i; i >= 0; i -= 1) { keyId = resultTable[i]; allDocResponse.rows[i] = { "id": priv.fileNameToIds(keyId)[0], "value": {} }; } allDocResponse = {"data": allDocResponse}; command.success(allDocResponse); } } function getXML() { command.method = 'alldocs'; that.XHRwrapper(command, '', '', 'GET', mime, '', false, false, function (response) { my_document = response; makeJSON(); } ); } getXML(); //fin alldocs }; }); }));