Flavonto Forms documentation, version 1.0.0

Developers

Recipes

Verify webhook signatures, send entries to Google Sheets, connect Zoho CRM, add merge tags, validation and integrations.

Verify a webhook signature

With a Signing secret set on a webhook, each request carries X-Flvf-Signature: t=<unix time>,v1=<hex HMAC-SHA256 of "<time>.<raw body>">. Check it before trusting the data.

PHP:

function flvf_signature_ok( $raw_body, $header, $secret, $tolerance = 300 ) {
	if ( ! preg_match( '/t=(\d+),v1=([a-f0-9]{64})/', (string) $header, $m ) ) {
		return false;
	}
	if ( abs( time() - (int) $m[1] ) > $tolerance ) {
		return false;
	}
	return hash_equals( hash_hmac( 'sha256', $m[1] . '.' . $raw_body, $secret ), $m[2] );
}

Node.js:

const crypto = require( 'crypto' );
function signatureOk( rawBody, header, secret, tolerance = 300 ) {
	const m = /t=(\d+),v1=([a-f0-9]{64})/.exec( header || '' );
	if ( ! m || Math.abs( Date.now() / 1000 - Number( m[ 1 ] ) ) > tolerance ) return false;
	const expected = crypto.createHmac( 'sha256', secret ).update( m[ 1 ] + '.' + rawBody ).digest( 'hex' );
	return crypto.timingSafeEqual( Buffer.from( expected ), Buffer.from( m[ 2 ] ) );
}

Send entries to Google Sheets

  1. In your spreadsheet, open Extensions, Apps Script, and replace the code with:
function doPost( e ) {
	var data = JSON.parse( e.postData.contents );
	var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[ 0 ];
	var entry = data._entry || {};
	delete data._entry;
	var keys = Object.keys( data );
	if ( sheet.getLastRow() === 0 ) {
		sheet.appendRow( [ 'Reference', 'Date' ].concat( keys ) );
	}
	var row = [ entry.reference || '', entry.created || '' ].concat( keys.map( function ( k ) {
		var v = data[ k ];
		return Array.isArray( v ) ? v.map( function ( x ) { return x.url || x; } ).join( ', ' ) : v;
	} ) );
	sheet.appendRow( row );
	return ContentService.createTextOutput( 'ok' );
}
  1. Select Deploy, New deployment, type Web app, execute as Me, access Anyone, and copy the web app address.
  2. In the form, add a Webhook integration with the Google Sheets (Apps Script) preset and paste the address.
  3. Send a test entry. The first entry writes the header row from the field names.

The header row is fixed after the first entry; if you add fields later, add their columns to the sheet by hand.

Connect Zoho CRM

Zoho uses OAuth. The plugin uses a "self client", so no redirect setup is needed.

  1. Open the Zoho API Console for your data centre (for example api-console.zoho.com or api-console.zoho.eu) and create a Self Client.
  2. In Generate Code, enter the scope ZohoCRM.modules.leads.CREATE,ZohoCRM.modules.contacts.CREATE,ZohoCRM.modules.leads.UPDATE,ZohoCRM.modules.contacts.UPDATE,ZohoCRM.settings.fields.READ,ZohoCRM.org.READ, choose a duration and create the code.
  3. Exchange the code for a refresh token (the console shows the request; or use a tool such as curl with your client id and secret against https://accounts.zoho.<dc>/oauth/v2/token with grant_type=authorization_code).
  4. In Forms, Settings, Integrations, enter the client id, client secret, refresh token and data centre, and select Test the connection.
  5. In the form, add Zoho CRM, choose lead or contact, and map fields to Zoho API names such as Last_Name, Email, Company.

Add a merge tag

add_filter( 'flvf_merge_tag', function ( $value, $ns, $arg, $mod, $data ) {
	if ( 'shop' === $ns && 'points' === $arg && ! empty( $data['entry']['user_id'] ) ) {
		return (string) get_user_meta( (int) $data['entry']['user_id'], 'loyalty_points', true );
	}
	return $value;
}, 10, 5 );
// Use {shop:points} in emails and confirmations.

Validate an answer your own way

Return true when the answer is valid, or a message.

add_filter( 'flvf_validate_field', function ( $result, $value, $field, $ctx ) {
	if ( true === $result && 'vat_number' === $field['name'] && '' !== $value && ! preg_match( '/^GB\d{9}$/', $value ) ) {
		return 'Enter a UK VAT number such as GB123456789.';
	}
	return $result;
}, 10, 4 );

Errors that depend on several answers go in flvf_validate_form, which receives the errors keyed by field id and can add more. The third argument holds the cleaned answers by field id:

add_filter( 'flvf_validate_form', function ( $errors, $doc, $answers ) {
	$v = $answers->values;
	if ( isset( $v['f_open01'], $v['f_high01'] ) && (float) $v['f_open01'] > (float) $v['f_high01'] ) {
		$errors['f_high01'] = 'The highest bid must be at least the opening bid.';
	}
	return $errors;
}, 10, 3 );

Fill choices from your own data

add_filter( 'flvf_field_options', function ( $options, $field, $args ) {
	if ( 'branch' !== $field['name'] ) {
		return $options;
	}
	$out = array();
	foreach ( my_branches() as $b ) {
		$out[] = array( 'label' => $b->name, 'value' => $b->code );
	}
	return $out;
}, 10, 3 );

The server checks submitted values against the same list, so visitors cannot send a value you did not offer.

Add your own integration

class My_CRM_Adapter extends \Flavonto\Forms\Integrations\Adapter {
	public function id() {
		return 'my_crm';
	}
	public function label() {
		return 'My CRM';
	}
	public function settings() {
		return array(
			$this->s( 'email', 'template', 'Email address', '{field:email}' ),
			$this->s( 'note', 'textarea', 'Note', '' ),
		);
	}
	public function run( $entry, $config, $data, $extra = array() ) {
		$res = $this->http( 'POST', 'https://crm.example.com/api/leads', array( 'Authorization' => 'Bearer ' . MY_CRM_TOKEN ), array(
			'email' => $this->tpl( $config['email'], $data ),
			'note'  => $this->tpl( $config['note'], $data ),
		) );
		return $this->classify( $res );
	}
}
add_filter( 'flvf_integrations', function ( $classes ) {
	$classes['my_crm'] = 'My_CRM_Adapter';
	return $classes;
} );

http() uses the same address checks as webhooks (no private network addresses, limited redirects, size limits) and classify() turns the reply into done, retry (timeouts, 408, 429 and 5xx) or failed. The integration then appears in every form's Integrations tab with logic, retries and timeline entries.

Only allow redirects to your own site

add_filter( 'flvf_allow_external_redirect', '__return_false' );