3232use RocketTheme \Toolbox \Event \Event ;
3333use RuntimeException ;
3434use Symfony \Contracts \HttpClient \Exception \TransportExceptionInterface ;
35- use Twig \Environment ;
3635use Twig \Extension \CoreExtension ;
37- use Twig \Extension \EscaperExtension ;
3836use Twig \TwigFunction ;
3937use function count ;
4038use function function_exists ;
@@ -116,7 +114,20 @@ class_alias(Form::class, 'Grav\Plugin\Form');
116114
117115 // Initialize the captcha manager
118116 CaptchaManager::initialize ();
119-
117+
118+ /** @var Uri $uri */
119+ $ uri = $ this ->grav ['uri ' ];
120+
121+ // Refresh Nonce Logic - Run early to catch both frontend and admin
122+ // Uri::param() returns false when missing, so use fallback even on falsey values.
123+ $ task = $ uri ->param ('task ' ) ?: $ uri ->query ('task ' ) ?: ($ _REQUEST ['task ' ] ?? null );
124+ if ($ task === 'get-nonce ' ) {
125+ $ action = $ uri ->param ('action ' ) ?: $ uri ->query ('action ' ) ?: ($ _REQUEST ['action ' ] ?? 'form ' );
126+ $ nonce = Utils::getNonce ($ action );
127+ $ response = new Response (200 , ['Content-Type ' => 'application/json ' ], json_encode (['nonce ' => $ nonce ]));
128+
129+ $ this ->grav ->close ($ response );
130+ }
120131
121132 if ($ this ->isAdmin ()) {
122133 $ this ->enable ([
@@ -126,11 +137,7 @@ class_alias(Form::class, 'Grav\Plugin\Form');
126137 return ;
127138 }
128139
129- /** @var Uri $uri */
130- $ uri = $ this ->grav ['uri ' ];
131-
132140 // Mini Keep-Alive Logic
133- $ task = $ uri ->param ('task ' );
134141 if ($ task === 'keep-alive ' ) {
135142 $ response = new Response (200 );
136143
@@ -368,17 +375,15 @@ public function onTwigInitialized(): void
368375 new TwigFunction ('forms ' , [$ this , 'getForm ' ])
369376 );
370377
371- if (Environment::VERSION_ID > 20000 ) {
372- // Twig 2/3
373- $ this ->grav ['twig ' ]->twig ()->getExtension (EscaperExtension::class)->setEscaper (
374- 'yaml ' ,
375- function ($ twig , $ string , $ charset ) {
376- return Yaml::dump ($ string );
377- }
378- );
378+ // Grav 1.8+ has setEscaper() helper that handles all Twig versions
379+ $ twig = $ this ->grav ['twig ' ];
380+ if (method_exists ($ twig , 'setEscaper ' )) {
381+ $ twig ->setEscaper ('yaml ' , function ($ twig , $ string , $ charset ) {
382+ return Yaml::dump ($ string );
383+ });
379384 } else {
380- // Twig 1.x
381- $ this -> grav [ ' twig ' ] ->twig ()->getExtension (CoreExtension::class)->setEscaper (
385+ // Grav 1.7 with Twig 1.x
386+ $ twig ->twig ()->getExtension (CoreExtension::class)->setEscaper (
382387 'yaml ' ,
383388 function ($ twig , $ string , $ charset ) {
384389 return Yaml::dump ($ string );
@@ -441,6 +446,18 @@ public function onTwigVariables(?Event $event = null): void
441446 if ($ this ->config ->get ('plugins.form.built_in_css ' )) {
442447 $ this ->grav ['assets ' ]->addCss ('plugin://form/assets/form-styles.css ' );
443448 }
449+ if ($ this ->config ->get ('plugins.form.refresh_nonce ' )) {
450+ $ timeout = (int )$ this ->config ->get ('system.session.timeout ' , 1800 );
451+ // Nonce lifetime is ~12h (current + previous tick); cap refresh window to that.
452+ $ effectiveTimeout = min ($ timeout , 43200 );
453+ // Refresh close to expiry: 10% lead time, capped between 5s and 60s.
454+ $ leadTime = min (60 , max (5 , (int )round ($ effectiveTimeout * 0.10 )));
455+ $ intervalSeconds = max (1 , $ effectiveTimeout - $ leadTime );
456+ $ interval = $ intervalSeconds * 1000 ;
457+
458+ $ this ->grav ['assets ' ]->addInlineJs ("window.GravForm = window.GravForm || {}; window.GravForm.refresh_nonce_interval = $ interval; " , ['group ' => 'bottom ' , 'position ' => 'before ' ]);
459+ $ this ->grav ['assets ' ]->addJs ('plugin://form/assets/form-nonce-refresh.js ' , ['group ' => 'bottom ' , 'defer ' => true ]);
460+ }
444461 $ twig ->twig_vars ['form_max_filesize ' ] = Form::getMaxFilesize ();
445462 $ twig ->twig_vars ['form_json_response ' ] = $ this ->json_response ;
446463 }
0 commit comments