@@ -154,7 +154,7 @@ private function renderField( array $field, string $idSuffix ): string
154154 {
155155 return '<div class="form-check mb-3"> '
156156 . '<input type="checkbox" class="form-check-input" id=" ' . $ this ->esc ( $ id ) . '" name=" ' . $ this ->esc ( $ name ) . '" value="1" ' . $ reqAttr . '> '
157- . '<label class="form-check-label" for=" ' . $ this ->esc ( $ id ) . '"> ' . $ this ->esc ( $ label ) . $ reqMark . '</label> '
157+ . '<label class="form-check-label" for=" ' . $ this ->esc ( $ id ) . '"> ' . $ this ->labelWithLink ( $ label, $ field ) . $ reqMark . '</label> '
158158 . '</div> ' ;
159159 }
160160
@@ -169,7 +169,7 @@ private function renderField( array $field, string $idSuffix ): string
169169 }
170170
171171 $ control = '<div class="mb-3"> ' ;
172- $ control .= '<label class="form-label" for=" ' . $ this ->esc ( $ id ) . '"> ' . $ this ->esc ( $ label ) . $ reqMark . '</label> ' ;
172+ $ control .= '<label class="form-label" for=" ' . $ this ->esc ( $ id ) . '"> ' . $ this ->labelWithLink ( $ label, $ field ) . $ reqMark . '</label> ' ;
173173
174174 switch ( $ type )
175175 {
@@ -227,7 +227,7 @@ private function renderCheckboxes( array $field, string $idSuffix, string $name,
227227 $ groups = FieldOptions::groups ( $ field );
228228
229229 $ html = '<fieldset class="mb-3 contact-checkboxes"> ' ;
230- $ html .= '<legend class="form-label fs-6"> ' . $ this ->esc ( $ label ) . $ reqMark . '</legend> ' ;
230+ $ html .= '<legend class="form-label fs-6"> ' . $ this ->labelWithLink ( $ label, $ field ) . $ reqMark . '</legend> ' ;
231231
232232 foreach ( $ groups as $ group )
233233 {
@@ -274,7 +274,7 @@ private function renderRadios( array $field, string $idSuffix, string $name, str
274274 $ reqAttr = $ required ? ' required ' : '' ;
275275
276276 $ html = '<fieldset class="mb-3 contact-radios"> ' ;
277- $ html .= '<legend class="form-label fs-6"> ' . $ this ->esc ( $ label ) . $ reqMark . '</legend> ' ;
277+ $ html .= '<legend class="form-label fs-6"> ' . $ this ->labelWithLink ( $ label, $ field ) . $ reqMark . '</legend> ' ;
278278
279279 foreach ( FieldOptions::groups ( $ field ) as $ group )
280280 {
@@ -382,6 +382,43 @@ private function session(): ?SessionManager
382382 return $ this ->_sessionManager ;
383383 }
384384
385+ /**
386+ * Build a field's (escaped) label, optionally embedding a safe hyperlink.
387+ *
388+ * A field may declare a link via config:
389+ *
390+ * link: { text: "Volunteer Agreement", url: "/pages/release-form" }
391+ *
392+ * Use the literal token "{link}" in the label to control placement; if the
393+ * token is absent the link is appended. Label text, link text and URL are
394+ * all escaped, so only this controlled anchor is ever injected as HTML.
395+ *
396+ * @param string $label
397+ * @param array $field
398+ * @return string
399+ */
400+ private function labelWithLink ( string $ label , array $ field ): string
401+ {
402+ $ escaped = $ this ->esc ( $ label );
403+ $ link = $ field ['link ' ] ?? null ;
404+
405+ if ( !is_array ( $ link ) || empty ( $ link ['url ' ] ) )
406+ {
407+ return $ escaped ;
408+ }
409+
410+ $ url = $ this ->esc ( (string ) $ link ['url ' ] );
411+ $ text = $ this ->esc ( (string ) ( $ link ['text ' ] ?? $ link ['url ' ] ) );
412+ $ anchor = '<a href=" ' . $ url . '" target="_blank" rel="noopener noreferrer"> ' . $ text . '</a> ' ;
413+
414+ if ( str_contains ( $ escaped , '{link} ' ) )
415+ {
416+ return str_replace ( '{link} ' , $ anchor , $ escaped );
417+ }
418+
419+ return $ escaped . ' ' . $ anchor ;
420+ }
421+
385422 /**
386423 * HTML-escape helper.
387424 *
0 commit comments