PageSourceSearch

https://loppingtonparishcouncil.gov.uk/wp-content/plugins/parish-plugin/assets/protect.js?ver=0.2.0.1790610780

js loppingtonparishcouncil.gov.uk collected 2026-10-02 22:49:02 UTC 32,275 bytes, 1,082 lines download raw bytes

1/**
2 * Parish Plugin — Protect
3 *
4 * Fetches a proof-of-work challenge when a visitor first touches a protected
5 * form, solves it in the background while they type, and puts the answer in a
6 * hidden field so the ordinary submission carries it.
7 *
8 * Nothing here enforces anything. Contact Form 7's submission endpoint is
9 * public and unauthenticated, so the decision is made on the server; this file
10 * exists to fill in a field. A bot that skips it simply fails verification.
11 *
12 * SHA-256 is implemented here rather than taken from Web Crypto. Three reasons:
13 * crypto.subtle exists only in a secure context, so a site accidentally served
14 * over plain HTTP would lose every message; its digest call is asynchronous,
15 * which is slow when the work is thousands of tiny hashes; and a hash we
16 * compute ourselves has no secure-context branch for an attacker to push the
17 * page down. A known-answer test runs at load, and the gate disables itself if
18 * it ever fails, so a mistake here breaks loudly rather than quietly.
19 *
20 * No dependencies, no build step, no network calls beyond this site's own
21 * challenge endpoint, no cookies and no storage.
22 */
23
24( function () {
25	'use strict';
26
27	var FIELD = '_parish_pow';
28	// The field's name before the leading underscore was added. A page cached
29	// before then still has it, but loads this script from the same address,
30	// so the old name is looked for too. Without that the field on such a page
31	// would never be filled in, and every message sent from it refused.
32	var LEGACY_FIELD = 'parish_pow';
33	// Set when the challenge address could not be reached, so a form sent
34	// without a proof tells the server why. It changes nothing about the
35	// outcome: the server refuses a form with no proof either way.
36	var STATUS_FIELD = '_parish_pow_status';
37	var SELECTOR = '[data-parish-protect]';
38	var MAX_WORKERS = 16;
39	// Above the highest protection level, so a legitimate challenge always fits.
40	var MAX_ATTEMPTS = 2000000;
41	// A replacement proof is worked out this far ahead of expiry and put into
42	// the form this much later, so the old one covers the gap between the two.
43	// The wait matters: the minimum fill time is measured from the moment the
44	// server issued the challenge, so a proof swapped in the instant it was
45	// made would look like a form filled in no time at all. Waiting means the
46	// replacement is already comfortably older than that check requires.
47	var SOLVE_LEAD_MS = 45000;
48	var SWAP_LEAD_MS = 5000;
49	var MIN_RENEW_DELAY_MS = 5000;
50	// How long a send may go without Contact Form 7 reporting an outcome before
51	// the next click is treated as a retry. It reports nothing at all when the
52	// request itself fails (a dropped connection, a firewall refusal, a
53	// timeout), so without a limit every later click would be swallowed and
54	// the visitor left with a form that silently does nothing.
55	var STUCK_MS = 20000;
56	// A form left open all day should stop asking for new challenges at some
57	// point. Past this, expiry falls back to being rejected once and retried.
58	var MAX_RENEWALS = 12;
59
60	/**
61	 * Returns a SHA-256 function over a single-byte-per-character string.
62	 *
63	 * Written as a factory so the same source can be handed to a Worker without
64	 * keeping two copies of the algorithm in step.
65	 *
66	 * Inputs here are always short and drawn from [0-9a-f|], so the message can
67	 * be read a character at a time with no UTF-8 encoding step.
68	 */
69	function shaFactory() {
70		var K = new Uint32Array( [
71			0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
72			0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
73			0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
74			0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
75			0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
76			0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
77			0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
78			0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
79		] );
80
81		var HEX = [];
82		for ( var h = 0; h < 256; h++ ) {
83			HEX.push( ( h < 16 ? '0' : '' ) + h.toString( 16 ) );
84		}
85
86		var W = new Uint32Array( 64 );
87		var H = new Uint32Array( 8 );
88
89		return function ( msg ) {
90			var len = msg.length;
91			var blocks = ( len + 9 + 63 ) >> 6;
92			var words = new Uint32Array( blocks << 4 );
93			var i;
94
95			for ( i = 0; i < len; i++ ) {
96				words[ i >> 2 ] |= ( msg.charCodeAt( i ) & 0xff ) << ( 24 - ( ( i & 3 ) << 3 ) );
97			}
98
99			words[ len >> 2 ] |= 0x80 << ( 24 - ( ( len & 3 ) << 3 ) );
100			words[ ( blocks << 4 ) - 1 ] = len << 3;
101
102			H[ 0 ] = 0x6a09e667; H[ 1 ] = 0xbb67ae85; H[ 2 ] = 0x3c6ef372; H[ 3 ] = 0xa54ff53a;
103			H[ 4 ] = 0x510e527f; H[ 5 ] = 0x9b05688c; H[ 6 ] = 0x1f83d9ab; H[ 7 ] = 0x5be0cd19;
104
105			for ( var b = 0; b < blocks; b++ ) {
106				var off = b << 4;
107
108				for ( i = 0; i < 16; i++ ) {
109					W[ i ] = words[ off + i ];
110				}
111
112				for ( i = 16; i < 64; i++ ) {
113					var x = W[ i - 15 ];
114					var y = W[ i - 2 ];
115					var s0 = ( ( x >>> 7 ) | ( x << 25 ) ) ^ ( ( x >>> 18 ) | ( x << 14 ) ) ^ ( x >>> 3 );
116					var s1 = ( ( y >>> 17 ) | ( y << 15 ) ) ^ ( ( y >>> 19 ) | ( y << 13 ) ) ^ ( y >>> 10 );
117					W[ i ] = ( W[ i - 16 ] + s0 + W[ i - 7 ] + s1 ) >>> 0;
118				}
119
120				var a = H[ 0 ], bb = H[ 1 ], c = H[ 2 ], d = H[ 3 ];
121				var e = H[ 4 ], f = H[ 5 ], g = H[ 6 ], hh = H[ 7 ];
122
123				for ( i = 0; i < 64; i++ ) {
124					var S1 = ( ( e >>> 6 ) | ( e << 26 ) ) ^ ( ( e >>> 11 ) | ( e << 21 ) ) ^ ( ( e >>> 25 ) | ( e << 7 ) );
125					var ch = ( e & f ) ^ ( ~e & g );
126					var t1 = ( hh + S1 + ch + K[ i ] + W[ i ] ) >>> 0;
127					var S0 = ( ( a >>> 2 ) | ( a << 30 ) ) ^ ( ( a >>> 13 ) | ( a << 19 ) ) ^ ( ( a >>> 22 ) | ( a << 10 ) );
128					var maj = ( a & bb ) ^ ( a & c ) ^ ( bb & c );
129					var t2 = ( S0 + maj ) >>> 0;
130
131					hh = g; g = f; f = e; e = ( d + t1 ) >>> 0;
132					d = c; c = bb; bb = a; a = ( t1 + t2 ) >>> 0;
133				}
134
135				H[ 0 ] = ( H[ 0 ] + a ) >>> 0; H[ 1 ] = ( H[ 1 ] + bb ) >>> 0;
136				H[ 2 ] = ( H[ 2 ] + c ) >>> 0; H[ 3 ] = ( H[ 3 ] + d ) >>> 0;
137				H[ 4 ] = ( H[ 4 ] + e ) >>> 0; H[ 5 ] = ( H[ 5 ] + f ) >>> 0;
138				H[ 6 ] = ( H[ 6 ] + g ) >>> 0; H[ 7 ] = ( H[ 7 ] + hh ) >>> 0;
139			}
140
141			var out = '';
142			for ( i = 0; i < 8; i++ ) {
143				var v = H[ i ];
144				out += HEX[ ( v >>> 24 ) & 0xff ] + HEX[ ( v >>> 16 ) & 0xff ] + HEX[ ( v >>> 8 ) & 0xff ] + HEX[ v & 0xff ];
145			}
146
147			return out;
148		};
149	}
150
151	var sha256 = shaFactory();
152
153	// Known-answer test. If this fails the algorithm is wrong, so the gate is
154	// switched off rather than left to block every submission on the site.
155	var HEALTHY = sha256( 'abc' ) === 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad';
156
157	if ( ! HEALTHY && window.console && window.console.error ) {
158		window.console.error( 'Parish Protect: hash self-test failed; verification is disabled on this page.' );
159	}
160
161	function workerCount() {
162		var cores = navigator.hardwareConcurrency || 4;
163		var memory = navigator.deviceMemory;
164
165		if ( memory && memory <= 4 ) {
166			cores = Math.min( 4, cores );
167		}
168
169		return Math.max( 1, Math.min( MAX_WORKERS, cores ) );
170	}
171
172	function workerSource() {
173		return 'var sha256 = (' + shaFactory.toString() + ')();\n' +
174			'self.onmessage = function (e) {\n' +
175			'  var d = e.data, prefix = d.salt + "|";\n' +
176			'  for (var n = d.start; n <= d.max; n += d.step) {\n' +
177			'    if (sha256(prefix + n) === d.target) { self.postMessage(n); return; }\n' +
178			'  }\n' +
179			'  self.postMessage(-1);\n' +
180			'};';
181	}
182
183	/**
184	 * Is this a challenge we are willing to work on?
185	 *
186	 * Only this site can sign a challenge, so a hostile one should be
187	 * unreachable. The check is here anyway because the cost of being wrong is
188	 * a visitor's browser grinding on a counter range that never ends, and the
189	 * cost of the check is four comparisons.
190	 */
191	function usable( wire ) {
192		return !! wire
193			&& 'string' === typeof wire.salt
194			&& 'string' === typeof wire.chal
195			&& 64 === wire.chal.length
196			&& 'number' === typeof wire.max
197			&& wire.max > 0
198			&& wire.max <= MAX_ATTEMPTS;
199	}
200
201	/**
202	 * Search the counter range for the answer.
203	 *
204	 * Workers take interleaved slices rather than contiguous blocks, so they
205	 * finish at similar times whichever part of the range holds the answer, and
206	 * the first to succeed cancels the rest.
207	 */
208	function solve( wire ) {
209		return new Promise( function ( resolve, reject ) {
210			var count = workerCount();
211			var workers = [];
212			var url = null;
213			var settled = false;
214			var finished = 0;
215
216			function cleanup() {
217				workers.forEach( function ( worker ) {
218					try {
219						worker.terminate();
220					} catch ( e ) {} // eslint-disable-line no-empty
221				} );
222				workers.length = 0;
223
224				if ( url ) {
225					URL.revokeObjectURL( url );
226					url = null;
227				}
228			}
229
230			function succeed( n ) {
231				if ( settled ) {
232					return;
233				}
234				settled = true;
235				cleanup();
236				resolve( n );
237			}
238
239			function fail( error ) {
240				if ( settled ) {
241					return;
242				}
243				settled = true;
244				cleanup();
245				reject( error );
246			}
247
248			try {
249				if ( typeof Worker !== 'function' || typeof Blob !== 'function' || ! window.URL || ! URL.createObjectURL ) {
250					throw new Error( 'workers unavailable' );
251				}
252
253				url = URL.createObjectURL( new Blob( [ workerSource() ], { type: 'text/javascript' } ) );
254
255				for ( var i = 0; i < count; i++ ) {
256					var worker = new Worker( url );
257
258					worker.onmessage = function ( event ) {
259						if ( event.data >= 0 ) {
260							succeed( event.data );
261							return;
262						}
263
264						finished++;
265
266						if ( finished >= count ) {
267							fail( new Error( 'no solution in range' ) );
268						}
269					};
270
271					// A worker that fails after it started is the same situation as
272					// one that could never be created, and that case already falls
273					// back to the main thread. A Content-Security-Policy without
274					// worker-src blob: reports the violation this way in some
275					// browsers, and refusing here would mean every visitor on such
276					// a site is told their message could not be verified.
277					worker.onerror = function () {
278						if ( settled ) {
279							return;
280						}
281
282						settled = true;
283						cleanup();
284						solveOnMainThread( wire ).then( resolve, reject );
285					};
286
287					worker.postMessage( {
288						salt: wire.salt,
289						target: wire.chal,
290						max: wire.max,
291						start: i,
292						step: count
293					} );
294
295					workers.push( worker );
296				}
297			} catch ( e ) {
298				cleanup();
299				solveOnMainThread( wire ).then( resolve, reject );
300			}
301		} );
302	}
303
304	/**
305	 * Fallback for browsers without Workers. Runs in slices with a yield between
306	 * them, so the page stays responsive while it searches.
307	 */
308	function solveOnMainThread( wire ) {
309		return new Promise( function ( resolve, reject ) {
310			var prefix = wire.salt + '|';
311			var n = 0;
312			var SLICE = 500;
313
314			function step() {
315				var end = Math.min( wire.max, n + SLICE );
316
317				for ( ; n <= end; n++ ) {
318					if ( sha256( prefix + n ) === wire.chal ) {
319						resolve( n );
320						return;
321					}
322				}
323
324				if ( n > wire.max ) {
325					reject( new Error( 'no solution in range' ) );
326					return;
327				}
328
329				setTimeout( step, 0 );
330			}
331
332			step();
333		} );
334	}
335
336	function encode( wire, n ) {
337		var payload = {
338			v: wire.v,
339			alg: wire.alg,
340			form: wire.form,
341			iat: wire.iat,
342			exp: wire.exp,
343			max: wire.max,
344			salt: wire.salt,
345			chal: wire.chal,
346			sig: wire.sig,
347			n: n
348		};
349
350		return btoa( JSON.stringify( payload ) );
351	}
352
353	/**
354	 * One protected form.
355	 */
356	function Guard( container, form ) {
357		this.container = container;
358		this.form = form;
359		this.field = container.querySelector( 'input[name="' + FIELD + '"]' )
360			|| container.querySelector( 'input[name="' + LEGACY_FIELD + '"]' );
361		this.status = container.querySelector( '.parish-protect__status' );
362		this.statusField = container.querySelector( 'input[name="' + STATUS_FIELD + '"]' );
363		this.url = container.getAttribute( 'data-challenge-url' );
364
365		// A challenge printed into the page, on pages that are never cached.
366		// Used once, first, so the form works even where the challenge address
367		// cannot be reached. Anything unreadable is ignored and the address is
368		// asked instead.
369		this.inline = null;
370
371		// How far this device's clock is from the server's, in milliseconds.
372		// See serverNow().
373		this.skew = 0;
374
375		var printed = container.getAttribute( 'data-challenge' );
376
377		if ( printed ) {
378			try {
379				this.inline = JSON.parse( printed );
380			} catch ( e ) {
381				this.inline = null;
382			}
383
384			// Printed as the page was built, a moment ago.
385			this.learnClock( this.inline );
386		}
387		this.pending = null;
388		this.passthrough = false;
389		this.started = false;
390		this.timer = null;
391		this.swapTimer = null;
392		this.expiresAt = 0;
393		this.issuedAt = 0;
394		this.inflight = false;
395		this.inflightAt = 0;
396		this.renewals = 0;
397		this.onVisible = null;
398		this.spare = null;
399
400		// How long the server insists a form takes to fill in. Sent with the
401		// markup so the wait above can be right for whatever this site has set,
402		// rather than right for the default and wrong for everyone else. It is
403		// no secret: a script that wanted to know could simply measure it.
404		var declared = parseInt( container.getAttribute( 'data-min-fill' ) || '0', 10 );
405
406		this.minFillMs = isNaN( declared ) || declared < 0 ? 0 : declared * 1000;
407	}
408
409	Guard.prototype.hasProof = function () {
410		return !! ( this.field && this.field.value );
411	};
412
413	/**
414	 * Is the proof in the form one the server will actually accept?
415	 *
416	 * Holding a proof is not the same as holding a usable one. It can be past
417	 * its expiry, because timers are frozen in a background tab and a page
418	 * restored from the back button resumes with whatever it had. It can be too
419	 * new, because the minimum fill time is measured from the moment the server
420	 * issued it. Both were previously waved through and refused by the server.
421	 */
422	Guard.prototype.usable = function () {
423		if ( ! this.hasProof() || ! this.expiresAt ) {
424			return false;
425		}
426
427		var now = this.serverNow();
428
429		if ( now >= ( this.expiresAt * 1000 ) - SWAP_LEAD_MS ) {
430			return false;
431		}
432
433		return now >= ( this.issuedAt * 1000 ) + this.minFillMs;
434	};
435
436	/**
437	 * Milliseconds until the proof in the form is old enough to be accepted.
438	 */
439	Guard.prototype.waitRemaining = function () {
440		var ready = ( this.issuedAt * 1000 ) + this.minFillMs;
441
442		return Math.max( 0, ready - this.serverNow() );
443	};
444
445	/**
446	 * The time on the server's clock, as best this page can tell.
447	 *
448	 * Every expiry and issue time comes from the server, and a visitor's
449	 * device can be minutes out. Compared with the device's own clock, a slow
450	 * one makes a fresh proof look too new, so the form is held back until the
451	 * server has already let it expire, and the visitor can never send; a fast
452	 * one makes a fresh challenge look expired before it is used.
453	 *
454	 * A challenge arrives within moments of being issued, so its issue time is
455	 * the server's clock at that point, give or take the trip. The difference
456	 * is taken each time one arrives. Issue times are whole seconds rounded
457	 * down, which makes the server look up to a second earlier than it is: the
458	 * safe direction, since it can only make the page wait slightly longer.
459	 */
460	Guard.prototype.serverNow = function () {
461		return Date.now() + this.skew;
462	};
463
464	/**
465	 * Take the device's clock offset from a challenge that has just arrived.
466	 */
467	Guard.prototype.learnClock = function ( wire ) {
468		if ( wire && 'number' === typeof wire.iat && isFinite( wire.iat ) ) {
469			this.skew = ( wire.iat * 1000 ) - Date.now();
470		}
471	};
472
473	/**
474	 * Resolve once the form holds a proof the server will accept, replacing an
475	 * expired one and waiting out the minimum fill time where that is all that
476	 * is missing. Waiting is right and refetching is wrong: a new challenge
477	 * carries a new issue time and would restart the same wait.
478	 */
479	Guard.prototype.ensureUsable = function () {
480		var guard = this;
481
482		if ( this.usable() ) {
483			return Promise.resolve();
484		}
485
486		var fresh = this.hasProof()
487			&& this.expiresAt
488			&& this.serverNow() < ( this.expiresAt * 1000 ) - SWAP_LEAD_MS;
489
490		var step = fresh ? Promise.resolve() : this.replace();
491
492		return step.then( function () {
493			var wait = guard.waitRemaining();
494
495			if ( wait <= 0 ) {
496				return undefined;
497			}
498
499			return new Promise( function ( resolve ) {
500				setTimeout( resolve, wait );
501			} );
502		} );
503	};
504
505	/**
506	 * Discard whatever is in the form and work out a new proof.
507	 */
508	Guard.prototype.replace = function () {
509		this.cancelRenewal();
510
511		if ( this.field ) {
512			this.field.value = '';
513		}
514
515		this.pending = null;
516		this.expiresAt = 0;
517		this.issuedAt = 0;
518
519		return this.prepare();
520	};
521
522	Guard.prototype.showStatus = function ( visible ) {
523		if ( this.status ) {
524			this.status.hidden = ! visible;
525		}
526	};
527
528	/**
529	 * Fetch a challenge and solve it. Safe to call repeatedly: one attempt runs
530	 * at a time, and a completed one is not repeated until the field is cleared.
531	 */
532	Guard.prototype.prepare = function () {
533		var guard = this;
534
535		if ( ! HEALTHY || ! this.field || ! this.url ) {
536			return Promise.reject( new Error( 'unavailable' ) );
537		}
538
539		if ( this.hasProof() ) {
540			return Promise.resolve();
541		}
542
543		if ( this.pending ) {
544			return this.pending;
545		}
546
547		this.started = true;
548
549		this.pending = this.obtain()
550			.then( function ( proof ) {
551				guard.field.value = proof.value;
552				guard.issuedAt = proof.wire.iat;
553				guard.pending = null;
554				guard.scheduleRenewal( proof.wire );
555			} )
556			.catch( function ( error ) {
557				guard.pending = null;
558				throw error;
559			} );
560
561		return this.pending;
562	};
563
564	/**
565	 * Ask for a challenge and work out the answer, without touching the form.
566	 *
567	 * Kept separate from putting it in the field, because a renewal has to hold
568	 * a finished proof back for a while before using it. See scheduleRenewal.
569	 */
570	Guard.prototype.obtain = function () {
571		var guard = this;
572		var printed = this.takeInline();
573
574		var source = printed
575			? Promise.resolve( printed )
576			: fetch( this.url, {
577				credentials: 'same-origin',
578				headers: { Accept: 'application/json' }
579			} ).then(
580				function ( response ) {
581					if ( ! response.ok ) {
582						return blockedBySomethingElse( response ).then( function ( blocked ) {
583							if ( blocked ) {
584								guard.markUnreachable();
585							}
586							throw new Error( 'challenge request failed' );
587						} );
588					}
589					return response.json().then( function ( wire ) {
590						guard.learnClock( wire );
591						return wire;
592					} );
593				},
594				function ( error ) {
595					guard.markUnreachable();
596					throw error;
597				}
598			);
599
600		return source
601			.then( function ( wire ) {
602				if ( ! usable( wire ) ) {
603					throw new Error( 'challenge was not usable' );
604				}
605
606				return solve( wire ).then( function ( n ) {
607					guard.markReachable();
608					return { wire: wire, value: encode( wire, n ) };
609				} );
610			} );
611	};
612
613	/**
614	 * Did something other than Protect refuse the challenge request?
615	 *
616	 * Only that is worth reporting as a blocked address, because it is what
617	 * the advice on the Protect screen says to fix. A 401 or 403 is a security
618	 * plugin or firewall. A 404 is too, unless it is Protect's own answer for a
619	 * page or form it is not serving, as happens on a page cached before that
620	 * page was switched off. A 429 is Protect's own throttle, and a server
621	 * error is a fault, not a block; neither is reported.
622	 */
623	function blockedBySomethingElse( response ) {
624		if ( 401 === response.status || 403 === response.status ) {
625			return Promise.resolve( true );
626		}
627
628		if ( 404 !== response.status ) {
629			return Promise.resolve( false );
630		}
631
632		return response.json().then(
633			function ( body ) {
634				return ! ( body && 'parish_plugin_unknown_form' === body.code );
635			},
636			function () {
637				// Not JSON at all: a web server or firewall page, not WordPress.
638				return true;
639			}
640		);
641	}
642
643	/**
644	 * The printed challenge, once, and only while it has time left to be used.
645	 */
646	Guard.prototype.takeInline = function () {
647		var wire = this.inline;
648
649		this.inline = null;
650
651		if ( ! usable( wire ) || 'number' !== typeof wire.exp ) {
652			return null;
653		}
654
655		return ( wire.exp * 1000 ) - this.serverNow() > SWAP_LEAD_MS + MIN_RENEW_DELAY_MS ? wire : null;
656	};
657
658	Guard.prototype.markUnreachable = function () {
659		if ( this.statusField ) {
660			this.statusField.value = 'unreachable';
661		}
662	};
663
664	Guard.prototype.markReachable = function () {
665		if ( this.statusField ) {
666			this.statusField.value = '';
667		}
668	};
669
670	/**
671	 * Replace the proof before it expires, rather than letting a visitor find
672	 * out at the point of sending.
673	 *
674	 * A proof is only valid for the challenge lifetime, which is five minutes by
675	 * default. Someone writing a considered message to their council can easily
676	 * take longer than that, and being told their message could not be verified
677	 * is no way to treat them. So the answer is quietly worked out again in the
678	 * background while they type.
679	 *
680	 * ALTCHA does the same thing, and has since it grew a challenge lifetime:
681	 * its widget reads the expiry out of the challenge, sets a timer, and
682	 * refetches on its own unless refetchonexpire is turned off.
683	 */
684	Guard.prototype.scheduleRenewal = function ( wire ) {
685		this.cancelRenewal();
686
687		if ( this.renewals >= MAX_RENEWALS ) {
688			return;
689		}
690
691		var expires = wire && 'number' === typeof wire.exp ? wire.exp : 0;
692
693		if ( ! expires ) {
694			return;
695		}
696
697		// Kept because the swap is timed against the proof in the form, not
698		// against its replacement. Scheduling from the replacement's own expiry
699		// would leave the old one in place for minutes after it had run out.
700		this.expiresAt = expires;
701
702		var delay = ( expires * 1000 ) - this.serverNow() - this.solveLead();
703
704		if ( delay < MIN_RENEW_DELAY_MS ) {
705			delay = MIN_RENEW_DELAY_MS;
706		}
707
708		var guard = this;
709
710		this.timer = setTimeout( function () {
711			guard.timer = null;
712			guard.renew();
713		}, delay );
714	};
715
716	/**
717	 * How far ahead of expiry the replacement is worked out.
718	 *
719	 * Far enough that by the time it is swapped in it is already older than the
720	 * minimum fill time, whatever this site has that set to.
721	 */
722	Guard.prototype.solveLead = function () {
723		var needed = this.minFillMs + SWAP_LEAD_MS + 5000;
724
725		return needed > SOLVE_LEAD_MS ? needed : SOLVE_LEAD_MS;
726	};
727
728	Guard.prototype.cancelRenewal = function () {
729		if ( this.timer ) {
730			clearTimeout( this.timer );
731			this.timer = null;
732		}
733
734		if ( this.swapTimer ) {
735			clearTimeout( this.swapTimer );
736			this.swapTimer = null;
737		}
738
739		this.spare = null;
740
741		if ( this.onVisible ) {
742			document.removeEventListener( 'visibilitychange', this.onVisible );
743			this.onVisible = null;
744		}
745	};
746
747	/**
748	 * Work out a fresh answer in place of the one about to expire.
749	 *
750	 * A hidden tab waits until it is looked at again. Solving costs real
751	 * processor time, and spending a laptop's battery on a form nobody is
752	 * currently reading would be rude.
753	 */
754	Guard.prototype.renew = function () {
755		var guard = this;
756
757		if ( document.hidden ) {
758			this.onVisible = function () {
759				document.removeEventListener( 'visibilitychange', guard.onVisible );
760				guard.onVisible = null;
761
762				if ( ! document.hidden ) {
763					guard.renew();
764				}
765			};
766
767			document.addEventListener( 'visibilitychange', this.onVisible );
768			return;
769		}
770
771		this.renewals++;
772
773		// The old proof stays in the form throughout. It is still good for
774		// another three quarters of a minute, and leaving it there means the
775		// visitor can send at any point during this without waiting.
776		this.obtain().then(
777			function ( proof ) {
778				guard.spare = proof;
779				guard.scheduleSwap();
780			},
781			function () {
782				// The replacement could not be made. The old proof is still in
783				// place and still valid, so there is nothing to undo and nothing
784				// worth telling the visitor.
785			}
786		);
787	};
788
789	/**
790	 * Put the replacement into the form once it is old enough to pass the
791	 * minimum fill time, and shortly before the old one runs out.
792	 */
793	Guard.prototype.scheduleSwap = function () {
794		var guard = this;
795		var delay = ( this.expiresAt * 1000 ) - this.serverNow() - SWAP_LEAD_MS;
796
797		if ( delay < 0 ) {
798			delay = 0;
799		}
800
801		this.swapTimer = setTimeout( function () {
802			guard.swapTimer = null;
803			guard.swap();
804		}, delay );
805	};
806
807	Guard.prototype.swap = function () {
808		var proof = this.spare;
809
810		if ( ! proof || ! this.field ) {
811			return;
812		}
813
814		this.spare = null;
815		this.field.value = proof.value;
816		this.issuedAt = proof.wire.iat;
817
818		this.scheduleRenewal( proof.wire );
819	};
820
821	/**
822	 * Send the form on its way now that it carries a proof.
823	 *
824	 * A Contact Form 7 form goes through its own public API, because calling it
825	 * fires no submit event and so cannot re-enter the gate. Anything else, a
826	 * log in or comment form among them, gets a real submit, dispatched with a
827	 * flag that lets it through. Handing a comment form to Contact Form 7 just
828	 * because Contact Form 7 is on the same page would send it nowhere.
829	 */
830	Guard.prototype.release = function ( submitter ) {
831		var wpcf7 = window.wpcf7;
832		var isCf7 = ( ' ' + ( this.form.getAttribute( 'class' ) || '' ) + ' ' ).indexOf( ' wpcf7-form ' ) !== -1;
833
834		if ( isCf7 && wpcf7 && typeof wpcf7.submit === 'function' ) {
835			wpcf7.submit( this.form, { submitter: submitter } );
836			return;
837		}
838
839		this.passthrough = true;
840
841		if ( typeof this.form.requestSubmit === 'function' ) {
842			this.form.requestSubmit( submitter || undefined );
843		} else {
844			this.form.submit();
845		}
846	};
847
848	Guard.prototype.clear = function () {
849		this.cancelRenewal();
850
851		if ( this.field ) {
852			this.field.value = '';
853		}
854		this.pending = null;
855		this.started = false;
856		this.inflight = false;
857		this.expiresAt = 0;
858		this.issuedAt = 0;
859		this.renewals = 0;
860		this.markReachable();
861	};
862
863	var guards = [];
864
865	function guardFor( form ) {
866		for ( var i = 0; i < guards.length; i++ ) {
867			if ( guards[ i ].form === form ) {
868				return guards[ i ];
869			}
870		}
871		return null;
872	}
873
874	function attach( container ) {
875		var form = container.closest ? container.closest( 'form' ) : null;
876
877		if ( ! form ) {
878			return;
879		}
880
881		var existing = guardFor( form );
882
883		if ( existing ) {
884			// A second copy of the widget in the same form, as happens when a
885			// theme or plugin fires the log in form's hook twice. Its field is
886			// never filled, and the server reads the last field of a name, so
887			// an empty duplicate would hide the real proof and refuse the form.
888			// Taken out of the submission instead.
889			if ( existing.container !== container ) {
890				// The status field too. The server would read the copy's empty
891				// value in place of the real one, and a form that could not
892				// reach the challenge address would not say so.
893				[ FIELD, LEGACY_FIELD, STATUS_FIELD ].forEach( function ( name ) {
894					var spare = container.querySelector( 'input[name="' + name + '"]' );
895
896					if ( spare ) {
897						spare.disabled = true;
898					}
899				} );
900			}
901			return;
902		}
903
904		var guard = new Guard( container, form );
905		guards.push( guard );
906
907		// The challenge is fetched when the visitor first engages with the form,
908		// not when the page is rendered. That keeps anything time-sensitive out
909		// of a cached page, lets the minimum-fill-time check use a timestamp the
910		// server issued and signed, and means the work is already done by the
911		// time anyone presses send.
912		var begin = function () {
913			form.removeEventListener( 'focusin', begin );
914			form.removeEventListener( 'input', begin );
915			form.removeEventListener( 'change', begin );
916
917			guard.prepare().catch( function () {} );
918		};
919
920		form.addEventListener( 'focusin', begin );
921		form.addEventListener( 'input', begin );
922		form.addEventListener( 'change', begin );
923	}
924
925	/**
926	 * Forget forms that have left the page.
927	 *
928	 * A guard that outlives its form goes on renewing a proof nobody can send,
929	 * once every few minutes, up to the renewal cap. Being hidden is not the
930	 * same as being gone: the feedback banner in the older parish theme sits in
931	 * the page until it is opened, and it is still connected, so it is kept.
932	 */
933	function sweep() {
934		for ( var i = guards.length - 1; i >= 0; i-- ) {
935			var form = guards[ i ].form;
936
937			// Browsers that do not report this keep everything, which is the
938			// behaviour these guards had before.
939			if ( form && false === form.isConnected ) {
940				guards[ i ].cancelRenewal();
941				guards.splice( i, 1 );
942			}
943		}
944	}
945
946	function scan() {
947		var containers = document.querySelectorAll( SELECTOR );
948
949		for ( var i = 0; i < containers.length; i++ ) {
950			attach( containers[ i ] );
951		}
952	}
953
954	/**
955	 * Hold a submission back only while the answer is still being worked out.
956	 *
957	 * Contact Form 7 registers its own submit listener in the bubbling phase on
958	 * the form itself, and does not check whether the event was already
959	 * cancelled. preventDefault alone therefore would not stop it. A capturing
960	 * listener on the document runs first and stopPropagation keeps the event
961	 * from reaching it at all.
962	 */
963	function onSubmitCapture( event ) {
964		var form = event.target;
965
966		if ( ! form || 'FORM' !== form.tagName ) {
967			return;
968		}
969
970		var guard = guardFor( form );
971
972		if ( ! guard ) {
973			return;
974		}
975
976		if ( guard.passthrough ) {
977			guard.passthrough = false;
978			return;
979		}
980
981		if ( ! HEALTHY ) {
982			return;
983		}
984
985		// A proof can only be spent once. Contact Form 7 does not guard against a
986		// second click, so without this the same proof goes twice: the message is
987		// sent, and then the replay is refused and that is the answer the visitor
988		// is left looking at.
989		if ( guard.inflight && Date.now() - guard.inflightAt < STUCK_MS ) {
990			event.preventDefault();
991			event.stopPropagation();
992			return;
993		}
994
995		// No outcome ever arrived for the last send. Start again with a fresh
996		// proof: if that send did get through after all, the retry is a second
997		// copy of the message rather than a refusal shown as the answer.
998		if ( guard.inflight ) {
999			guard.clear();
1000		}
1001
1002		if ( guard.usable() ) {
1003			guard.inflight = true;
1004			guard.inflightAt = Date.now();
1005			return;
1006		}
1007
1008		// Either there is no proof yet, or the one in the form is expired or not
1009		// yet old enough. Hold the submission, put that right, then release it.
1010		event.preventDefault();
1011		event.stopPropagation();
1012
1013		var submitter = event.submitter || null;
1014
1015		guard.inflight = true;
1016		guard.inflightAt = Date.now();
1017		guard.showStatus( true );
1018
1019		var go = function () {
1020			guard.showStatus( false );
1021			guard.release( submitter );
1022		};
1023
1024		// Both ways lead to release. A form that hangs with no message is worse
1025		// than one that says it could not be verified.
1026		guard.ensureUsable().then( go, go );
1027	}
1028
1029	/**
1030	 * Contact Form 7 fires wpcf7submit last, whatever the outcome. Challenges
1031	 * are single use, so the next attempt needs a fresh one.
1032	 */
1033	function onCf7Submit( event ) {
1034		var guard = guardFor( event.target );
1035
1036		if ( ! guard ) {
1037			return;
1038		}
1039
1040		guard.clear();
1041		guard.prepare().catch( function () {} );
1042
1043	}
1044
1045	function init() {
1046		scan();
1047
1048		document.addEventListener( 'submit', onSubmitCapture, true );
1049		document.addEventListener( 'wpcf7submit', onCf7Submit );
1050
1051		// Coming back to a page with the Back button can restore it as it was
1052		// left, mid-send and holding a proof already spent. Start each form
1053		// afresh, so the next attempt gets a new proof and is not held back as
1054		// a double click.
1055		if ( typeof window.addEventListener === 'function' ) {
1056			window.addEventListener( 'pageshow', function ( event ) {
1057				if ( ! event.persisted ) {
1058					return;
1059				}
1060
1061				guards.forEach( function ( guard ) {
1062					guard.clear();
1063				} );
1064			} );
1065		}
1066
1067		// Forms can arrive later: a popup, a tab, or the feedback banner in the
1068		// older parish theme, which is in the page but hidden until it is opened.
1069		if ( typeof MutationObserver === 'function' ) {
1070			new MutationObserver( function () {
1071				scan();
1072				sweep();
1073			} ).observe( document.documentElement, { childList: true, subtree: true } );
1074		}
1075	}
1076
1077	if ( 'loading' === document.readyState ) {
1078		document.addEventListener( 'DOMContentLoaded', init );
1079	} else {
1080		init();
1081	}
1082}() );

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.