feat: add ponyfills to support multipart requests on android webview versions < 50 and iOS versions < 13.2

This commit is contained in:
Sefa Ilkimen 2019-11-18 02:01:02 +01:00
parent 3e5c941fdd
commit 7a09fa9460
12 changed files with 184 additions and 36 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

View File

@ -17,6 +17,7 @@
<js-module src="www/local-storage-store.js" name="local-storage-store"/>
<js-module src="www/lodash.js" name="lodash"/>
<js-module src="www/messages.js" name="messages"/>
<js-module src="www/ponyfills.js" name="ponyfills"/>
<js-module src="www/public-interface.js" name="public-interface"/>
<js-module src="www/umd-tough-cookie.js" name="tough-cookie"/>
<js-module src="www/url-util.js" name="url-util"/>

View File

@ -799,12 +799,12 @@ const tests = [
}
},
{
disabled: true,
description: 'should serialize FormData instance correctly when it contains string value',
expected: 'resolved: {"status": 200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var formData = new FormData();
var ponyfills = cordova.plugin.http.ponyfills;
var formData = new ponyfills.FormData();
formData.append('myString', 'This is a test!');
var url = 'https://httpbin.org/anything';
@ -818,13 +818,13 @@ const tests = [
}
},
{
disabled: true,
description: 'should serialize FormData instance correctly when it contains blob value',
expected: 'resolved: {"status": 200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
helpers.getWithXhr(function(blob) {
var formData = new FormData();
var formData = new ponyfills.FormData();
formData.append('CordovaLogo', blob);
var url = 'https://httpbin.org/anything';

View File

@ -22,8 +22,7 @@ const configs = {
},
localAndroidEmulator: {
platformName: 'Android',
platformVersion: '9',
automationName: 'XCUITest',
platformVersion: '5',
deviceName: 'Android Emulator',
autoWebview: true,
fullReset: true,

View File

@ -529,7 +529,7 @@ describe('Common helpers', function () {
const jsUtil = require('../www/js-util');
const messages = require('../www/messages');
const dependencyValidator = require('../www/dependency-validator')(mockWindow, null, messages);
const helpers = require('../www/helpers')(mockWindow, jsUtil, null, messages, base64, null, dependencyValidator);
const helpers = require('../www/helpers')(mockWindow, jsUtil, null, messages, base64, null, dependencyValidator, {});
const testString = 'Test String öäüß 👍😉';
const testStringBase64 = Buffer.from(testString).toString('base64');
@ -540,11 +540,6 @@ describe('Common helpers', function () {
(() => helpers.processData({}, 'utf8')).should.throw(messages.TYPE_MISMATCH_DATA);
});
it('throws an error when needed Web API is not available', () => {
const helpers = require('../www/helpers')({}, jsUtil, null, messages, null, null);
(() => helpers.processData(null, 'multipart')).should.throw(`${messages.INSTANCE_TYPE_NOT_SUPPORTED} FormData`);
});
it('throws an error when given data does not match allowed instance types', () => {
(() => helpers.processData('myString', 'multipart')).should.throw(messages.INSTANCE_TYPE_MISMATCH_DATA);
});
@ -640,12 +635,12 @@ describe('Dependency Validator', function () {
});
});
describe('checkFormDataApi()', function () {
it('throws an error if FormData.entries() API is not supported', function () {
describe('checkFormDataInstance()', function () {
it('throws an error if FormData.entries() is not supported on given instance', function () {
const console = new ConsoleMock();
const validator = require('../www/dependency-validator')({ FormData: {}}, console, messages);
(() => validator.checkFormDataApi()).should.throw(messages.MISSING_FORMDATA_ENTRIES_API);
(() => validator.checkFormDataInstance({})).should.throw(messages.MISSING_FORMDATA_ENTRIES_API);
});
});
@ -658,3 +653,94 @@ describe('Dependency Validator', function () {
});
});
});
describe('Ponyfills', function () {
const mockWindow = {
Blob: BlobMock,
File: FileMock,
};
const init = require('../www/ponyfills');
init.debug = true;
const ponyfills = init(mockWindow);
describe('Iterator', function () {
it('exposes interface correctly', () => {
const iterator = new ponyfills.Iterator([]);
iterator.next.should.be.a('function');
});
describe('next()', function () {
it('returns iteration object correctly when list is empty', () => {
const iterator = new ponyfills.Iterator([]);
iterator.next().should.be.eql({ done: true, value: undefined });
});
it('returns iteration object correctly when end posititon of list is not reached yet', () => {
const iterator = new ponyfills.Iterator([['first', 'this is the first item']]);
iterator.next().should.be.eql({ done: false, value: ['first', 'this is the first item'] });
});
it('returns iteration object correctly when end posititon of list is already reached', () => {
const iterator = new ponyfills.Iterator([['first', 'this is the first item']]);
iterator.next();
iterator.next().should.be.eql({ done: true, value: undefined });
});
});
});
describe('FormData', function () {
it('exposes interface correctly', () => {
const formData = new ponyfills.FormData();
formData.append.should.be.a('function');
formData.entries.should.be.a('function');
});
describe('append()', function () {
it('appends string value correctly', () => {
const formData = new ponyfills.FormData();
formData.append('test', 'myTestString');
formData.__items[0].should.be.eql(['test', 'myTestString']);
});
it('appends numeric value correctly', () => {
const formData = new ponyfills.FormData();
formData.append('test', 10);
formData.__items[0].should.be.eql(['test', '10']);
formData.__items[0][1].should.be.a('string');
});
it('appends Blob value correctly', () => {
const formData = new ponyfills.FormData();
const blob = new BlobMock(['another test'], { type: 'text/plain' });
formData.append('myBlob', blob, 'myFileName.txt');
formData.__items[0].should.be.eql(['myBlob', blob]);
formData.__items[0][1].name.should.be.equal('myFileName.txt');
formData.__items[0][1].lastModifiedDate.should.be.a('Date');
});
it('appends File value correctly', () => {
const formData = new ponyfills.FormData();
const blob = new BlobMock(['another test'], { type: 'text/plain' });
const file = new FileMock(blob, 'myFileName.txt');
formData.append('myFile', file, 'myOverriddenFileName.txt');
formData.__items[0].should.be.eql(['myFile', file]);
formData.__items[0][1].name.should.be.equal('myFileName.txt');
formData.__items[0][1].lastModifiedDate.should.be.eql(file.lastModifiedDate);
});
});
describe('entries()', function () {
it('returns an iterator correctly', () => {
const formData = new ponyfills.FormData();
formData.entries().should.be.an.instanceof(ponyfills.Iterator);
})
});
});
});

View File

@ -4,9 +4,14 @@ module.exports = class FileMock extends BlobMock {
constructor(blob, fileName) {
super(blob, { type: blob.type });
this._fileName = fileName || '';
this.__lastModifiedDate = new Date();
}
get name() {
return this._fileName;
}
get lastModifiedDate() {
return this.__lastModifiedDate;
}
}

View File

@ -15,9 +15,10 @@ var lodash = require(pluginId + '.lodash');
var WebStorageCookieStore = require(pluginId + '.local-storage-store')(ToughCookie, lodash);
var cookieHandler = require(pluginId + '.cookie-handler')(window.localStorage, ToughCookie, WebStorageCookieStore);
var dependencyValidator = require(pluginId + '.dependency-validator')(window, window.console, messages);
var helpers = require(pluginId + '.helpers')(window, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator);
var ponyfills = require(pluginId + '.ponyfills')(window);
var helpers = require(pluginId + '.helpers')(window, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator, ponyfills);
var urlUtil = require(pluginId + '.url-util')(jsUtil);
var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes);
var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes, ponyfills);
dependencyValidator.logWarnings();

View File

@ -2,7 +2,7 @@ module.exports = function init(global, console, messages) {
var interface = {
checkBlobApi: checkBlobApi,
checkFileReaderApi: checkFileReaderApi,
checkFormDataApi: checkFormDataApi,
checkFormDataInstance: checkFormDataInstance,
checkTextEncoderApi: checkTextEncoderApi,
logWarnings: logWarnings,
};
@ -29,8 +29,8 @@ module.exports = function init(global, console, messages) {
}
}
function checkFormDataApi() {
if (!global.FormData || !global.FormData.prototype || !global.FormData.prototype.entries) {
function checkFormDataInstance(instance) {
if (!instance || !instance.entries) {
throw new Error(messages.MISSING_FORMDATA_ENTRIES_API);
}
}

View File

@ -1,4 +1,4 @@
module.exports = function init(global, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator) {
module.exports = function init(global, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator, ponyfills) {
var validSerializers = ['urlencoded', 'json', 'utf8', 'multipart'];
var validCertModes = ['default', 'nocheck', 'pinned', 'legacy'];
var validClientAuthModes = ['none', 'systemstore', 'buffer'];
@ -370,24 +370,30 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
}
}
function getAllowedInstanceType(dataSerializer) {
return dataSerializer === 'multipart' ? 'FormData' : null;
function getAllowedInstanceTypes(dataSerializer) {
return dataSerializer === 'multipart' ? ['FormData'] : null;
}
function processData(data, dataSerializer, cb) {
var currentDataType = jsUtil.getTypeOf(data);
var allowedDataTypes = getAllowedDataTypes(dataSerializer);
var allowedInstanceType = getAllowedInstanceType(dataSerializer);
var allowedInstanceTypes = getAllowedInstanceTypes(dataSerializer);
if (allowedInstanceType && !global[allowedInstanceType]) {
throw new Error(messages.INSTANCE_TYPE_NOT_SUPPORTED + ' ' + allowedInstanceType);
if (allowedInstanceTypes) {
var isCorrectInstanceType = false;
allowedInstanceTypes.forEach(function(type) {
if ((global[type] && data instanceof global[type]) || (ponyfills[type] && data instanceof ponyfills[type])) {
isCorrectInstanceType = true;
}
});
if (!isCorrectInstanceType) {
throw new Error(messages.INSTANCE_TYPE_MISMATCH_DATA + ' ' + allowedInstanceTypes.join(', '));
}
}
if (allowedInstanceType && !(data instanceof global[allowedInstanceType])) {
throw new Error(messages.INSTANCE_TYPE_MISMATCH_DATA + ' ' + allowedInstanceType);
}
if (!allowedInstanceType && allowedDataTypes.indexOf(currentDataType) === -1) {
if (!allowedInstanceTypes && allowedDataTypes.indexOf(currentDataType) === -1) {
throw new Error(messages.TYPE_MISMATCH_DATA + ' ' + allowedDataTypes.join(', '));
}
@ -404,8 +410,8 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
function processFormData(data, cb) {
dependencyValidator.checkBlobApi();
dependencyValidator.checkFileReaderApi();
dependencyValidator.checkFormDataApi();
dependencyValidator.checkTextEncoderApi();
dependencyValidator.checkFormDataInstance(data);
var textEncoder = new global.TextEncoder('utf8');
var iterator = data.entries();

View File

@ -3,7 +3,6 @@ module.exports = {
EMPTY_FILE_PATHS: 'advanced-http: "filePaths" option array must not be empty, <filePaths: string[]>',
EMPTY_NAMES: 'advanced-http: "names" option array must not be empty, <names: string[]>',
INSTANCE_TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following instance types:',
INSTANCE_TYPE_NOT_SUPPORTED: 'advanced-http: this webview does not support following Web API which is needed for this plugin:',
INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or undefined, <alias: string | undefined>',
INVALID_CLIENT_AUTH_MODE: 'advanced-http: invalid client certificate authentication mode, supported modes are:',
INVALID_CLIENT_AUTH_OPTIONS: 'advanced-http: invalid client certificate authentication options, needs to be an dictionary style object',
@ -22,7 +21,7 @@ module.exports = {
MISSING_BLOB_API: 'advanced-http: Blob API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FILE_READER_API: 'advanced-http: FileReader API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FORMDATA_API: 'advanced-http: FormData API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FORMDATA_ENTRIES_API: 'advanced-http: This webview does not implement FormData API specification correctly, FormData.entries() is missing. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FORMDATA_ENTRIES_API: 'advanced-http: Given instance of FormData does not implement FormData API specification correctly, FormData.entries() is missing. If you want to use "multipart/form-data" requests, you can use an included ponyfill. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_TEXT_ENCODER_API: 'advanced-http: TextEncoder API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
POST_PROCESSING_FAILED: 'advanced-http: an error occured during post processing response:',
TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following data types:',

47
www/ponyfills.js Normal file
View File

@ -0,0 +1,47 @@
module.exports = function init(global) {
var interface = { FormData: FormData };
// expose all constructor functions for testing purposes
if (init.debug) {
interface.Iterator = Iterator;
}
function FormData() {
this.__items = [];
}
FormData.prototype.append = function(name, value, filename) {
if (global.File && value instanceof global.File) {
// nothing to do
} else if (global.Blob && value instanceof global.Blob) {
// mimic File instance by adding missing properties
value.lastModifiedDate = new Date();
value.name = filename || '';
} else {
value = value.toString ? value.toString() : value;
}
this.__items.push([ name, value ]);
};
FormData.prototype.entries = function() {
return new Iterator(this.__items);
};
function Iterator(items) {
this.__items = items;
this.__position = -1;
}
Iterator.prototype.next = function() {
this.__position += 1;
if (this.__position < this.__items.length) {
return { done: false, value: this.__items[this.__position] };
}
return { done: true, value: undefined };
}
return interface;
};

View File

@ -1,4 +1,4 @@
module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes) {
module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes, ponyfills) {
var publicInterface = {
getBasicAuthHeader: getBasicAuthHeader,
useBasicAuth: useBasicAuth,
@ -29,7 +29,8 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
head: head,
uploadFile: uploadFile,
downloadFile: downloadFile,
ErrorCode: errorCodes
ErrorCode: errorCodes,
ponyfills: ponyfills
};
function getBasicAuthHeader(username, password) {