Skip to content

Commit 7f545c3

Browse files
committed
Force an Asset Manager handshake on the draft stack
Each draft image on a page independently triggers its own Signon/Asset Manager OAuth handshake when there's no existing session. With more than one draft image, these handshakes race and clobber each other's CSRF state, so all but (at best) one image fail to load. In this commit, we load a single placeholder image from the draft-assets host, and once its request has settled - whether it succeeds or fails, we only care that the handshake has finished; we then retry every draft image already on the page with a cache-busting query param, so they get a fresh attempt against what should now be a warm session cookie.
1 parent d566cfd commit 7f545c3

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
/* Warms up the Asset Manager draft-assets session cookie via a single
3+
* placeholder image request, then retries any draft images already on
4+
* the page once that request has settled (success or failure).
5+
*
6+
* Usage: add `data-module="AssetManagerSession"` and
7+
* `data-placeholder-asset-url="..."` to an element.
8+
*/
9+
;(function (Modules) {
10+
function AssetManagerSession (element) {
11+
this.element = element
12+
}
13+
14+
AssetManagerSession.prototype.init = function () {
15+
const draftAssets = this.draftAssetImages()
16+
if (draftAssets.length < 2) return
17+
18+
const retry = this.retryDraftAssets.bind(this, draftAssets)
19+
const placeholder = new Image()
20+
placeholder.onload = retry
21+
placeholder.onerror = retry
22+
placeholder.src = this.element.getAttribute('data-placeholder-asset-url')
23+
}
24+
25+
AssetManagerSession.prototype.draftAssetImages = function () {
26+
return [...document.images].filter((image) =>
27+
image.src.includes('draft-assets.')
28+
)
29+
}
30+
31+
AssetManagerSession.prototype.retryDraftAssets = function (draftAssets) {
32+
draftAssets.forEach((image) => {
33+
const separator = image.src.includes('?') ? '&' : '?'
34+
image.src = `${image.src}${separator}_asset_manager_retry=${Date.now()}`
35+
})
36+
}
37+
38+
Modules.AssetManagerSession = AssetManagerSession
39+
})(window.GOVUK.Modules)

app/views/shared/_footer_navigation.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@
88
</div>
99
</div>
1010
<% end %>
11+
12+
<% if draft_host? %>
13+
<div
14+
data-module="AssetManagerSession"
15+
data-placeholder-asset-url="<%= placeholder_asset_url %>"
16+
hidden>
17+
</div>
18+
<% end %>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
describe('A asset manager session module', function () {
2+
let element, originalImage
3+
4+
const addImage = (src) => {
5+
const img = document.createElement('img')
6+
img.src = src
7+
document.body.appendChild(img)
8+
return img
9+
}
10+
11+
const stubImage = ({ succeeds }) => {
12+
function FakeImage () {}
13+
Object.defineProperty(FakeImage.prototype, 'src', {
14+
set () {
15+
succeeds ? this.onload && this.onload() : this.onerror && this.onerror()
16+
}
17+
})
18+
window.Image = FakeImage
19+
}
20+
21+
beforeEach(function () {
22+
originalImage = window.Image
23+
element = document.createElement('div')
24+
element.setAttribute('data-module', 'AssetManagerSession')
25+
element.setAttribute(
26+
'data-placeholder-asset-url',
27+
'https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg'
28+
)
29+
})
30+
31+
afterEach(function () {
32+
window.Image = originalImage
33+
document.querySelectorAll('img').forEach((img) => img.remove())
34+
})
35+
36+
it('does nothing when fewer than two draft images are on the page', function () {
37+
addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
38+
stubImage({ succeeds: true })
39+
40+
new GOVUK.Modules.AssetManagerSession(element).init()
41+
42+
expect(document.images[0].src).toEqual('https://draft-assets.test.gov.uk/media/1/one.jpg')
43+
})
44+
45+
it('retries all draft images once the placeholder request succeeds', function () {
46+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
47+
const second = addImage('https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar')
48+
stubImage({ succeeds: true })
49+
50+
new GOVUK.Modules.AssetManagerSession(element).init()
51+
52+
expect(first.src).toContain('one.jpg?_asset_manager_retry=')
53+
expect(second.src).toContain('two.jpg?foo=bar&_asset_manager_retry=')
54+
})
55+
56+
it('also retries all draft images when the placeholder request errors', function () {
57+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
58+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
59+
stubImage({ succeeds: false })
60+
61+
new GOVUK.Modules.AssetManagerSession(element).init()
62+
63+
expect(first.src).toContain('_asset_manager_retry=')
64+
})
65+
66+
it('ignores non-draft-assets images when counting/retrying', function () {
67+
const live = addImage('https://assets.test.gov.uk/media/1/one.jpg')
68+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
69+
stubImage({ succeeds: true })
70+
71+
new GOVUK.Modules.AssetManagerSession(element).init()
72+
73+
expect(live.src).toEqual('https://assets.test.gov.uk/media/1/one.jpg')
74+
})
75+
})

0 commit comments

Comments
 (0)