{"id":273423,"date":"2026-02-18T17:10:41","date_gmt":"2026-02-18T17:10:41","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/tofu-template-oriented-form-utilities\/"},"modified":"2026-09-17T09:59:13","modified_gmt":"2026-09-17T09:59:13","slug":"template-oriented-form-utilities","status":"publish","type":"plugin","link":"https:\/\/hat.wordpress.org\/plugins\/template-oriented-form-utilities\/","author":23433906,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.1.0","stable_tag":"0.1.0","tested":"7.1.1","requires":"","requires_php":"8.1","requires_plugins":null,"header_name":"TOFU (Template-Oriented Form Utilities)","header_author":"lionheartgroup","header_description":"Template-Oriented Form Utilities is a WordPress plugin that provides a set of utilities for handling forms in a template-oriented manner.","assets_banners_color":"b8bdad","last_updated":"2026-09-17 09:59:13","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/lionheart-group.github.io\/template-oriented-form-utilities\/","header_author_uri":"https:\/\/www.lionheart.co.jp\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":422,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","changelog"],"tags":{"0.0.1":{"tag":"0.0.1","author":"lionheartgroup","date":"2026-02-18 17:10:10","revision":3464549},"0.0.2":{"tag":"0.0.2","author":"lionheartgroup","date":"2026-02-19 02:46:47","revision":3464706},"0.1.0":{"tag":"0.1.0","author":"lionheartgroup","date":"2026-09-17 09:59:13","revision":3700005}},"upgrade_notice":{"0.1.0":"<p><strong>The plugin&#039;s own hidden form fields have moved to a <code>__tofu_<\/code> prefix, and the field\nnames a form declares may no longer start with <code>_tofu_<\/code> or <code>__tofu_<\/code>.<\/strong><\/p>\n\n<ol>\n<li><p><strong>Hidden field names renamed.<\/strong> <code>_tofu_{key}_nonce<\/code> \u2192 <code>__tofu_{key}_nonce<\/code>,\n   _tofu_recaptcha_token \u2192 <code>__tofu_recaptcha_token<\/code>, <code>_tofu_turnstile_token<\/code> \u2192\n   __tofu_turnstile_token. All of the plugin&#039;s form fields now share one prefix, which\nthe plugin also reserves, so a form&#039;s own field can never silently overwrite one.<\/p>\n\n<ul>\n<li><strong>Themes using <code>Form::formOpen()<\/code>\/<code>formClose()<\/code> and the <code>Form::<\/code> helpers need no\nchanges<\/strong> \u2014 the names are generated for you.<\/li>\n<li><strong>AJAX\/headless clients that hardcode these names must be updated.<\/strong> Clients reading\n field_name and <code>token_field_name<\/code> from <code>GET \/wp-json\/tofu\/v1\/forms\/{key}\/nonce<\/code>\nalready follow automatically. Note the reCAPTCHA\/Turnstile <em>error<\/em> keys in a\n {&quot;success&quot;:false,&quot;errors&quot;:{\u2026}} response change with the field names.<\/li>\n<li>Nothing outside form fields changed: the <code>_tofu_key<\/code> query parameter, the\n _tofu_session_key cookie and the <code>_tofu_form_{key}<\/code> element ID are untouched, as\nthey cannot collide with a form&#039;s field names.<\/li>\n<li>A visitor who loaded a form page before updating can still submit it: the old field\nnames are accepted as a fallback for one release.<\/li>\n<\/ul><\/li>\n<li><p><strong>Field names starting with <code>_tofu_<\/code> or <code>__tofu_<\/code> are now rejected at registration.<\/strong>\n   FormConfig throws an <code>InvalidArgumentException<\/code> naming the offending field if\n   allows, <code>rules<\/code>, <code>names<\/code>, <code>messages<\/code> or <code>records<\/code> declares one. Such a field never\nworked \u2014 PHP keeps only the last value for a repeated name, and the plugin&#039;s input is\nrendered last, so the form&#039;s own value was silently dropped. Rename the field.<\/p><\/li>\n<li><p><strong>Submission nonces are now bound to the form.<\/strong> The redirect flow minted its nonce\nagainst a bare <code>input<\/code>\/<code>confirm<\/code> action, so a nonce issued for one form verified\nagainst any other; the form key is now part of the action, as it already was for the\nREST flow. No configuration change is needed.<\/p><\/li>\n<\/ol>","0.0.7":"<p><strong>The validation library has been replaced with an in-house engine. No rule name was\nremoved, so existing <code>rules:<\/code> configuration keeps working unchanged.<\/strong><\/p>\n\n<ol>\n<li><p><strong>English validation messages have been rewritten.<\/strong> They previously came from the\nbundled library; they are now TOFU&#039;s own text, in one consistent voice. Verdicts are\nunaffected \u2014 a submission that passed before still passes \u2014 but any English wording\nyour site displays, or that your tests assert on, will differ. Japanese messages have\nbeen rewritten to match.<\/p>\n\n<ul>\n<li><code>required_file<\/code> returns as the name for the required-file rule.\n custom_required_file, its name since 0.0.3, still works and behaves identically.<\/li>\n<\/ul><\/li>\n<li><p><strong>Translations now load properly.<\/strong> The plugin never called\n   load_plugin_textdomain(), so its Japanese <code>.mo<\/code> was almost certainly never used \u2014\nvalidation, reCAPTCHA and Turnstile messages rendered in English on Japanese sites.\nThat call has been added, so those strings appear translated for the first time.<\/p>\n\n<ul>\n<li>German, French, Turkish and Chinese messages, which the old library bundled, now\nfall back to English until a <code>.po<\/code> is contributed for them.<\/li>\n<\/ul><\/li>\n<li><p><strong>Behaviour fixes that may change what a form accepts:<\/strong><\/p>\n\n<ul>\n<li>A full-width space (U+3000) now counts as blank. It is what a Japanese IME emits\nfor the space bar in full-width mode, and it previously passed <code>required<\/code> \u2014 a field\nthe visitor believes is empty could satisfy a required check.<\/li>\n<li><code>required<\/code> now understands file fields. It previously accepted a file input that\nthe visitor left empty, because the <code>$_FILES<\/code> entry is a non-empty array.<\/li>\n<li>Invalid input no longer raises a fatal error. <code>after<\/code>, <code>before<\/code>, <code>extension<\/code> and\n uuid could return a 500 for ordinary input \u2014 a blank date field was enough. They\nnow fail validation and show a message.<\/li>\n<li><code>uploaded_file<\/code>, <code>mimes<\/code> and <code>extension<\/code> work at all now; they previously depended\non a check that is never true in this plugin&#039;s request flow.<\/li>\n<li>A file carried over to the confirm page is verified against the server&#039;s own\nsession record, so a tampered form can no longer claim an upload that is not there.<\/li>\n<\/ul><\/li>\n<\/ol>","0.0.3":"<p><strong>Breaking changes from v0.0.2:<\/strong><\/p>\n\n<ol>\n<li><p><strong>Validation rules renamed<\/strong> (somnambulist\/validation replaces wixel\/gump):<\/p>\n\n<ul>\n<li><code>required_file<\/code> \u2192 <code>custom_required_file<\/code><\/li>\n<li><code>max_len<\/code> \u2192 <code>max<\/code><\/li>\n<li><code>min_len<\/code> \u2192 <code>min<\/code><\/li>\n<li><code>valid_email<\/code> \u2192 <code>email<\/code><\/li>\n<li><code>numeric<\/code> stays the same<\/li>\n<li><code>filters<\/code> parameter has been removed from <code>ValidationConfig<\/code> \u2014 sanitize input in the <code>after<\/code> hook if needed.<\/li>\n<li>A custom validation rule can also be registered site-wide via the\n tofu_register_validation_rules action; see docs\/hooks\/index.md.<\/li>\n<\/ul><\/li>\n<li><p><strong>reCAPTCHA \/ Turnstile configuration moved to plugin level:<\/strong><\/p>\n\n<ul>\n<li>Remove <code>recaptcha: new ReCAPTCHAConfig(...)<\/code> and <code>turnstile: new TurnstileConfig(...)<\/code> from <code>FormConfig<\/code>.<\/li>\n<li>Call <code>Form::setRecaptcha(new ReCAPTCHAConfig(...))<\/code> once before registering forms.<\/li>\n<li>Call <code>Form::setTurnstile(new TurnstileConfig(...))<\/code> once before registering forms.<\/li>\n<li>Replace with <code>recaptchaEnabled: true<\/code> or <code>turnstileEnabled: true<\/code> in each <code>FormConfig<\/code>.<\/li>\n<\/ul><\/li>\n<li><p><strong>Confirm step requires explicit opt-in:<\/strong><\/p>\n\n<ul>\n<li><code>confirmStep: true<\/code> must now be set explicitly in <code>FormConfig<\/code> to enable the confirm step.<\/li>\n<li>For the traditional redirect flow, also set <code>template-&amp;gt;confirmPath<\/code>.<\/li>\n<li><code>Form::embedScript()<\/code> is no longer needed on the confirm page template.<\/li>\n<\/ul><\/li>\n<\/ol>"},"ratings":[],"assets_icons":{"icon-128x128.jpg":{"filename":"icon-128x128.jpg","revision":3464552,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.jpg":{"filename":"icon-256x256.jpg","revision":3464552,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.jpg":{"filename":"banner-1544x500.jpg","revision":3464552,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.jpg":{"filename":"banner-772x250.jpg","revision":3464552,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.0.1","0.0.2","0.1.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[601,256133,4351],"plugin_category":[42],"plugin_contributors":[256134],"plugin_business_model":[],"class_list":["post-273423","plugin","type-plugin","status-publish","hentry","plugin_tags-forms","plugin_tags-template-oriented","plugin_tags-utilities","plugin_category-contact-forms","plugin_contributors-lionheartgroup","plugin_committers-lionheartgroup"],"banners":{"banner":"https:\/\/ps.w.org\/template-oriented-form-utilities\/assets\/banner-772x250.jpg?rev=3464552","banner_2x":"https:\/\/ps.w.org\/template-oriented-form-utilities\/assets\/banner-1544x500.jpg?rev=3464552","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/template-oriented-form-utilities\/assets\/icon-128x128.jpg?rev=3464552","icon_2x":"https:\/\/ps.w.org\/template-oriented-form-utilities\/assets\/icon-256x256.jpg?rev=3464552","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Template-Oriented Form Utilities (TOFU) is designed to streamline the process of creating and managing forms within WordPress themes and plugins. By adopting a template-oriented approach, TOFU allows developers to define form structures and behaviors using reusable templates, making it easier to manage\/maintain with team collaboration through version control systems.<\/p>\n\n<p>GitHub and documentation for this plugin can be found at:<\/p>\n\n<p><a href=\"https:\/\/github.com\/lionheart-group\/template-oriented-form-utilities\">https:\/\/github.com\/lionheart-group\/template-oriented-form-utilities<\/a><\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin relies on third-party services to protect your website from spam and automated attacks. Depending on your configuration, this plugin connects to the following services:<\/p>\n\n<h4>Google reCAPTCHA<\/h4>\n\n<ul>\n<li><strong>Purpose:<\/strong> Protecting forms from spam and bot abuse.<\/li>\n<li><strong>When data is sent:<\/strong> When a page containing a reCAPTCHA-protected form is loaded or submitted.<\/li>\n<li><strong>Data sent:<\/strong> IP address, mouse movements, browser\/device information, and duration of stay.<\/li>\n<li><strong>Service Provider:<\/strong> Google LLC.<\/li>\n<li><strong>Links:<\/strong> <a href=\"https:\/\/policies.google.com\/privacy\">Google Privacy Policy<\/a>, <a href=\"https:\/\/policies.google.com\/terms\">Google Terms of Service<\/a>.<\/li>\n<\/ul>\n\n<h4>Cloudflare Turnstile<\/h4>\n\n<ul>\n<li><strong>Purpose:<\/strong> Privacy-focused alternative for bot protection and spam prevention.<\/li>\n<li><strong>When data is sent:<\/strong> When a user interacts with a form protected by Turnstile.<\/li>\n<li><strong>Data sent:<\/strong> Browser\/device characteristics and interaction data (privacy-friendly, does not use cookies for tracking).<\/li>\n<li><strong>Service Provider:<\/strong> Cloudflare, Inc.<\/li>\n<li><strong>Links:<\/strong> <a href=\"https:\/\/www.cloudflare.com\/privacypolicy\/\">Cloudflare Privacy Policy<\/a>, <a href=\"https:\/\/www.cloudflare.com\/website-terms\/\">Cloudflare Website Terms<\/a>.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>From the WP admin panel, click \"Plugins\" -&gt; \"Add new\".<\/li>\n<li>In the browser input box, type \"Template-Oriented Form Utilities\".<\/li>\n<li>Select the \"Template-Oriented Form Utilities\" plugin and click \"Install\".<\/li>\n<li>Activate the plugin.<\/li>\n<\/ol>\n\n<p>OR\u2026<\/p>\n\n<ol>\n<li>Download the plugin from this page.<\/li>\n<li>Save the .zip file to a location on your computer.<\/li>\n<li>Open the WP admin panel, and click \"Plugins\" -&gt; \"Add new\".<\/li>\n<li>Click \"upload\".. then browse to the .zip file downloaded from this page.<\/li>\n<li>Click \"Install\".. and then \"Activate plugin\".<\/li>\n<\/ol>\n\n<!--section=changelog-->\n<ul>\n<li>v0.0.1 - Initial release.<\/li>\n<li>v0.0.2 - Arranged required PHP version to 8.1, added external services section to the readme.<\/li>\n<li>v0.0.3\n\n<ul>\n<li>Implemented Ajax form submission and validation with reCAPTCHA and Turnstile support.<\/li>\n<li>Fixed recaptcha issue when embedded multiple forms on the same page.<\/li>\n<li>Replaced validation library from wixel\/gump to somnambulist\/validation.<\/li>\n<\/ul><\/li>\n<li>v0.0.4\n\n<ul>\n<li>Added optional database recording of form submissions (<code>saveToDatabase<\/code>), encrypted at\nrest, with an admin screen to view recorded data.<\/li>\n<li>Added <code>Form::setTemplate()<\/code> to override a form's input\/confirm\/result URLs per visitor\nsession, so a single registered form can be embedded on multiple pages (e.g. post\ntemplates); added <code>FormConfig::$dynamicTemplate<\/code> for forms configured this way.<\/li>\n<\/ul><\/li>\n<li>v0.0.5\n\n<ul>\n<li>The AJAX nonce endpoint (<code>GET \/wp-json\/tofu\/v1\/forms\/{key}\/nonce<\/code>) now also returns the\nreCAPTCHA\/Turnstile site key when enabled, so cross-origin\/headless clients no longer need\nto hardcode it separately from the server-side configuration.<\/li>\n<\/ul><\/li>\n<li>v0.0.6\n\n<ul>\n<li>Fixed: AJAX form submissions with a field literally named <code>key<\/code> could fail with a\n\"form not found\" error, because the REST handlers read the form key from the merged\nrequest parameters instead of strictly from the URL route.<\/li>\n<\/ul><\/li>\n<li>v0.0.7\n\n<ul>\n<li>Replaced the bundled validation library with an in-house engine. The plugin now has no\nruntime dependencies at all. Every rule name still resolves, so existing <code>rules:<\/code>\nconfiguration keeps working \u2014 see the upgrade notice for the behavioural differences.<\/li>\n<li>Added: <code>required_file<\/code> as the name of the required-file rule. <code>custom_required_file<\/code>,\nits name since 0.0.3, still works and runs the same code.<\/li>\n<li>Fixed: a full-width space (U+3000) no longer satisfies <code>required<\/code>. It is what a Japanese\nIME emits for the space bar, so a field the visitor sees as empty could pass.<\/li>\n<li>Fixed: <code>required<\/code> now recognises an empty file input as empty.<\/li>\n<li>Fixed: <code>after<\/code>, <code>before<\/code>, <code>extension<\/code> and <code>uuid<\/code> no longer raise a fatal error on\nordinary input \u2014 a blank date field was enough to return a 500.<\/li>\n<li>Fixed: <code>uploaded_file<\/code>, <code>mimes<\/code> and <code>extension<\/code> now work. They depended on a check that\nis never true in this plugin's request flow.<\/li>\n<li>Fixed: a file carried over to the confirm page is verified against the server's own\nsession record, so a tampered form cannot claim an upload that is not there.<\/li>\n<li>Fixed: the session cookie is issued only when a session is actually saved. It was\npreviously sent on every request, including pages with no form and the admin screens,\nwhich is enough to stop most full-page caches serving anything cached.<\/li>\n<li>Fixed: the plugin never called <code>load_plugin_textdomain()<\/code>, so its bundled Japanese\ntranslations were not loaded and validation, reCAPTCHA and Turnstile messages rendered\nin English on Japanese sites.<\/li>\n<li>Fixed: \"reCAPTCHA token is missing.\" and \"Turnstile token is missing.\" were the only\nbot-protection messages not passed through <code>__()<\/code>, so they stayed English even on a\ntranslated site.<\/li>\n<li>Fixed: none of the recorded-submissions admin screen's text had been extracted for\ntranslation since it was added in 0.0.4, so translators never received it. It is in the\n.pot now, with Japanese supplied.<\/li>\n<li>Tested against WordPress 7.1.<\/li>\n<\/ul><\/li>\n<li>v0.1.0\n\n<ul>\n<li>Added: seven actions and filters, so code outside a form's own configuration can react to\nsubmissions and extend validation \u2014 <code>tofu_form_submitted<\/code>,\n  tofu_register_validation_rules, <code>tofu_pre_send_mail<\/code>, <code>tofu_validation_failed<\/code>,\n  tofu_redirect_url, <code>tofu_record_values<\/code> and <code>tofu_admin_page_capability<\/code>. They fire for\nthe redirect and AJAX flows alike. See docs\/hooks\/index.md.<\/li>\n<li>Added: a named custom validation rule can now be registered site-wide, via the\n  tofu_register_validation_rules action. The validator factory was previously unreachable,\nso this was not possible without editing the plugin.<\/li>\n<li>Fixed: <code>Form::setTemplate()<\/code> no longer writes to the session, and so no longer sends a\n  Set-Cookie, when a page is merely rendered. A theme calling it for each of its registered\nforms on every page load was issuing one cookie per form on every response \u2014 including\npages with no form on them \u2014 which stops server and CDN page caches serving anything\ncached. The override is now carried to the following POST in a hidden field and persisted\nonly when the visitor actually submits.<\/li>\n<li>Fixed: at most one session cookie is issued per response. A single response can save the\nsession more than once, and each save sent its own redundant <code>Set-Cookie<\/code>.<\/li>\n<li>Fixed: database migrations now run when the plugin is updated. The plugin listened for\n  upgrade_process_complete, which WordPress does not define \u2014 the hook is\n  upgrader_process_complete \u2014 so migrations had only ever run on activation. A site that\ninstalled before a migration was added and updated since never received it; updating to\nthis version applies anything outstanding. Migrations are tracked in <code>wp_tofu_migrate<\/code> and\nalready-applied ones are skipped, so there is nothing to do by hand.<\/li>\n<li>Fixed: a submission nonce is now bound to the form it was issued for. The redirect flow\nminted its nonce against a bare <code>input<\/code>\/<code>confirm<\/code> action, so a nonce issued for one form\nverified against every other one; only the field name distinguished them, and a field name\nis a label the sender controls. The REST flow already did this.<\/li>\n<li>Changed: the plugin's own hidden field names now all use a <code>__tofu_<\/code> prefix, and a form may\nno longer declare a field name starting with <code>_tofu_<\/code> or <code>__tofu_<\/code> \u2014 <code>FormConfig<\/code> throws at\nregistration instead of letting the collision silently drop the form's value. Breaking for\nAJAX\/headless clients that hardcode those names; see the upgrade notice.<\/li>\n<\/ul><\/li>\n<\/ul>","raw_excerpt":"Template-Oriented Form Utilities is a WordPress plugin that simplifies form creation, validation, and processing via template-based data management.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/273423","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=273423"}],"author":[{"embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/lionheartgroup"}],"wp:attachment":[{"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=273423"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=273423"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=273423"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=273423"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=273423"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/hat.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=273423"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}