diff options
author | Struan Donald <struan@exo.org.uk> | 2020-05-04 17:38:39 +0100 |
---|---|---|
committer | Dave Arter <davea@mysociety.org> | 2020-08-06 10:59:39 +0100 |
commit | ae687e6dd042982f2423d4e7e79e41c54d729e7b (patch) | |
tree | 8da7cfb85f100038e010aefb4a806495a165fc2b | |
parent | ed0002b0d76560cc3d5b1e62f8395bbfae74403c (diff) |
Handle multipart inspector form in service worker.
This manually reconstructs the POST as there is no support for formData
in safari, plus our storage mechanism does not handle formData as it's
not a simple object.
-rw-r--r-- | templates/web/base/offline/service_worker.html | 27 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 6 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/offline.js | 52 |
3 files changed, 76 insertions, 9 deletions
diff --git a/templates/web/base/offline/service_worker.html b/templates/web/base/offline/service_worker.html index 3c480f9b1..71e5c282a 100644 --- a/templates/web/base/offline/service_worker.html +++ b/templates/web/base/offline/service_worker.html @@ -52,15 +52,30 @@ addEventListener('fetch', fetchEvent => { } catch { fetchEvent.waitUntil(async function() { - var text = await request.text(); - let formData = new URLSearchParams(text); - formData.set('save', 2); - formData.set('saved_at', Math.floor(+new Date() / 1000)); - formData = formData.toString(); + var request_buffer = await request.arrayBuffer(); + var headers = request.headers; + let formData = {}; + formData.contentType = headers.get('Content-Type'); + let boundary_re = /.*boundary=(.*)/; + let bound = formData.contentType.match(boundary_re); + + let saved = '--' + bound[1] + "\r\nContent-Disposition: form-data; name=\"saved_at\"\r\n\r\n" + Math.floor(+new Date() / 1000) + "\r\n"; + var savedBuffer = new ArrayBuffer(saved.length); + var bufView = new Uint8Array(savedBuffer); + for (var i=0; i<saved.length; i++) { + bufView[i] = saved.charCodeAt(i); + } + + var tmp = new Uint8Array(request_buffer.byteLength + savedBuffer.byteLength); + tmp.set(new Uint8Array(request_buffer), 0); + tmp.set(bufView, 0); + tmp.set(new Uint8Array(request_buffer), savedBuffer.byteLength); + + formData.text = tmp.buffer; var data = await idbKeyval.get('offlineData') || { cachedReports: {}, forms: [] }; var forms = data.forms; - if (!forms.length || formData != forms[forms.length - 1][1]) { + if (!forms.length || tmp.toString() != new Uint8Array(forms[forms.length - 1][1].text).toString()) { forms.push([request.url, formData]); } return idbKeyval.set('offlineData', data); diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index 8073530c6..ad29ab470 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -666,6 +666,12 @@ $.extend(fixmystreet.set_up, { return; } + // we don't want to create this if we're offline (e.g using the inspector + // panel to add a photo) as the server side bit does not work. + if (!navigator.onLine) { + return; + } + // Pass a jQuery element, eg $('.foobar'), into this function // to limit all the selectors to that element. Handy if you want // to only bind/detect Dropzones in a particular part of the page, diff --git a/web/cobrands/fixmystreet/offline.js b/web/cobrands/fixmystreet/offline.js index 908326a69..c6609fe1a 100644 --- a/web/cobrands/fixmystreet/offline.js +++ b/web/cobrands/fixmystreet/offline.js @@ -28,6 +28,23 @@ fixmystreet.offlineBanner = (function() { $('.top_banner--offline').slideUp(); } + // Compare two typed arrays for equality + function isEqual(view1, view2) { + for (var i=0; i != view1.byteLength; i++) { + if (view1[i] != view2[i]) return false; + } + return true; + } + + // Create a Uint8Array of a string + function makeView(str) { + var view = new Uint8Array(str.length); + for (var i=0; i<str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; + } + return { make: function(offline) { fixmystreet.offlineData.getFormsLength().then(function(num) { @@ -61,7 +78,13 @@ fixmystreet.offlineBanner = (function() { } function postForm(url, data) { - return $.ajax({ url: url, data: data, type: 'POST' }).done(nextForm); + return $.ajax({ + url: url, + contentType: data.contentType, + data: data.text, + type: 'POST', + processData: false + }).done(nextForm); } $(document).on('click', '#oFN', function(e) { @@ -83,8 +106,23 @@ fixmystreet.offlineBanner = (function() { if (!token) { return nextForm(); } - var param = form[1].replace(/&token=[^&]*/, '&token=' + token); - return postForm(form[0], param).fail(nextForm); + + var tokenView = makeView(token); + var tokenName = makeView('name="token"\r\n\r\n'); + + // Make a typed array to update the request body with + // This only works because tokens are always the same length + var curView = new Uint8Array(form[1].text); + + // Find the spot at which the token is in the buffer + var idxS = curView.findIndex(function isToken(element, i, array) { + var sl = array.slice(i, i+tokenName.byteLength); + return isEqual(sl, tokenName); + }); + // Replace the old token with the new one in the right spot + curView.set(tokenView, idxS + tokenName.byteLength); + + return postForm(form[0], form[1]).fail(nextForm); }); }); }); @@ -302,6 +340,14 @@ fixmystreet.offline = (function() { $('.moderate-display.segmented-control, .shadow-wrap, #update_form, #report-cta, .mysoc-footer, .nav-wrapper').hide(); $('.js-back-to-report-list').attr('href', '/my/planned'); + // On iOS we want to hide the photo fields on the offline inspector + // form because including a photo entirely breaks the form submission. + if (/iPad|iPhone|iPod/.test(navigator.platform) || + (/Mac/.test(navigator.userAgent) && 'ontouchend' in document)) // iPadOS 13 pretends to be a desktop Mac + { + $("#form_photos, label[for=form_photo]").hide(); + } + // Refill form with saved data if there is any fixmystreet.offlineData.getForms().then(function(forms) { var savedForm; |