` use the prefix `_`: * * _list list / audience / campaign / form / group id (select loaded from * GET zadex/v1/forms/integrations//lists, see ListsController) * _email_field field id holding the email ('' = the first email field) * _name_field field id of the (first) name _last_name_field field id of the last name * _fields repeater {key, value}: provider custom / merge fields, value with shortcodes * _tags comma separated tags (names; ConvertKit: tag ids) * _double_optin Mailchimp only: new contacts get "pending" (confirmation email) * _required a failure makes the visitor see the form's error message * * Every HTTP call goes through `Http` (`wp_remote_request`). Errors are logged (`Log`) and returned as * `WP_Error`, which the action registry reports through `zadex/forms/action_failed`; visitors never see * provider messages. */ abstract class EmailProvider { abstract public function id(): string; abstract public function label(): string; /** Whether the credentials needed by this provider are saved. */ abstract public function configured(): bool; /** Whether the provider needs a list / audience to subscribe to. */ public function needs_list(): bool { return true; } /** * Lists / audiences for the editor select. * * @return array|WP_Error */ abstract public function lists(); /** * Adds or updates the contact. * * @param array{email: string, first_name: string, last_name: string, name: string, fields: array, tags: array, list: string, double_optin: bool} $contact * @return true|WP_Error */ abstract public function subscribe(array $contact); /** Action handler registered in the Actions registry. @return null|WP_Error */ public function handle(Submission $s, array $settings) { $contact = $this->contact($s, $settings); if ($contact instanceof WP_Error) { Log::error($this->id(), $contact->get_error_message(), ['form' => $s->form_id]); return $contact; } if (!$this->configured()) { $error = new WP_Error('zadex_integration_not_configured', $this->label() . ': the API key is not set (Zadex > Settings > Integrations).'); Log::error($this->id(), $error->get_error_message(), ['form' => $s->form_id]); return $error; } if ($this->needs_list() && $contact['list'] === '') { $error = new WP_Error('zadex_integration_no_list', $this->label() . ': no list selected in the form settings.'); Log::error($this->id(), $error->get_error_message(), ['form' => $s->form_id]); return $error; } $result = $this->subscribe($contact); if ($result instanceof WP_Error) { Log::error($this->id(), $result->get_error_message(), ['form' => $s->form_id, 'submission' => $s->id]); return $result; } return null; } /** * Contact data from the submission and the form settings. * * @return array{email: string, first_name: string, last_name: string, name: string, fields: array, tags: array, list: string, double_optin: bool}|WP_Error */ public function contact(Submission $s, array $settings) { $p = $this->id() . '_'; $get = static fn(string $k): string => isset($settings[$p . $k]) && is_scalar($settings[$p . $k]) ? trim((string) $settings[$p . $k]) : ''; $email_field = Fields::sanitize_id($get('email_field')); $email = $email_field !== '' ? $s->text($email_field) : $s->first_email(); $email = sanitize_email($email); if ($email === '' || !is_email($email)) { return new WP_Error('zadex_integration_no_email', $this->label() . ': the submission has no valid email address.'); } $name_field = Fields::sanitize_id($get('name_field')); $last_field = Fields::sanitize_id($get('last_name_field')); $first = $name_field !== '' ? sanitize_text_field($s->text($name_field)) : ''; $last = $last_field !== '' ? sanitize_text_field($s->text($last_field)) : ''; $fields = []; foreach (is_array($settings[$p . 'fields'] ?? null) ? $settings[$p . 'fields'] : [] as $row) { if (!is_array($row)) { continue; } $key = isset($row['key']) && is_scalar($row['key']) ? preg_replace('/[^A-Za-z0-9_\-.]/', '', (string) $row['key']) : ''; $value = isset($row['value']) && is_scalar($row['value']) ? (string) $row['value'] : ''; if ($key !== '' && $key !== null) { $fields[$key] = Shortcodes::render($value, $s, 'plain'); } } return [ 'email' => $email, 'first_name' => $first, 'last_name' => $last, 'name' => trim($first . ' ' . $last), 'fields' => $fields, 'tags' => self::tags(Shortcodes::render($get('tags'), $s, 'header')), 'list' => $get('list'), 'double_optin' => !empty($settings[$p . 'double_optin']), ]; } /** @return array */ public static function tags(string $raw): array { $tags = array_map(static fn(string $t): string => sanitize_text_field($t), explode(',', $raw)); return array_values(array_unique(array_filter($tags, static fn(string $t): bool => $t !== ''))); } /** * Maps a provider list response into select options. * * @param mixed $items * @return array */ protected static function options($items, string $id_key, string $name_key): array { $out = []; foreach (is_array($items) ? $items : [] as $item) { if (is_array($item) && isset($item[$id_key]) && is_scalar($item[$id_key])) { $label = isset($item[$name_key]) && is_scalar($item[$name_key]) ? wp_strip_all_tags((string) $item[$name_key]) : ''; $out[] = ['value' => (string) $item[$id_key], 'label' => $label !== '' ? $label : (string) $item[$id_key]]; } } return $out; } protected static function basic(string $user, string $password): string { // HTTP Basic authentication header required by the provider API (not obfuscation). return 'Basic ' . base64_encode($user . ':' . $password); } }