__( 'Knack', 'advanced-form-integration' ),
'tasks' => array(
'create_record' => __( 'Create Record', 'advanced-form-integration' ),
),
);
return $actions;
}
add_filter( 'adfoin_settings_tabs', 'adfoin_knack_settings_tab', 10, 1 );
function adfoin_knack_settings_tab( $providers ) {
$providers['knack'] = __( 'Knack', 'advanced-form-integration' );
return $providers;
}
add_action( 'adfoin_settings_view', 'adfoin_knack_settings_view', 10, 1 );
function adfoin_knack_settings_view( $current_tab ) {
if ( 'knack' !== $current_tab ) {
return;
}
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
$fields = array(
array(
'name' => 'appId',
'label' => __( 'App ID', 'advanced-form-integration' ),
'type' => 'text',
'required' => true,
'show_in_table' => true,
),
array(
'name' => 'apiKey',
'label' => __( 'REST API Key', 'advanced-form-integration' ),
'type' => 'text',
'required' => true,
'mask' => true,
'show_in_table' => false,
),
);
$instructions = sprintf(
'
- %s
- %s
- %s
',
esc_html__( 'In Knack, open Settings → API & Code and copy the App ID.', 'advanced-form-integration' ),
esc_html__( 'Click "API Keys" and create (or reuse) a REST API key.', 'advanced-form-integration' ),
esc_html__( 'Paste both below. AFI calls https://api.knack.com/v1/ with these headers.', 'advanced-form-integration' )
);
ADFOIN_Account_Manager::render_settings_view( 'knack', __( 'Knack', 'advanced-form-integration' ), $fields, $instructions );
}
add_action( 'wp_ajax_adfoin_get_knack_credentials', 'adfoin_get_knack_credentials', 10, 0 );
function adfoin_get_knack_credentials() {
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
ADFOIN_Account_Manager::ajax_get_credentials_list( 'knack' );
}
add_action( 'wp_ajax_adfoin_save_knack_credentials', 'adfoin_save_knack_credentials', 10, 0 );
function adfoin_save_knack_credentials() {
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
ADFOIN_Account_Manager::ajax_save_credentials( 'knack', array( 'appId', 'apiKey' ) );
}
function adfoin_knack_credentials_list() {
foreach ( adfoin_read_credentials( 'knack' ) as $option ) {
printf( '', esc_attr( $option['id'] ), esc_html( $option['title'] ) );
}
}
add_action( 'adfoin_action_fields', 'adfoin_knack_action_fields' );
function adfoin_knack_action_fields() {
?>
__( 'No Knack account selected.', 'advanced-form-integration' ) ) );
}
$credentials = adfoin_get_credentials_by_id( 'knack', $cred_id );
$app_id = isset( $credentials['appId'] ) ? trim( (string) $credentials['appId'] ) : '';
if ( ! $app_id ) {
wp_send_json_error( array( 'message' => __( 'Knack App ID missing.', 'advanced-form-integration' ) ) );
}
$response = adfoin_knack_request( 'applications/' . rawurlencode( $app_id ), 'GET', array(), array(), $cred_id );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$objects = adfoin_knack_extract_objects( $body );
wp_send_json_success( $objects );
}
add_action( 'wp_ajax_adfoin_get_knack_fields', 'adfoin_get_knack_fields', 10, 0 );
function adfoin_get_knack_fields() {
adfoin_verify_nonce();
$cred_id = isset( $_POST['credId'] ) ? sanitize_text_field( wp_unslash( $_POST['credId'] ) ) : '';
$object_key = isset( $_POST['objectKey'] ) ? sanitize_text_field( wp_unslash( $_POST['objectKey'] ) ) : '';
if ( ! $cred_id || ! $object_key ) {
wp_send_json_error( array( 'message' => __( 'Knack account and object are required.', 'advanced-form-integration' ) ) );
}
$credentials = adfoin_get_credentials_by_id( 'knack', $cred_id );
$app_id = isset( $credentials['appId'] ) ? trim( (string) $credentials['appId'] ) : '';
if ( ! $app_id ) {
wp_send_json_error( array( 'message' => __( 'Knack App ID missing.', 'advanced-form-integration' ) ) );
}
$response = adfoin_knack_request( 'applications/' . rawurlencode( $app_id ), 'GET', array(), array(), $cred_id );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$objects = adfoin_knack_extract_objects( $body, true );
$fields = array();
foreach ( $objects as $object ) {
if ( ( $object['key'] ?? '' ) !== $object_key ) {
continue;
}
foreach ( $object['fields'] as $field ) {
$key = $field['key'] ?? '';
$name = $field['name'] ?? $key;
if ( '' === $key ) {
continue;
}
$fields[] = array( 'key' => $key, 'value' => $name );
}
break;
}
wp_send_json_success( $fields );
}
if ( ! function_exists( 'adfoin_knack_extract_objects' ) ) :
/**
* Pull the objects array out of a /v1/applications/{id} payload.
*
* @param array $body
* @param bool $with_fields When true, include the fields array on each object.
*
* @return array
*/
function adfoin_knack_extract_objects( $body, $with_fields = false ) {
$raw = array();
if ( isset( $body['application']['objects'] ) && is_array( $body['application']['objects'] ) ) {
$raw = $body['application']['objects'];
} elseif ( isset( $body['objects'] ) && is_array( $body['objects'] ) ) {
$raw = $body['objects'];
}
$out = array();
foreach ( $raw as $object ) {
$key = isset( $object['key'] ) ? (string) $object['key'] : '';
$name = isset( $object['name'] ) ? (string) $object['name'] : $key;
if ( '' === $key ) {
continue;
}
$entry = array( 'key' => $key, 'name' => $name );
if ( $with_fields ) {
$entry['fields'] = isset( $object['fields'] ) && is_array( $object['fields'] ) ? $object['fields'] : array();
}
$out[] = $entry;
}
return $out;
}
endif;
if ( ! function_exists( 'adfoin_knack_field_types' ) ) :
/**
* Map of {field_key => Knack field type} for an object, cached 12h per
* credential. Lets send_data format typed fields correctly.
*/
function adfoin_knack_field_types( $object_key, $cred_id ) {
$cache_key = 'adfoin_knack_ftypes_' . md5( (string) $cred_id . '|' . (string) $object_key );
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) {
return $cached;
}
$types = array();
$response = adfoin_knack_request( 'objects/' . rawurlencode( $object_key ) . '/fields', 'GET', array(), array(), $cred_id );
if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $body['fields'] ) && is_array( $body['fields'] ) ) {
foreach ( $body['fields'] as $field ) {
if ( isset( $field['key'], $field['type'] ) ) {
$types[ $field['key'] ] = $field['type'];
}
}
}
}
set_transient( $cache_key, $types, 12 * HOUR_IN_SECONDS );
return $types;
}
endif;
if ( ! function_exists( 'adfoin_knack_format_payload' ) ) :
/**
* Coerce flat string values into the structured shapes Knack requires for
* typed fields. The critical case is `name`: Knack stores it as {first,last},
* and a plain string is silently dropped (stored empty). `email` is wrapped as
* {email} (canonical; Knack accepts a string too). Values that are already
* arrays (e.g. from the Pro "Record JSON" merge) are left untouched.
*/
function adfoin_knack_format_payload( $payload, $object_key, $cred_id ) {
if ( empty( $payload ) || ! is_array( $payload ) ) {
return $payload;
}
$types = adfoin_knack_field_types( $object_key, $cred_id );
if ( empty( $types ) ) {
return $payload;
}
foreach ( $payload as $key => $value ) {
if ( is_array( $value ) ) {
continue; // already structured
}
$type = isset( $types[ $key ] ) ? $types[ $key ] : '';
if ( 'name' === $type ) {
$value = trim( (string) $value );
if ( '' === $value ) {
continue;
}
$parts = preg_split( '/\s+/', $value, 2 );
$payload[ $key ] = array(
'first' => $parts[0],
'last' => isset( $parts[1] ) ? $parts[1] : '',
);
} elseif ( 'email' === $type ) {
$payload[ $key ] = array( 'email' => (string) $value );
}
}
return $payload;
}
endif;
add_action( 'adfoin_knack_job_queue', 'adfoin_knack_job_queue', 10, 1 );
function adfoin_knack_job_queue( $data ) {
adfoin_knack_send_data( $data['record'], $data['posted_data'] );
}
function adfoin_knack_send_data( $record, $posted_data ) {
if ( 'create_record' !== ( $record['task'] ?? '' ) ) {
return;
}
$record_data = json_decode( $record['data'], true );
if ( adfoin_check_conditional_logic( $record_data['action_data']['cl'] ?? array(), $posted_data ) ) {
return;
}
$field_data = isset( $record_data['field_data'] ) ? $record_data['field_data'] : array();
$cred_id = isset( $field_data['credId'] ) ? $field_data['credId'] : '';
$object_key = isset( $field_data['objectKey'] ) ? trim( (string) $field_data['objectKey'] ) : '';
if ( ! $cred_id || ! $object_key ) {
return;
}
$reserved = array( 'credId' => 1, 'objectKey' => 1 );
$payload = array();
foreach ( $field_data as $key => $value ) {
if ( isset( $reserved[ $key ] ) ) {
continue;
}
if ( 0 !== strpos( $key, 'field_' ) ) {
continue;
}
$parsed = adfoin_get_parsed_values( $value, $posted_data );
if ( '' !== $parsed && null !== $parsed ) {
$payload[ $key ] = $parsed;
}
}
$payload = apply_filters( 'adfoin_knack_record_payload', $payload, $field_data, $posted_data );
if ( empty( $payload ) ) {
return;
}
// Format typed fields (name -> {first,last}, email -> {email}) so they're
// not silently dropped by Knack.
$payload = adfoin_knack_format_payload( $payload, $object_key, $cred_id );
adfoin_knack_request( 'objects/' . rawurlencode( $object_key ) . '/records', 'POST', $payload, $record, $cred_id );
}
if ( ! function_exists( 'adfoin_knack_request' ) ) :
/**
* Call the Knack API.
*
* @param string $endpoint Path under /v1/.
* @param string $method HTTP verb.
* @param mixed $data Body (POST/PUT) or query (GET).
* @param array $record Submission record for logging.
* @param string $cred_id Saved credential id.
*
* @return array|WP_Error
*/
function adfoin_knack_request( $endpoint, $method = 'GET', $data = array(), $record = array(), $cred_id = '' ) {
$app_id = '';
$api_key = '';
if ( $cred_id && function_exists( 'adfoin_get_credentials_by_id' ) ) {
$credentials = adfoin_get_credentials_by_id( 'knack', $cred_id );
if ( is_array( $credentials ) ) {
$app_id = isset( $credentials['appId'] ) ? trim( (string) $credentials['appId'] ) : '';
$api_key = isset( $credentials['apiKey'] ) ? trim( (string) $credentials['apiKey'] ) : '';
}
}
if ( ! $app_id || ! $api_key ) {
return new WP_Error( 'knack_missing_credentials', __( 'Knack credentials are not configured.', 'advanced-form-integration' ) );
}
$url = 'https://api.knack.com/v1/' . ltrim( $endpoint, '/' );
$method = strtoupper( $method );
$args = array(
'timeout' => 30,
'method' => $method,
'headers' => array(
'X-Knack-Application-Id' => $app_id,
'X-Knack-REST-API-Key' => $api_key,
'Accept' => 'application/json',
),
);
if ( 'GET' === $method ) {
if ( is_array( $data ) && ! empty( $data ) ) {
$url = add_query_arg( $data, $url );
}
} else {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode( $data );
}
$response = wp_remote_request( $url, $args );
if ( $record ) {
adfoin_add_to_log( $response, $url, $args, $record );
}
return $response;
}
endif;