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.