send( $dto, $source_slug ); } } /** @return array Keyed "{source}:{form id}". */ public static function all() { $saved = get_option( self::OPTION, array() ); return is_array( $saved ) ? $saved : array(); } public static function key( $source_slug, $form_id ) { return $source_slug . ':' . $form_id; } /** @return array{form:string, consent:string}|null */ public static function get( $source_slug, $form_id ) { $all = self::all(); $key = self::key( $source_slug, $form_id ); if ( ! isset( $all[ $key ]['form'], $all[ $key ]['consent'] ) || '' === $all[ $key ]['form'] || '' === $all[ $key ]['consent'] ) { return null; } return $all[ $key ]; } public static function save( array $connections ) { update_option( self::OPTION, $connections, false ); } /** * Cleans what Other_Forms_Page posted. A form is connected only with a * 404 Emails form that exists and a "who" that is either everyone or one * of that form's own consent fields. * * @param array $raw emails404_connections[{key}][form|consent], unslashed. * @param array $forms The site's forms, keyed like self::key() => source form (see Form_Source_Interface). * @param array $known_ids Published 404 Emails form ids. * @return array{connections: array, incomplete: string[]} `incomplete` - keys with a 404 Emails form but no "who". */ public static function sanitize( array $raw, array $forms, array $known_ids ) { $connections = array(); $incomplete = array(); foreach ( $forms as $key => $form ) { if ( ! isset( $raw[ $key ] ) || ! is_array( $raw[ $key ] ) ) { continue; } $target = isset( $raw[ $key ]['form'] ) && is_string( $raw[ $key ]['form'] ) ? trim( $raw[ $key ]['form'] ) : ''; $consent = isset( $raw[ $key ]['consent'] ) && is_string( $raw[ $key ]['consent'] ) ? $raw[ $key ]['consent'] : ''; if ( '' === $target || ! in_array( $target, $known_ids, true ) || ! self::find_field( $form, self::KIND_EMAIL ) ) { continue; } if ( self::EVERYONE !== $consent && ! self::consent_field( $form, $consent ) ) { $incomplete[] = $key; continue; } $connections[ $key ] = array( 'form' => $target, 'consent' => $consent, ); } return array( 'connections' => $connections, 'incomplete' => $incomplete, ); } /** * What "Who is added" starts as for a form not set up yet: a checkbox * whose wording is about emails or news. Otherwise nothing, and the owner * chooses - a form's first checkbox is often "I agree to the privacy * policy", which isn't permission to send newsletters. */ public static function suggested_consent( array $form ) { foreach ( self::consent_fields( $form ) as $field ) { if ( preg_match( '/(*UCP)(news|e-?mail|marketing|offers|updates|subscribe|novink|zpravodaj|nabídk|newsletter|odběr)/iu', $field['label'] ) ) { return $field['key']; } } return ''; } /** * Called by a source when one of its forms was sent. * * @param Form_Source_Interface $source * @param string $form_id The form's id in that plugin. * @param array $values Field key => submitted value (string or array). * @param string $entity_id Unique to this submission (an entry id, or a random one). * @param array|null $form The form's description, when the source already has it. */ public static function receive( Form_Source_Interface $source, $form_id, array $values, $entity_id, $form = null ) { if ( ! Settings::is_connected() || null === self::$sync_service ) { return; } $connection = self::get( $source->get_slug(), (string) $form_id ); if ( null === $connection ) { return; } if ( ! is_array( $form ) ) { $form = $source->describe_form( (string) $form_id ); } if ( ! is_array( $form ) ) { return; } $dto = self::build_signup( $source->get_slug(), $form, $values, $connection, $entity_id, self::site_host() ); if ( null !== $dto ) { self::$sync_service->send( $dto, $source->get_slug() ); } } /** * The sign-up to send, or null when there's nothing to send: no valid * email, or the person didn't tick the permission box. * * @return Signup_Dto|null */ public static function build_signup( $source_slug, array $form, array $values, array $connection, $entity_id, $site_host ) { if ( self::EVERYONE === $connection['consent'] ) { /* translators: 1: a form's title, 2: the website, e.g. "example.com". */ $consent_text = sprintf( __( 'Sent the "%1$s" sign-up form on %2$s', '404-emails' ), $form['title'], $site_host ); } else { $field = self::consent_field( $form, $connection['consent'] ); if ( ! $field ) { return null; } $ticked = self::is_ticked( isset( $values[ $field['key'] ] ) ? $values[ $field['key'] ] : '' ); if ( ! empty( $field['invert'] ) ) { $ticked = ! $ticked; } if ( ! $ticked ) { return null; } $consent_text = '' !== trim( (string) $field['label'] ) ? (string) $field['label'] : null; } $email = ''; foreach ( $form['fields'] as $field ) { if ( self::KIND_EMAIL === $field['kind'] ) { $candidate = sanitize_email( self::text( $values, $field['key'] ) ); if ( '' !== $candidate && is_email( $candidate ) ) { $email = $candidate; break; } } } if ( '' === $email ) { return null; } list( $first_name, $last_name ) = self::names( $form, $values ); if ( null !== $consent_text ) { $consent_text = function_exists( 'mb_substr' ) ? mb_substr( $consent_text, 0, self::CONSENT_TEXT_MAX ) : substr( $consent_text, 0, self::CONSENT_TEXT_MAX ); } return new Signup_Dto( $email, $first_name, $last_name, true, sprintf( 'wordpress-%s:form-%s', $source_slug, $form['id'] ), $connection['form'], Idempotency::build_key( $source_slug, 'form-' . $form['id'] . '-' . $entity_id, $connection['form'] ), $consent_text ); } /** * First and last name: from separate fields, or a single "name" field * split at its first space ("Jana Nová Malá" → "Jana", "Nová Malá"). * * @return array{0:string|null, 1:string|null} */ public static function names( array $form, array $values ) { $first = null; $last = null; $full = null; foreach ( $form['fields'] as $field ) { $value = self::text( $values, $field['key'] ); if ( '' === $value ) { continue; } if ( self::KIND_FIRST_NAME === $field['kind'] && null === $first ) { $first = $value; } elseif ( self::KIND_LAST_NAME === $field['kind'] && null === $last ) { $last = $value; } elseif ( self::KIND_FULL_NAME === $field['kind'] && null === $full ) { $full = $value; } } if ( null === $first && null === $last && null !== $full ) { $parts = preg_split( '/\s+/u', $full, 2 ); $first = $parts[0]; $last = isset( $parts[1] ) && '' !== $parts[1] ? $parts[1] : null; } return array( self::limit( $first ), self::limit( $last ) ); } /** * A field's kind from its name and label, for plugins that only know * "text" (Contact Form 7, a text field in WPForms or Elementor). * English and Czech words. */ public static function kind_from_words( $key, $label ) { $words = strtolower( $key . ' ' . $label ); if ( function_exists( 'mb_strtolower' ) ) { $words = mb_strtolower( $key . ' ' . $label ); } $words = str_replace( array( '-', '_' ), ' ', $words ); if ( preg_match( '/(*UCP)\b(full ?name|your name|jméno a příjmení|celé jméno|vaše jméno|tvoje jméno)\b/u', $words ) || preg_match( '/^\s*(name|jméno)\s*\*?\s*$/iu', $label ) || in_array( trim( strtolower( $key ) ), array( 'name', 'your-name', 'jmeno' ), true ) ) { return self::KIND_FULL_NAME; } if ( preg_match( '/(*UCP)\b(last ?name|surname|family name|příjmení|prijmeni)\b/u', $words ) ) { return self::KIND_LAST_NAME; } if ( preg_match( '/(*UCP)\b(first ?name|given name|křestní|krestni|jméno|jmeno)\b/u', $words ) ) { return self::KIND_FIRST_NAME; } return self::KIND_OTHER; } /** A checkbox's submitted value: ticked when it has any value other than an "off" one. */ public static function is_ticked( $value ) { if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( self::is_ticked( $item ) ) { return true; } } return false; } $value = strtolower( trim( (string) $value ) ); return '' !== $value && ! in_array( $value, array( '0', 'no', 'false', 'off' ), true ); } /** @return array|null The form's first field of that kind. */ public static function find_field( array $form, $kind ) { foreach ( $form['fields'] as $field ) { if ( $kind === $field['kind'] ) { return $field; } } return null; } /** @return array[] The form's consent checkboxes. */ public static function consent_fields( array $form ) { return array_values( array_filter( $form['fields'], static function ( $field ) { return Form_Connections::KIND_CONSENT === $field['kind']; } ) ); } private static function consent_field( array $form, $key ) { foreach ( self::consent_fields( $form ) as $field ) { if ( (string) $key === (string) $field['key'] ) { return $field; } } return null; } private static function text( array $values, $key ) { if ( ! isset( $values[ $key ] ) ) { return ''; } $value = $values[ $key ]; if ( is_array( $value ) ) { $value = implode( ' ', array_filter( array_map( 'strval', $value ) ) ); } return trim( sanitize_text_field( (string) $value ) ); } private static function limit( $value ) { if ( null === $value ) { return null; } return function_exists( 'mb_substr' ) ? mb_substr( $value, 0, 200 ) : substr( $value, 0, 200 ); } private static function site_host() { $host = function_exists( 'home_url' ) ? wp_parse_url( home_url(), PHP_URL_HOST ) : ''; return $host ? (string) $host : 'this website'; } }