$page); $key = self::scalar($input, 'return_group'); $group = strlen($key) <= 120 && $key !== '' ? Outcomes::group($key) : null; if ($group && $page === 'customfields') { $args['edit'] = $group['key']; } if ($group && $page === 'customfields-start') { $args['group'] = $group['key']; $record = self::scalar($input, 'return_record'); if (preg_match('/^(0|[1-9][0-9]{0,9})$/D', $record) && Outcomes::context($group, (int) $record)) { $args['record'] = (int) $record; } } $tab = self::scalar($input, 'return_tab'); if ($page === 'customfields-publishing' && in_array($tab, array('blocks', 'listings'), true)) { $args['tab'] = $tab; } return add_query_arg($args, admin_url('admin.php')); } private static function key($name) { global $wpdb; return $wpdb->get_blog_prefix() . $name; } private static function progress() { return get_user_meta(get_current_user_id(), self::key(self::PROGRESS), true); } private static function complete() { update_user_meta(get_current_user_id(), self::key(self::PROGRESS), 'complete'); } private static function binding() { return hash('sha256', home_url('/') . '|' . get_current_blog_id()); } private static function email_hash($email) { return hash_hmac('sha256', strtolower(trim($email)), wp_salt('auth')); } private static function state() { $state = WelcomeStore::freshOption(self::OPTION, array()); if (!is_array($state)) { throw new \RuntimeException('Welcome choices are unreadable.'); } foreach (array('registration', 'newsletter') as $scope) { if (!isset($state[$scope])) { continue; } $record = $state[$scope]; if (!is_array($record) || !is_string($record['id'] ?? null) || !preg_match('/^[a-f0-9]{32}$/D', $record['id']) || !is_string($record['token'] ?? null) || !preg_match('/^[a-f0-9]{64}$/D', $record['token']) || !is_string($record['binding'] ?? null) || !preg_match('/^[a-f0-9]{64}$/D', $record['binding']) || !in_array($record['status'] ?? '', array('pending', 'accepted'), true) || !is_int($record['created_at'] ?? null) || !is_string($record['consented_at'] ?? null) || (isset($record['withdraw']) && $record['withdraw'] !== true)) { throw new \RuntimeException('Welcome scope is unreadable.'); } if (empty($record['withdraw'])) { $payload = $record['payload'] ?? null; if (!is_array($payload) || ($payload['consent_version'] ?? '') !== self::CONSENT || !is_int($payload['created_at'] ?? null) || !is_array($payload[$scope] ?? null)) { throw new \RuntimeException('Welcome permission is unreadable.'); } $value = $payload[$scope][$scope === 'registration' ? 'domain' : 'email'] ?? null; if (!is_string($value) || ($scope === 'registration' ? !self::public_domain($value) : !is_email($value))) { throw new \RuntimeException('Welcome detail is unreadable.'); } } } return $state; } private static function choice_action($state) { return 'customfields_welcome_choice_' . hash('sha256', wp_json_encode($state)); } private static function locked($callback) { $store = new WelcomeStore(); $owner = $store->lock('welcome_connection', 60); if (!$owner) { return new \WP_Error('cf_welcome_busy', __('Your choices are being saved. Wait a moment, then retry.', 'customfields-com')); } try { return $callback(); } finally { $store->unlock('welcome_connection', $owner); } } /** First deliberate, nonce-authorized individual activation only. Updates and reactivation do not hijack navigation. */ public static function activated($network_wide = false) { $first = add_option('customfields_welcome_activation_seen', 1, '', false); if (!$first || $network_wide || is_multisite() || !is_admin() || wp_doing_ajax() || wp_doing_cron() || !self::allowed() || !current_user_can('activate_plugins') || (defined('WP_CLI') && WP_CLI) || (defined('REST_REQUEST') && REST_REQUEST)) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Core authorizes plugin activation; the nonce below additionally limits this local one-time handoff. $action = self::scalar($_REQUEST, 'action'); $nonce = (isset($_REQUEST['_wpnonce']) && is_string($_REQUEST['_wpnonce']) ? sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])) : ''); if ($action !== 'activate' || !wp_verify_nonce($nonce, 'activate-plugin_' . plugin_basename(CUSTOMFIELDS_FILE))) { return; } set_transient('customfields_welcome_redirect_' . get_current_user_id(), true, 2 * MINUTE_IN_SECONDS); } public static function redirect() { global $pagenow; if ($pagenow !== 'plugins.php' || !self::allowed() || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) { return; } $key = 'customfields_welcome_redirect_' . get_current_user_id(); if (!get_transient($key)) { return; } delete_transient($key); if (headers_sent()) { return; } wp_safe_redirect(admin_url('admin.php?page=customfields-start')); exit; } public static function schema_saved($groups, $before) { if (!self::allowed() || !is_array($groups) || !is_array($before) || $groups === $before) { return; } foreach ($groups as $group) { if (is_array($group) && !empty($group['fields'])) { self::ready(); return; } } } public static function values_saved($group, $id, $values) { if (self::allowed() && is_array($values) && $values) { self::ready(); } } private static function ready() { // Presentation only. Never overwrites a completed choice or authorizes external reporting. if (self::progress() === 'complete') { return; } add_user_meta(get_current_user_id(), self::key(self::PROGRESS), 'ready', true); } private static function own_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only asset/presentation routing; no consent is inferred. $page = sanitize_key(self::scalar($_GET, 'page')); return $page === 'customfields' || strpos($page, 'customfields-') === 0; } public static function menu() { add_submenu_page('customfields', __('Welcome choices', 'customfields-com'), __('Welcome choices', 'customfields-com'), 'manage_options', 'customfields-welcome', array(__CLASS__, 'page')); } public static function assets() { if (!self::allowed() || !self::own_page()) { return; } wp_enqueue_style('customfields-welcome', CUSTOMFIELDS_URL . 'assets/welcome.css', array('customfields-admin'), CUSTOMFIELDS_VERSION); wp_enqueue_script('customfields-welcome', CUSTOMFIELDS_URL . 'assets/welcome.js', array(), CUSTOMFIELDS_VERSION, true); } public static function dashboard() { if (!self::allowed()) { return; } echo '

' . esc_html__('Optional welcome choices', 'customfields-com') . '' . esc_html__('Site registration and email tips, only if you choose.', 'customfields-com') . '

'; } public static function domain() { return strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); } private static function public_domain($domain) { return is_string($domain) && strlen($domain) <= 253 && preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/D', $domain) && !preg_match('/\.(?:localhost|local|internal|test|invalid|example)$/D', $domain) && !filter_var($domain, FILTER_VALIDATE_IP); } /** No site-bearing default UA, cookies, redirects, arbitrary endpoint or response diagnostics. */ private static function request($record, $operation, $payload) { if (!preg_match('/^[a-f0-9]{32}$/D', $record['id'] ?? '') || !preg_match('/^[a-f0-9]{64}$/D', $record['token'] ?? '')) { return new \WP_Error('cf_welcome_state', __('This saved request is unavailable. Review your choices.', 'customfields-com')); } $body = wp_json_encode($payload); if (!is_string($body) || strlen($body) > 2048) { return new \WP_Error('cf_welcome_payload', __('The selected details could not be prepared.', 'customfields-com')); } $response = wp_safe_remote_post(self::ENDPOINT . $record['id'] . '/' . $operation, array( 'timeout' => 8, 'redirection' => 0, 'sslverify' => true, 'limit_response_size' => 2048, 'user-agent' => 'CustomFields-Welcome/1', 'cookies' => array(), 'data_format' => 'body', 'headers' => array('Content-Type' => 'application/json', 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $record['token']), 'body' => $body, )); $code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response); if ($code !== ($operation === 'status' ? 200 : 202)) { return new \WP_Error('cf_welcome_delivery', __('Delivery could not be confirmed. Your fields still work. Review the saved request below before retrying.', 'customfields-com')); } $data = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($data) || ($operation !== 'status' && ($data['accepted'] ?? null) !== true)) { return new \WP_Error('cf_welcome_response', __('The service response could not be confirmed. Review the saved request before retrying.', 'customfields-com')); } return $data; } private static function payload($input) { foreach (array('registration', 'newsletter') as $scope) { if (isset($input[$scope]) && (!is_string($input[$scope]) || self::scalar($input, $scope) !== '1')) { return new \WP_Error('cf_welcome_choice', __('Choose only the options you want to share.', 'customfields-com')); } } $email = trim(self::scalar($input, 'email')); if (self::scalar($input, 'newsletter') === '1' && (strlen($email) > 254 || !is_email($email) || sanitize_email($email) !== $email)) { return new \WP_Error('cf_welcome_email', __('Enter a valid email address, or leave the email choice unchecked.', 'customfields-com')); } $domain = self::domain(); if (self::scalar($input, 'registration') === '1' && !self::public_domain($domain)) { return new \WP_Error('cf_welcome_domain', __('This site has no supported public domain to share. You can still choose email tips.', 'customfields-com')); } return array('registration' => self::scalar($input, 'registration') === '1' ? array('domain' => $domain) : null, 'newsletter' => self::scalar($input, 'newsletter') === '1' ? array('email' => $email) : null); } private static function save($input) { $choices = self::payload($input); if (is_wp_error($choices)) { return $choices; } if (!$choices['registration'] && !$choices['newsletter']) { return array('message' => __('Nothing shared. All Free tools remain available.', 'customfields-com')); } $nonce = sanitize_text_field(self::scalar($input, 'choice_nonce')); return self::locked(static function() use ($choices, $nonce) { $state = self::state(); if (!wp_verify_nonce($nonce, self::choice_action($state))) { return new \WP_Error('cf_welcome_stale', __('Your choices changed in another request. Reload this screen and review them before sharing.', 'customfields-com')); } foreach ($choices as $scope => $value) { if (!$value) { continue; } if (!empty($state[$scope])) { return new \WP_Error('cf_welcome_existing', __('This choice already has a saved request. Manage it below before sharing different details.', 'customfields-com')); } } foreach ($choices as $scope => $value) { if (!$value) { continue; } $state[$scope] = array('id' => bin2hex(random_bytes(16)), 'token' => bin2hex(random_bytes(32)), 'binding' => self::binding(), 'status' => 'pending', 'created_at' => time(), 'consented_at' => gmdate('c'), 'payload' => array('consent_version' => self::CONSENT, 'created_at' => time(), 'registration' => $scope === 'registration' ? $value : null, 'newsletter' => $scope === 'newsletter' ? $value : null)); } $state['revision'] = bin2hex(random_bytes(24)); WelcomeStore::saveOption(self::OPTION, $state); $error = null; foreach ($choices as $scope => $value) { if (!$value) { continue; } $result = self::deliver($state, $scope); if (is_wp_error($result)) { $error = $result; } } return $error ?: array('message' => self::success_message($choices)); }); } private static function success_message($choices) { if (!$choices['newsletter']) { return __('Your site registration request was accepted. You can manage or remove it in Welcome choices.', 'customfields-com'); } return $choices['registration'] ? __('Your site registration and email request were accepted. Check your inbox or existing email preferences; existing subscriptions and preferences stay unchanged.', 'customfields-com') : __('Your email request was accepted. Check your inbox or existing email preferences; existing subscriptions and preferences stay unchanged.', 'customfields-com'); } private static function deliver(&$state, $scope) { $record = $state[$scope]; if (($record['binding'] ?? '') !== self::binding()) { return new \WP_Error('cf_welcome_binding', __('The site address changed. Remove the old request before making choices for this site.', 'customfields-com')); } if (time() - (int) $record['created_at'] > 15 * MINUTE_IN_SECONDS) { return new \WP_Error('cf_welcome_expired', __('This unconfirmed request is too old to resend. Cancel it, then review and save fresh choices.', 'customfields-com')); } $result = self::request($record, 'enroll', $record['payload']); if (!is_wp_error($result)) { $state[$scope]['status'] = 'accepted'; $state[$scope]['last_refresh'] = time(); } WelcomeStore::saveOption(self::OPTION, $state); return $result; } private static function withdraw_scope(&$state, $scope) { if (empty($state[$scope])) { return true; } $record = $state[$scope]; $record['withdraw'] = true; if ($scope === 'newsletter' && !empty($record['payload']['newsletter']['email'])) { $record['email_hash'] = self::email_hash($record['payload']['newsletter']['email']); } unset($record['payload']); $state[$scope] = $record; $state['revision'] = bin2hex(random_bytes(24)); WelcomeStore::saveOption(self::OPTION, $state); $result = self::request($record, 'withdraw', array('registration' => $scope === 'registration', 'newsletter' => $scope === 'newsletter')); if (is_wp_error($result)) { return new \WP_Error('cf_welcome_withdraw', __('Sharing stopped locally, but removal could not be confirmed. Retry the removal below. You can also unsubscribe using a link in an email.', 'customfields-com')); } unset($state[$scope]); WelcomeStore::saveOption(self::OPTION, $state); return true; } private static function manage($operation, $scope, $nonce) { return self::locked(static function() use ($operation, $scope, $nonce) { $state = self::state(); if (!wp_verify_nonce($nonce, self::choice_action($state))) { return new \WP_Error('cf_welcome_stale', __('Your choices changed. Reload before changing or refreshing this request.', 'customfields-com')); } if (empty($state[$scope])) { return array('message' => __('There is no saved request for this choice.', 'customfields-com')); } if ($operation === 'stop' || !empty($state[$scope]['withdraw'])) { $result = self::withdraw_scope($state, $scope); return is_wp_error($result) ? $result : array('message' => __('The selected removal was accepted. Other choices and pre-existing subscriptions are unchanged.', 'customfields-com')); } if (($state[$scope]['binding'] ?? '') !== self::binding()) { return new \WP_Error('cf_welcome_binding', __('The site address changed. Remove the old request before making choices for this site.', 'customfields-com')); } if ($operation === 'retry' && $state[$scope]['status'] === 'pending') { $result = self::deliver($state, $scope); return is_wp_error($result) ? $result : array('message' => __('Request accepted. Check your inbox or existing email preferences.', 'customfields-com')); } if ($operation === 'refresh' && $scope === 'registration') { if ($state[$scope]['status'] !== 'accepted') { return new \WP_Error('cf_welcome_pending', __('Confirm or cancel the saved request before refreshing.', 'customfields-com')); } $result = self::request($state[$scope], 'refresh', array('consent_version' => self::CONSENT, 'registration' => array('domain' => self::domain()))); if (is_wp_error($result)) { return $result; } $state[$scope]['last_refresh'] = time(); $state[$scope]['remote_status'] = 'active'; WelcomeStore::saveOption(self::OPTION, $state); return array('message' => __('Site registration refreshed. No email information was sent.', 'customfields-com')); } $result = self::request($state[$scope], 'status', (object) array()); if (is_wp_error($result)) { return $result; } // Only fixed public states are accepted; response text and URLs are never rendered. $status = $result[$scope] ?? ''; if (is_array($status)) { $status = $status['status'] ?? ''; } $allowed = $scope === 'registration' ? array('active', 'inactive') : array('pending', 'confirmed', 'unsubscribed', 'not_requested'); if (!is_string($status) || !in_array($status, $allowed, true)) { return new \WP_Error('cf_welcome_status', __('Status could not be confirmed. Your local choices are unchanged.', 'customfields-com')); } $state[$scope]['remote_status'] = $status; $state[$scope]['status_checked'] = time(); WelcomeStore::saveOption(self::OPTION, $state); return array('message' => __('Status refreshed for this request.', 'customfields-com')); }); } public static function handle() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact HTTP method comparison only. if (!self::allowed() || (isset($_SERVER['REQUEST_METHOD']) && is_string($_SERVER['REQUEST_METHOD']) ? wp_unslash($_SERVER['REQUEST_METHOD']) : '') !== 'POST') { wp_die(esc_html__('Administrator access and a POST request are required.', 'customfields-com'), '', array('response' => 403)); } $nonce = (isset($_POST['_wpnonce']) && is_string($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''); if (!wp_verify_nonce($nonce, 'customfields_welcome') || self::scalar($_POST, 'complete') !== '1') { wp_die(esc_html__('Reload this complete form before continuing.', 'customfields-com'), '', array('response' => 403)); } $operation = sanitize_key(self::scalar($_POST, 'operation')); $scope = sanitize_key(self::scalar($_POST, 'scope')); try { if ($operation === 'skip') { $result = array('message' => __('No new information shared. Existing choices, if any, remain unchanged.', 'customfields-com')); } elseif ($operation === 'save') { $result = self::save($_POST); } elseif (in_array($operation, array('stop', 'retry', 'status', 'refresh'), true) && in_array($scope, array('registration', 'newsletter'), true)) { $result = self::manage($operation, $scope, sanitize_text_field(self::scalar($_POST, 'choice_nonce'))); } else { $result = new \WP_Error('cf_welcome_action', __('Choose an available action.', 'customfields-com')); } if (!is_wp_error($result) && in_array($operation, array('save', 'skip'), true)) { self::complete(); } } catch (\Throwable $error) { $result = new \WP_Error('cf_welcome_storage', __('Your choices could not be confirmed. Review the saved state before retrying. All Free tools remain available.', 'customfields-com')); } set_transient('customfields_welcome_notice_' . get_current_user_id(), array('error' => is_wp_error($result), 'message' => is_wp_error($result) ? $result->get_error_message() : $result['message']), 2 * MINUTE_IN_SECONDS); $return = !is_wp_error($result) && in_array($operation, array('save', 'skip'), true) ? self::return_url($_POST) : self::url(); wp_safe_redirect($return); exit; } public static function notice() { if (!self::allowed() || !self::own_page()) { return; } $key = 'customfields_welcome_notice_' . get_current_user_id(); $notice = get_transient($key); if (!is_array($notice) || !is_string($notice['message'] ?? null)) { return; } delete_transient($key); echo '

' . esc_html($notice['message']) . '

'; } private static function form($operation, $scope = '') { echo '
'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only return context; handle() verifies the form nonce and rebuilds an allowlisted local route. $context = array('return_page' => sanitize_key(self::scalar($_GET, 'page')), 'return_group' => sanitize_text_field(self::scalar($_GET, 'edit') ?: self::scalar($_GET, 'group')), 'return_record' => sanitize_text_field(self::scalar($_GET, 'record')), 'return_tab' => sanitize_key(self::scalar($_GET, 'tab'))); foreach ($context as $name => $value) { echo ''; } wp_nonce_field('customfields_welcome'); wp_nonce_field(self::choice_action(self::state()), 'choice_nonce', false); } private static function end_form($label, $primary = false) { echo '
'; } private static function choices($state) { if (!empty($state['registration']) && !empty($state['newsletter'])) { return; } self::form('save'); if (empty($state['registration'])) { echo '
' . esc_html__('Optional site registration', 'customfields-com') . '

' . esc_html__('Exact domain:', 'customfields-com') . ' ' . esc_html(self::domain()) . '

' . esc_html__('Domain only. Registration expires after 90 days unless you explicitly refresh it here. No automatic reports, full URL, field values, software versions or plugin list.', 'customfields-com') . '

'; } if (empty($state['registration']) && !self::public_domain(self::domain())) { echo '

' . esc_html__('Site registration needs a public domain. Local addresses and IPs cannot register; email tips are still optional and available.', 'customfields-com') . '

'; } if (empty($state['newsletter'])) { echo '
' . esc_html__('Optional email signup', 'customfields-com') . '

' . esc_html__('Use an address you are authorized to subscribe. Confirm from your inbox to join; unsubscribe anytime. Existing subscriptions and preferences stay unchanged.', 'customfields-com') . '

'; } echo '

' . esc_html__('Choose either, both or neither. Free works without sharing. You can manage these choices here later.', 'customfields-com') . '

' . esc_html__('Data and privacy details', 'customfields-com') . '

' . esc_html__('Only your selected details are sent to CustomFields.com, with a random request ID, protected credential, consent version and consent time. The service also receives the connection’s network address. Email-only signup does not include your domain or site URL.', 'customfields-com') . '

' . esc_html__('Inactive site registrations expire after 90 days. Unconfirmed email requests expire after 48 hours; confirmed subscriptions are retained for at most 730 days. Consent and unsubscribe suppression records can remain for 180 days. Removal controls and backups have their own retention periods described in the privacy policy.', 'customfields-com') . '

' . esc_html__('Privacy policy (opens in a new tab)', 'customfields-com') . '
'; self::end_form(__('Save choices', 'customfields-com'), true); self::form('skip'); self::end_form(__('Continue without sharing', 'customfields-com')); } private static function connection($scope, $record) { $labels = array('pending' => __('Check your inbox or existing email preferences', 'customfields-com'), 'confirmed' => __('Confirmed subscriber', 'customfields-com'), 'unsubscribed' => __('Unsubscribed', 'customfields-com'), 'not_requested' => __('No subscription linked to this request', 'customfields-com'), 'active' => __('Registered', 'customfields-com'), 'inactive' => __('Inactive registration', 'customfields-com')); echo '

' . esc_html($scope === 'registration' ? __('Site registration', 'customfields-com') : __('Email tips and offers', 'customfields-com')) . '

'; if (!empty($record['withdraw'])) { echo '

' . esc_html__('Stopped locally. Remote removal is awaiting confirmation.', 'customfields-com') . '

'; self::form('retry', $scope); self::end_form(__('Retry removal', 'customfields-com')); } else { $value = $scope === 'registration' ? ($record['payload']['registration']['domain'] ?? '') : ($record['payload']['newsletter']['email'] ?? ''); echo '

' . esc_html($value) . '

' . esc_html($record['status'] === 'pending' ? __('Delivery unconfirmed. Review or cancel this request.', 'customfields-com') : ($labels[$record['remote_status'] ?? ''] ?? __('Request accepted. Use Refresh status to check this request.', 'customfields-com'))) . '

'; if (($record['binding'] ?? '') !== self::binding()) { echo '

' . esc_html__('The site address changed. Remove the old request before making choices for this site.', 'customfields-com') . '

'; } self::form($record['status'] === 'pending' ? 'retry' : 'status', $scope); self::end_form($record['status'] === 'pending' ? __('Retry saved request', 'customfields-com') : __('Refresh status', 'customfields-com')); if ($scope === 'registration' && $record['status'] === 'accepted') { self::form('refresh', $scope); self::end_form(__('Refresh site registration', 'customfields-com')); } if ($scope === 'registration' && time() > (int) ($record['last_refresh'] ?? 0) + 90 * DAY_IN_SECONDS) { echo '

' . esc_html__('This registration is older than 90 days and may have expired. Check its status before making fresh choices.', 'customfields-com') . '

'; } self::form('stop', $scope); self::end_form($scope === 'registration' ? __('Remove site registration', 'customfields-com') : __('Stop these emails', 'customfields-com')); } if ($scope === 'newsletter') { echo '

' . esc_html__('Stop these emails cancels or unsubscribes only a signup created by this request. For a subscription you already had, use the preferences or unsubscribe link in one of its emails.', 'customfields-com') . '

'; } echo '
'; } public static function page() { if (!self::allowed()) { return; } nocache_headers(); echo '

' . esc_html__('Your welcome choices', 'customfields-com') . '

' . esc_html__('Stay in touch, if you like. Both choices are optional.', 'customfields-com') . '

'; try { $state = self::state(); foreach (array('registration', 'newsletter') as $scope) { if (!empty($state[$scope])) { self::connection($scope, $state[$scope]); } } self::choices($state); } catch (\Throwable $error) { echo '

' . esc_html__('Saved choices could not be read. Restore database access before retrying. Your fields remain available.', 'customfields-com') . '

'; } echo '

' . esc_html__('Back to field groups', 'customfields-com') . '

'; } public static function dialog() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only suppression of duplicate optional UI on its dedicated page. if (!self::allowed() || !self::own_page() || self::progress() === 'complete' || self::scalar($_GET, 'page') === 'customfields-welcome') { return; } try { $state = self::state(); } catch (\Throwable $error) { return; } if (!empty($state['registration']) || !empty($state['newsletter'])) { return; } echo '

' . esc_html__('Your first save is ready. Stay in touch, if you like.', 'customfields-com') . '

' . esc_html__('Both choices are optional. Your work is already saved.', 'customfields-com') . '

'; self::choices($state); echo '
'; } public static function exporters($items) { $items['customfields-welcome'] = array('exporter_friendly_name' => __('CustomFields welcome choices', 'customfields-com'), 'callback' => array(__CLASS__, 'export_data')); return $items; } public static function erasers($items) { $items['customfields-welcome'] = array('eraser_friendly_name' => __('CustomFields welcome choices', 'customfields-com'), 'callback' => array(__CLASS__, 'erase_data')); return $items; } public static function export_data($email, $page = 1) { $state = self::state(); $record = $state['newsletter'] ?? array(); $data = array(); if (is_string($email) && $email !== '' && strcasecmp($record['payload']['newsletter']['email'] ?? '', $email) === 0) { $data[] = array('group_id' => 'customfields-welcome', 'group_label' => __('CustomFields welcome choices', 'customfields-com'), 'item_id' => 'customfields-welcome-email', 'data' => array(array('name' => __('Email address', 'customfields-com'), 'value' => $email), array('name' => __('Consent time', 'customfields-com'), 'value' => $record['consented_at'] ?? ''), array('name' => __('Consent version', 'customfields-com'), 'value' => self::CONSENT))); } return array('data' => $data, 'done' => true); } public static function erase_data($email, $page = 1) { $result = self::locked(static function() use ($email) { $state = self::state(); $record = $state['newsletter'] ?? array(); if (!is_string($email) || $email === '') { return false; } $match = strcasecmp($record['payload']['newsletter']['email'] ?? '', $email) === 0; $pending = !empty($record['withdraw']) && isset($record['email_hash']) && hash_equals($record['email_hash'], self::email_hash($email)); if (!$match && !$pending) { return false; } return self::withdraw_scope($state, 'newsletter'); }); return array('items_removed' => $result === true, 'items_retained' => is_wp_error($result), 'messages' => is_wp_error($result) ? array($result->get_error_message()) : array(), 'done' => true); } }