array( 'name' => __( 'Webhook', '2ranq-forms' ), 'note' => __( 'Sends the entry as JSON to any address you give. This is how you reach Zapier, Make, n8n, or your own system.', '2ranq-forms' ), 'link' => 'https://zapier.com/apps/webhook/integrations', 'link_text' => __( 'How to catch a webhook in Zapier', '2ranq-forms' ), 'steps' => array( __( 'In Zapier, Make or n8n, start a new automation with a webhook trigger.', '2ranq-forms' ), __( 'They give you an address. Paste it into the form, under Settings, Where else the entry goes.', '2ranq-forms' ), __( 'Send the form once. The address then sees your fields and you can map them.', '2ranq-forms' ), ), 'keys' => array(), 'form' => array( 'url' => array( 'label' => __( 'Address to send to', '2ranq-forms' ), 'type' => 'url' ), 'secret' => array( 'label' => __( 'Secret, sent as a header', '2ranq-forms' ), 'type' => 'text', 'help' => __( 'Optional. Arrives as X-Ranq-Secret so the other side can tell it is really you.', '2ranq-forms' ) ), ), ), 'slack' => array( 'name' => __( 'Slack', '2ranq-forms' ), 'note' => __( 'Posts every entry into a channel, so the team sees it without opening the dashboard.', '2ranq-forms' ), 'link' => 'https://api.slack.com/messaging/webhooks', 'link_text' => __( 'How to make a Slack webhook', '2ranq-forms' ), 'steps' => array( __( 'In Slack, open api.slack.com, press Create New App, then From scratch, and pick your workspace.', '2ranq-forms' ), __( 'Open Incoming Webhooks, turn it on, and press Add New Webhook to Workspace.', '2ranq-forms' ), __( 'Choose the channel, then copy the address that starts with hooks.slack.com into the box below.', '2ranq-forms' ), ), 'keys' => array( 'webhook' => array( 'label' => __( 'Incoming webhook address', '2ranq-forms' ), 'type' => 'url' ), ), 'form' => array(), ), 'mailchimp' => array( 'name' => __( 'Mailchimp', '2ranq-forms' ), 'note' => __( 'Adds the person who filled the form to one of your audiences, with tags if you want them.', '2ranq-forms' ), 'link' => 'https://admin.mailchimp.com/account/api/', 'link_text' => __( 'Open API keys in Mailchimp', '2ranq-forms' ), 'steps' => array( __( 'In Mailchimp, open Account and billing, then Extras, then API keys.', '2ranq-forms' ), __( 'Press Create A Key, name it, and copy it. It ends with something like -us21.', '2ranq-forms' ), __( 'The Audience ID is under Audience, Settings, Audience name and defaults. You type it in the form itself.', '2ranq-forms' ), ), 'keys' => array( 'api_key' => array( 'label' => __( 'API key', '2ranq-forms' ), 'type' => 'password' ), ), 'form' => array( 'list' => array( 'label' => __( 'Audience ID', '2ranq-forms' ), 'type' => 'text', 'help' => __( 'In Mailchimp under Audience, Settings, Audience name and defaults.', '2ranq-forms' ) ), 'tags' => array( 'label' => __( 'Tags, separated by commas', '2ranq-forms' ), 'type' => 'text' ), ), ), ); } /** * The keys, with empty values for whatever was not entered. * * @return array */ public static function keys() { $saved = get_option( self::OPTION, array() ); $saved = is_array( $saved ) ? $saved : array(); $out = array(); foreach ( self::services() as $key => $service ) { foreach ( $service['keys'] as $polje => $opis ) { $out[ $key ][ $polje ] = isset( $saved[ $key ][ $polje ] ) ? (string) $saved[ $key ][ $polje ] : ''; } } return $out; } /** * Saving the keys. * * A secret field that arrives empty is NOT cleared, because the screen does * not print it back. * * @param array $raw The raw values. * * @return void */ public static function save_keys( $raw ) { $stare = self::keys(); $out = array(); foreach ( self::services() as $key => $service ) { foreach ( $service['keys'] as $polje => $opis ) { $ime = 'ranq_conn_' . $key . '_' . $polje; $nova = isset( $raw[ $ime ] ) ? trim( (string) $raw[ $ime ] ) : ''; if ( 'url' === $opis['type'] ) { $nova = esc_url_raw( $nova ); } else { $nova = sanitize_text_field( $nova ); } if ( 'password' === $opis['type'] && '' === $nova ) { $out[ $key ][ $polje ] = $stare[ $key ][ $polje ]; continue; } $out[ $key ][ $polje ] = $nova; } } update_option( self::OPTION, $out, false ); } /** * The link settings of one form. * * @param int $form_id The form id. * * @return array */ public static function for_form( $form_id ) { $s = Ranq_Forms_Form_Settings::get( $form_id ); $out = array(); foreach ( self::services() as $key => $service ) { $out[ $key ]['on'] = ! empty( $s[ 'conn_' . $key . '_on' ] ); foreach ( $service['form'] as $polje => $opis ) { $ime = 'conn_' . $key . '_' . $polje; $out[ $key ][ $polje ] = isset( $s[ $ime ] ) ? $s[ $ime ] : ''; } } return $out; } /** * The per form settings, in the shape the Settings tab understands. * * No record of its own is created. The links go into the existing form * settings, so they are saved, drawn and hidden by the same code as * everything else. * * @return array */ public static function form_schema() { $out = array(); foreach ( self::services() as $key => $service ) { $out[ 'conn_' . $key . '_on' ] = array( /* translators: %s: name of the outside service. */ 'label' => sprintf( __( 'Send the entry to %s', '2ranq-forms' ), $service['name'] ), 'type' => 'toggle', 'default' => false, 'help' => $service['note'], ); foreach ( $service['form'] as $polje => $opis ) { $out[ 'conn_' . $key . '_' . $polje ] = array( 'label' => $opis['label'], 'type' => 'text', 'default' => '', 'depends' => array( 'conn_' . $key . '_on', true ), 'help' => isset( $opis['help'] ) ? $opis['help'] : '', ); } } return $out; } /** * Sending the entry to everyone who is switched on. * * @param int $entry_id The entry id. * @param int $form_id The form id. * @param array $values The field values. * * @return void */ public static function dispatch( $entry_id, $form_id, $values ) { $veze = self::for_form( $form_id ); if ( ! $veze ) { return; } $citljivo = self::readable( $form_id, $values ); foreach ( self::services() as $key => $service ) { if ( empty( $veze[ $key ]['on'] ) ) { continue; } $greska = ''; switch ( $key ) { case 'webhook': $greska = self::send_webhook( $veze[ $key ], $entry_id, $form_id, $citljivo ); break; case 'slack': $greska = self::send_slack( $form_id, $citljivo ); break; case 'mailchimp': $greska = self::send_mailchimp( $veze[ $key ], $citljivo ); break; } if ( '' !== $greska ) { self::log( $key, $form_id, $greska ); } } } /** * The values in a shape a person understands: the field label instead of the * key. * * @param int $form_id The form id. * @param array $values The values. * * @return array */ private static function readable( $form_id, $values ) { $out = array(); foreach ( Ranq_Forms_Fields::get_form_fields( $form_id ) as $field ) { if ( ! isset( $field['id'] ) || ! array_key_exists( $field['id'], $values ) ) { continue; } $object = Ranq_Forms_Fields::get( $field['type'] ); $label = isset( $field['label'] ) && '' !== $field['label'] ? $field['label'] : $field['id']; $out[ $field['id'] ] = array( 'label' => $label, 'type' => $field['type'], 'value' => $object ? $object->format_value( $values[ $field['id'] ], $field ) : $values[ $field['id'] ], ); } return $out; } /** * The webhook. * * @param array $config The settings. * @param int $entry_id The entry id. * @param int $form_id The form id. * @param array $polja The readable values. * * @return string Empty when it went through. */ private static function send_webhook( $config, $entry_id, $form_id, $polja ) { if ( empty( $config['url'] ) ) { return __( 'No address given.', '2ranq-forms' ); } $headers = array( 'Content-Type' => 'application/json; charset=utf-8' ); if ( ! empty( $config['secret'] ) ) { $headers['X-Ranq-Secret'] = $config['secret']; } $telo = array( 'entry_id' => (int) $entry_id, 'form_id' => (int) $form_id, 'form' => get_the_title( $form_id ), 'site' => home_url(), 'sent_at' => gmdate( 'c' ), 'fields' => $polja, ); $r = wp_remote_post( $config['url'], array( 'timeout' => 8, 'headers' => $headers, 'body' => wp_json_encode( $telo ), ) ); return self::result( $r ); } /** * Slack. * * @param int $form_id The form id. * @param array $polja The readable values. * * @return string */ private static function send_slack( $form_id, $polja ) { $kljucevi = self::keys(); if ( empty( $kljucevi['slack']['webhook'] ) ) { return __( 'No Slack address in Settings.', '2ranq-forms' ); } $redovi = array(); foreach ( $polja as $polje ) { $vrednost = is_array( $polje['value'] ) ? implode( ', ', $polje['value'] ) : (string) $polje['value']; if ( '' === trim( $vrednost ) ) { continue; } $redovi[] = '*' . $polje['label'] . ':* ' . $vrednost; } $tekst = sprintf( /* translators: %s: form name. */ __( 'New entry from %s', '2ranq-forms' ), get_the_title( $form_id ) ) . "\n" . implode( "\n", $redovi ); $r = wp_remote_post( $kljucevi['slack']['webhook'], array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'text' => $tekst ) ), ) ); return self::result( $r ); } /** * Mailchimp. * * @param array $config The per form settings. * @param array $polja The readable values. * * @return string */ private static function send_mailchimp( $config, $polja ) { $kljucevi = self::keys(); $kljuc = isset( $kljucevi['mailchimp']['api_key'] ) ? $kljucevi['mailchimp']['api_key'] : ''; if ( '' === $kljuc || empty( $config['list'] ) ) { return __( 'Missing API key or audience.', '2ranq-forms' ); } // The key carries its own data centre at the end, after the dash. $delovi = explode( '-', $kljuc ); $centar = end( $delovi ); if ( ! $centar || $centar === $kljuc ) { return __( 'That API key does not look right.', '2ranq-forms' ); } $posta = ''; $ime = ''; foreach ( $polja as $polje ) { $vrednost = is_array( $polje['value'] ) ? implode( ' ', $polje['value'] ) : (string) $polje['value']; if ( '' === $posta && ( 'email' === $polje['type'] || is_email( $vrednost ) ) ) { $posta = $vrednost; } if ( '' === $ime && in_array( $polje['type'], array( 'name', 'text' ), true ) ) { $ime = $vrednost; } } if ( ! is_email( $posta ) ) { return __( 'No email address in this entry.', '2ranq-forms' ); } $telo = array( 'email_address' => $posta, 'status' => 'subscribed', 'merge_fields' => array( 'FNAME' => $ime ), ); if ( ! empty( $config['tags'] ) ) { $telo['tags'] = array_filter( array_map( 'trim', explode( ',', $config['tags'] ) ) ); } $r = wp_remote_post( 'https://' . rawurlencode( $centar ) . '.api.mailchimp.com/3.0/lists/' . rawurlencode( $config['list'] ) . '/members', array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . base64_encode( 'ranq:' . $kljuc ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- this is what basic authentication asks for, not code hiding. ), 'body' => wp_json_encode( $telo ), ) ); // A person who is on the list already is not an error. $kod = is_wp_error( $r ) ? 0 : (int) wp_remote_retrieve_response_code( $r ); if ( 400 === $kod && false !== strpos( (string) wp_remote_retrieve_body( $r ), 'Member Exists' ) ) { return ''; } return self::result( $r ); } /** * The outcome of one request. * * @param array|WP_Error $r The response. * * @return string */ private static function result( $r ) { if ( is_wp_error( $r ) ) { return $r->get_error_message(); } $kod = (int) wp_remote_retrieve_response_code( $r ); if ( $kod >= 200 && $kod < 300 ) { return ''; } return sprintf( /* translators: 1: HTTP status code, 2: first part of the response. */ __( 'The other side answered %1$d: %2$s', '2ranq-forms' ), $kod, substr( (string) wp_remote_retrieve_body( $r ), 0, 160 ) ); } /** * A note about an error, so the owner knows why a link does not work. * * @param string $service The service. * @param int $form_id The form id. * @param string $message The message. * * @return void */ private static function log( $service, $form_id, $message ) { $log = get_option( self::LOG, array() ); $log = is_array( $log ) ? $log : array(); array_unshift( $log, array( 'when' => gmdate( 'Y-m-d H:i' ), 'service' => $service, 'form' => (int) $form_id, 'message' => $message, ) ); update_option( self::LOG, array_slice( $log, 0, 10 ), false ); } }