Skip to content

Commit

Permalink
Adjust attachment support for binary roundtrip
Browse files Browse the repository at this point in the history
  • Loading branch information
brianjmiller committed Sep 16, 2016
1 parent 6bd8f7d commit df99555
Show file tree
Hide file tree
Showing 21 changed files with 860 additions and 185 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ sudo: false
language: node_js
node_js:
- "6"
- "5"
- "4"
- "0.12"
- "0.10"
before_install: npm install -g grunt-cli
install: npm install
Expand Down
7 changes: 7 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ module.exports = function(grunt) {
bower;

browserFileList.push(
// needed because IE10 doesn't support Uint8ClampedArray
// which is required by CryptoJS for typedarray support
"node_modules/js-polyfills/typedarray.js",
// needed because IE10 doesn't have ArrayBuffer slice
"node_modules/arraybuffer-slice/index.js",
// needed for IE and Safari for TextDecoder/TextEncoder
"node_modules/text-encoding/lib/encoding.js",
"src/Environment/Browser.js"
);
nodeFileList.push(
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,19 @@ Environments
------------

Implementing a new Environment should be straightforward and requires overloading a couple
of methods on the `TinCan.LRS` prototype. There are currently two examples, `Environment/Browser`
of methods in the library. There are currently two examples, `Environment/Browser`
and `Environment/Node`.

Attachment Support
------------------

Sending and retrieving statements with attachments via the multipart/mixed request/response
cycle works end to end with binary attachments in Node.js 4+ and in the typical modern browsers:
Chrome 53+, Firefox 48+, Safari 9+, IE 10+ (current versions at time of implementation, older versions
may work without changes but have not been tested). Attachments without included content (those using
only the `fileUrl` property) should be supported in all environments supported by the library.

Several polyfills (TypedArrays, ArrayBuffer w/ slice, Blob, TextDecoder/TextEncoder) are needed
to support various browser versions, if you are targeting a recent enough set of browsers you
can reduce the overall size of the built library by commenting out those polyfills in the
`Gruntfile.js` file and building yourself.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"doc": "doc"
},
"dependencies": {
"xhr2": "0.0.7"
"xhr2": "0.0.7",
"js-polyfills": "0.1.22",
"text-encoding": "0.6.0",
"arraybuffer-slice": "0.1.2"
},
"author": "Rustici Software <support@tincanapi.com>",
"contributors": [
Expand Down
45 changes: 40 additions & 5 deletions src/Attachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ TinCan client library

/**
@property content
@type String
@type ArrayBuffer
*/
this.content = null;

Expand Down Expand Up @@ -118,9 +118,12 @@ TinCan client library
}

if (cfg.hasOwnProperty("content") && cfg.content !== null) {
this.content = cfg.content;
this.length = cfg.content.length;
this.sha2 = TinCan.Utils.getSHA256String(cfg.content);
if (typeof cfg.content === "string") {
this.setContentFromString(cfg.content);
}
else {
this.setContent(cfg.content);
}
}
},

Expand Down Expand Up @@ -162,7 +165,37 @@ TinCan client library
@method getLangDictionaryValue
*/
getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue
getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue,

/**
@method setContent
@param {ArrayBuffer} content Sets content from ArrayBuffer
*/
setContent: function (content) {
this.content = content;
this.length = content.byteLength;
this.sha2 = TinCan.Utils.getSHA256String(content);
},

/**
@method setContentFromString
@param {String} content Sets the content property of the attachment from a string
*/
setContentFromString: function (content) {
var _content = content;

_content = TinCan.Utils.stringToArrayBuffer(content);

this.setContent(_content);
},

/**
@method getContentAsString
@return {String} Value of content property as a string
*/
getContentAsString: function () {
return TinCan.Utils.stringFromArrayBuffer(this.content);
}
};

/**
Expand All @@ -176,4 +209,6 @@ TinCan client library

return new Attachment(_attachment);
};

Attachment._defaultEncoding = "utf-8";
}());
124 changes: 118 additions & 6 deletions src/Environment/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ TinCan client library
@submodule TinCan.Environment.Browser
**/
(function () {
/* globals window, XMLHttpRequest, XDomainRequest */
/* globals window, XMLHttpRequest, XDomainRequest, Blob */
"use strict";
var LOG_SRC = "Environment.Browser",
requestComplete,
__IEModeConversion,
nativeRequest,
xdrRequest,
requestComplete,
__createJSONSegment,
__createAttachmentSegment,
__delay,
__IEModeConversion,
env = {},
log = TinCan.prototype.log;

Expand Down Expand Up @@ -268,18 +270,35 @@ TinCan client library
// http://blogs.msdn.com/b/ie/archive/2006/01/23/516393.aspx
//
xhr = new ActiveXObject("Microsoft.XMLHTTP");

if (cfg.expectMultipart) {
err = new Error("Attachment support not available");
if (typeof cfg.callback !== "undefined") {
cfg.callback(err, null);
}
return {
err: err,
xhr: null
};
}
}

xhr.open(cfg.method, fullUrl, async);

//
// setting the .responseType before .open was causing IE to fail
// with a StateError, so moved it to here
//
if (cfg.expectMultipart) {
xhr.responseType = "arraybuffer";
}

for (prop in headers) {
if (headers.hasOwnProperty(prop)) {
xhr.setRequestHeader(prop, headers[prop]);
}
}

if (typeof cfg.data !== "undefined") {
cfg.data += "";
}
data = cfg.data;

if (async) {
Expand Down Expand Up @@ -330,6 +349,16 @@ TinCan client library
},
err;

if (cfg.expectMultipart) {
err = new Error("Attachment support not available");
if (typeof cfg.callback !== "undefined") {
cfg.callback(err, null);
}
return {
err: err,
xhr: null
};
}
if (typeof headers["Content-Type"] !== "undefined" && headers["Content-Type"] !== "application/json") {
err = new Error("Unsupported content type for IE Mode request");
if (cfg.callback) {
Expand Down Expand Up @@ -565,4 +594,87 @@ TinCan client library
// Synchronous xhr handling is accepted in the browser environment
//
TinCan.LRS.syncEnabled = true;

TinCan.LRS.prototype._getMultipartRequestData = function (boundary, jsonContent, requestAttachments) {
var parts = [],
i;

parts.push(
__createJSONSegment(
boundary,
jsonContent
)
);
for (i = 0; i < requestAttachments.length; i += 1) {
if (requestAttachments[i].content !== null) {
parts.push(
__createAttachmentSegment(
boundary,
requestAttachments[i].content,
requestAttachments[i].sha2,
requestAttachments[i].contentType
)
);
}
}
parts.push("\r\n--" + boundary + "--\r\n");

return new Blob(parts);
};

__createJSONSegment = function (boundary, jsonContent) {
var content = [
"--" + boundary,
"Content-Type: application/json",
"",
JSON.stringify(jsonContent)
].join("\r\n");

content += "\r\n";

return content;
};

__createAttachmentSegment = function (boundary, content, sha2, contentType) {
var blobParts = [],
header = [
"--" + boundary,
"Content-Type: " + contentType,
"Content-Transfer-Encoding: binary",
"X-Experience-API-Hash: " + sha2
].join("\r\n");

header += "\r\n\r\n";

blobParts.push(header);
blobParts.push(content);

return new Blob(blobParts);
};

TinCan.Utils.stringToArrayBuffer = function (content, encoding) {
/* global TextEncoder */
var encoder;

if (! encoding) {
encoding = TinCan.Utils.defaultEncoding;
}

encoder = new TextEncoder(encoding);

return encoder.encode(content).buffer;
};

TinCan.Utils.stringFromArrayBuffer = function (content, encoding) {
/* global TextDecoder */
var decoder;

if (! encoding) {
encoding = TinCan.Utils.defaultEncoding;
}

decoder = new TextDecoder(encoding);

return decoder.decode(content);
};
}());
Loading

0 comments on commit df99555

Please sign in to comment.