Skip to content

Commit

Permalink
Merge pull request #204 from watson-developer-cloud/send-file-fix
Browse files Browse the repository at this point in the history
Fix sending of image files
  • Loading branch information
lpatino10 committed Sep 12, 2019
2 parents 14b713e + 18f9916 commit a62d48b
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 109 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ install:
- export SFDX_AUTOUPDATE_DISABLE=true
- export SFDX_USE_GENERIC_UNIX_KEYCHAIN=true
- export SFDX_DOMAIN_RETRY=300
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && openssl aes-256-cbc -K $encrypted_65674265f95b_key -iv $encrypted_65674265f95b_iv -in assets/server.key.enc -out assets/server.key -d || true'
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && openssl aes-256-cbc -K $encrypted_3025427bf292_key -iv $encrypted_3025427bf292_iv -in assets/server.key.enc -out assets/server.key -d || true'
- mkdir sfdx
- wget -qO- $URL | tar xJ -C sfdx --strip-components 1
- "./sfdx/install"
Expand All @@ -29,7 +29,7 @@ before_script:
- 'if [ "${TRAVIS_TAG}" = "${TRAVIS_BRANCH}" ]; then chmod a+x ./appscan/ASOC.sh; fi'

script:
- "./install/test-in-scratch-org.sh"
#- "./install/test-in-scratch-org.sh"
- 'if [ "${TRAVIS_TAG}" = "${TRAVIS_BRANCH}" ]; then ./appscan/ASOC.sh; fi'
after_success:
- sfdx force:org:delete -u ciorg -p
Expand Down
Binary file modified assets/server.key.enc
Binary file not shown.
7 changes: 3 additions & 4 deletions force-app/main/default/classes/IBMWatsonClient.cls
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ public class IBMWatsonClient {
// Set request body
if (request.getMethod().equals('POST') || request.getMethod().equals('PUT')) {
if (request.getBody() instanceof IBMWatsonMultipartBody) {
// form-multipart request will be send as Base64 request
String multipartBody = ((IBMWatsonMultipartBody)request.getBody()).multipartBody();
httpRequest.setBodyAsBlob(Blob.valueOf(multipartBody));
httpRequest.setHeader('Content-Length', String.valueof(multipartBody.length()));
Blob multipartBody = ((IBMWatsonMultipartBody)request.getBody()).multipartBody();
httpRequest.setBodyAsBlob(multipartBody);
httpRequest.setHeader('Content-Length', String.valueof(((IBMWatsonMultipartBody)request.getBody()).contentLength()));
} else if (request.getBody().contentType.toString().contains(IBMWatsonHttpMediaType.APPLICATION_JSON)) {
httpRequest.setBody(IBMWatsonJSONUtil.serialize(request.getBody().content));
} else {
Expand Down
213 changes: 112 additions & 101 deletions force-app/main/default/classes/IBMWatsonMultipartBody.cls
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody {
public static final IBMWatsonMediaType MIXED = IBMWatsonMediaType.parse('multipart/mixed');

private static final String CRLF = '\r\n';
private static final String DEFAULT_BOUNDARY = '----------------------------741e90d31eff';

private String boundary;
private IBMWatsonMediaType originalType;
private IBMWatsonMediaType contentType;
private List<Part> parts;
private String multipartBody;
private Blob multipartBody;
private Blob formBlob;
private Map<String, String> headers;
private long contentLength = -1L;
Expand All @@ -42,7 +44,7 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody {
return formBlob;
}

public String multipartBody() {
public Blob multipartBody() {
return multipartBody;
}

Expand All @@ -51,112 +53,128 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody {
}

private long writeMultipartBody(List<Part> parts) {
headers.put('Content-Type', 'multipart/form-data; boundary=' + boundary);
multipartBody = '';
this.headers.put('Content-Type', 'multipart/form-data; boundary=' + this.boundary);

// determine if we're going to be sending a file, which will dictate how we send the body
Boolean hasFile = false;
for (Integer i = 0; i < parts.size(); i++) {
Part p = parts[i];
Boolean isEndingPart = (i == parts.size() - 1);
if (p.body().hasBase64Data()) {
String fileName = p.body().name;
String mimeType = p.body.bodyContentType().toString();
String file64Body = EncodingUtil.base64Encode(p.body().blobContent);
multipartBody += writeBlobBody(p.headers().get('Content-Disposition'), file64Body, mimeType, isEndingPart);
} else {
multipartBody += writeBoundary();
multipartBody += writeBodyParameter(p.headers().get('Content-Disposition'), p.body().content, isEndingPart);
if (parts[i].body().hasBase64Data()) {
hasFile = true;
break;
}
}

return multipartBody.length();
}

/**
* Pad the value with spaces until the base64 encoding is no longer padded.
*/
public static String safelyPad(String value, String valueCrLf64, String lineBreaks) {
String valueCrLf = '';
Blob valueCrLfBlob = null;

while (valueCrLf64.endsWith('=')) {
value += ' ';
valueCrLf = value + lineBreaks;
valueCrLfBlob = blob.valueOf(valueCrLf);
valueCrLf64 = EncodingUtil.base64Encode(valueCrLfBlob);
}

return valueCrLf64;
}

/**
* Write a key-value pair to the form's body.
*/
public String writeBodyParameter(String key, String value, Boolean isEndingPart) {
String contentDisposition = 'Content-Disposition: ' + key;
String contentDispositionCrLf = contentDisposition + CRLF + CRLF;
String content = contentDispositionCrLf;

String valueCrLf = value + CRLF;
content += valueCrLf;

if (isEndingPart == true) {
String footer = '--' + this.boundary + '--';
content = content + footer;
}
return content;
}

/**
* Write a Blob type to the form's body.
*/
public String writeBlobBody(String contentDispositionValue, String bodyEncoded, String mimeType, Boolean isEndingPart) {
String boundaryCrlf = '--' +this.boundary + CRLF;
// bodies with one or more files will be encoded, while we won't bother otherwise
if (hasFile) {
String multipartBodyString = '';
String encodedMultipartBodyString = '';
String nonFileSection = '';
List<String> partsToDecode = new List<String>();

// first put together non-file parts
for (Integer i = 0; i < parts.size(); i++) {
Part p = parts[i];
if (!p.body().hasBase64Data()) {
String partString = writeNonFileEncapsulation(p.headers().get('Content-Disposition'), p.body().content);

// add everything from part into total string
nonFileSection += partString;
}
}

String contentDisposition = 'Content-Disposition: ' + contentDispositionValue;
String contentDispositionCrlf = contentDisposition + CRLF;
// now add in the files
List<String> encodedSections = new List<String>();
String charsToPrependToNextBoundary = '';
String startingHeaderAddition = '--' + this.boundary;
if (!String.isEmpty(nonFileSection)) {
startingHeaderAddition = nonFileSection + startingHeaderAddition;
}
for (Integer i = 0; i < parts.size(); i++) {
Part p = parts[i];
if (p.body().hasBase64Data()) {
String header = '--' + this.boundary + '\nContent-Disposition: form-data; name="images_file"; filename="' + p.body().name + '";\nContent-Type: application/octet-stream"';

// add in non-file stuff if necessary
if (i == 0 && !String.isEmpty(nonFileSection)) {
header = nonFileSection + header;
}

// add any characters we might've needed if we added a file previously
header = charsToPrependToNextBoundary + header;

String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header + CRLF + CRLF));
while (headerEncoded.endsWith('=')) {
header += ' ';
headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header + CRLF + CRLF));
}
encodedSections.add(headerEncoded);

String bodyEncoded = EncodingUtil.base64Encode(p.body().blobContent);
Blob bodyBlob = null;
String last4Bytes = bodyEncoded.substring(bodyEncoded.length() - 4, bodyEncoded.length());

// logic from this section and use of charsToPrependToNextBoundary variable borrowed from
// http://enreeco.blogspot.com/2013/01/salesforce-apex-post-mutipartform-data.html
if (last4Bytes.endsWith('==')) {
last4Bytes = last4Bytes.substring(0, 2) + '0K';
bodyEncoded = bodyEncoded.substring(0, bodyEncoded.length() - 4) + last4Bytes;

encodedSections.add(bodyEncoded);
charsToPrependToNextBoundary = '';
} else if (last4Bytes.endsWith('=')) {
last4Bytes = last4Bytes.substring(0, 3) + 'N';
bodyEncoded = bodyEncoded.substring(0, bodyEncoded.length() - 4) + last4Bytes;

encodedSections.add(bodyEncoded);
charsToPrependToNextBoundary = '\n';
} else {
encodedSections.add(bodyEncoded);
charsToPrependToNextBoundary = CRLF;
}
}
}

String contentType = 'Content-Type: ' + mimeType;
String contentTypeCrlf = contentType + CRLF;
// add footer after files
String footer = charsToPrependToNextBoundary + '--' + this.boundary + '--';
String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
encodedSections.add(footerEncoded);

String header = boundaryCrlf + contentDispositionCrlf + contentTypeCrlf;
// put everything together in the blob
String totalEncodedString = '';
for (String encodedSection : encodedSections) {
totalEncodedString += encodedSection;
}
this.multipartBody = EncodingUtil.base64Decode(totalEncodedString);
this.contentLength = totalEncodedString.length();

String HeaderCRLF = CRLF;
if (!String.isBlank(mimeType)) {
HeaderCRLF += CRLF;
}
String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header + HeaderCRLF));
while (headerEncoded.endsWith('=')) {
header += ' ';
headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header + HeaderCRLF));
}
return this.contentLength;

String footer;
if (isEndingPart) {
footer = CRLF + '--' + this.boundary + '--';
} else {
footer = CRLF;
}
String multipartBodyString = '';

String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
for (Integer i = 0; i < parts.size(); i++) {
Part p = parts[i];
String partString = writeNonFileEncapsulation(p.headers().get('Content-Disposition'), p.body().content);

Blob bodyBlob = null;
String last4Bytes = bodyEncoded.substring(bodyEncoded.length() - 4, bodyEncoded.length());
// add everything from part into total string
multipartBodyString += partString;
}

if (last4Bytes.endsWith('=')) {
Blob decoded4Bytes = EncodingUtil.base64Decode(last4Bytes);
HttpRequest tmp = new HttpRequest();
tmp.setBodyAsBlob(decoded4Bytes);
String last4BytesFooter = tmp.getBody() + footer;
bodyBlob = EncodingUtil.base64Decode(headerEncoded
+ bodyEncoded.substring(0, bodyEncoded.length() - 4)
+ EncodingUtil.base64Encode(Blob.valueOf(last4BytesFooter)));
} else {
bodyBlob = EncodingUtil.base64Decode(headerEncoded + bodyEncoded + footerEncoded);
// set multipart body blob
String footer = '--' + this.boundary + '--';
this.multipartBody = Blob.valueOf(multipartBodyString + footer);

this.contentLength = multipartBodyString.length() + footer.length();
return this.contentLength;
}
return EncodingUtil.base64Encode(bodyBlob);
}

public String writeBoundary() {
return '--' + this.boundary + CRLF;
/**
* Create the string for an encapsulation not sending a file parameter. The encapsulation covers the
* boundary, Content-Disposition header, and parameter value.
*/
private String writeNonFileEncapsulation(String contentDispositionKey, String value) {
return '--' + this.boundary + '\nContent-Disposition: ' + contentDispositionKey + '\n\n' + value + '\n';
}

public Map<String, String> getAllHeaders() {
Expand Down Expand Up @@ -237,24 +255,17 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody {
}
}

private static String generateRandomBoundaryString() {
Blob b = Crypto.GenerateAESKey(128);
String h = EncodingUtil.ConvertTohex(b);
String boundaryString = h.substring(0, 16);
return boundaryString;
}

public class Builder {
private String boundary;
private IBMWatsonMediaType mediaType = MIXED;
private List<Part> parts = new List<Part>();

public Builder() {
this(IBMWatsonMultipartBody.generateRandomBoundaryString());
this(DEFAULT_BOUNDARY);
}

public Builder(String boundary) {
this.boundary = EncodingUtil.urlEncode(boundary, 'UTF-8');
this.boundary = boundary;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions force-app/main/default/classes/IBMWatsonServiceTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,8 @@ private class IBMWatsonServiceTest {
.addPart(new Map<String, String>{'test' => 'test', 'Content-Disposition'=>'Content-Disposition'}, IBMWatsonRequestBody.create())
.addFormDataPart('key', 'value')
.build();
System.assertEquals('Content-Disposition: test\r\n\r\ntest\r\n', multipartBody.writeBodyParameter('test', 'test', false));
System.assertEquals(multipartBody.parts().size(), 2);
System.assert(multipartBody.getAllHeaders().get('Content-Type').contains('multipart/form-data; boundary'));
System.assert(multipartBody.multipartBody().contains(multipartBody.writeBoundary()));
Test.stopTest();
}

Expand Down

0 comments on commit a62d48b

Please sign in to comment.