foreach (array('_fbp' => 'fbp', '_fbc' => 'fbc') as $cookie => $field) { if (!empty($_COOKIE[$cookie]) && is_string($_COOKIE[$cookie])) { $data[$field] = sanitize_text_field(wp_unslash($_COOKIE[$cookie])); } } return array_filter($data); } private static function server($order) { if (!self::eligible($order) || !self::consent() || NR_PMPRO_META_TOKEN === '') { return; } $browser_sent = (int) get_option(self::key($order->id, 'browser'), 0); if ($browser_sent && $browser_sent < time() - DAY_IN_SECONDS) { return; } $first_attempt = (int) get_option(self::key($order->id, 'event_time'), 0); // Ambiguous timeouts must not lead to retries outside the deduplication window. if ($first_attempt && $first_attempt < time() - DAY_IN_SECONDS) { return; } $sent_key = self::key($order->id, 'capi'); if (get_option($sent_key, false)) { return; } // Atomic lock prevents concurrent checkout/confirmation requests sending twice. $lock = self::key($order->id, 'lock'); $now = time(); if (!add_option($lock, $now, '', false)) { $old = (int) get_option($lock, 0); if ($old > 0 && $old < $now - 60) { delete_option($lock); } return; } try { if (get_option($sent_key, false)) { return; } $event = self::data($order); $time_key = self::key($order->id, 'event_time'); add_option($time_key, $now, '', false); $payload = array( 'data' => array(array( 'event_name' => $event['name'], 'event_time' => (int) get_option($time_key, $now), 'event_id' => $event['id'], 'action_source' => 'website', 'event_source_url' => pmpro_url('confirmation'), 'user_data' => self::user_data($order), 'custom_data' => $event['params'], )), ); if (NR_PMPRO_META_TEST_CODE !== '') { $payload['test_event_code'] = NR_PMPRO_META_TEST_CODE; } $response = wp_remote_post('https://graph.facebook.com/' . NR_PMPRO_META_API . '/' . NR_PMPRO_META_PIXEL . '/events', array( 'timeout' => 5, 'headers' => array('Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . NR_PMPRO_META_TOKEN), 'body' => wp_json_encode($payload), )); $body = is_wp_error($response) ? array() : json_decode(wp_remote_retrieve_body($response), true); $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); if ($code >= 200 && $code < 300 && !empty($body['events_received'])) { update_option($sent_key, time(), false); } else { // Never log token, payload, email, or raw API response. error_log('NR PMPro Meta: CAPI fehlgeschlagen; PMPro-Bestellung ' . absint($order->id) . ', HTTP ' . $code); } } finally { delete_option($lock); } } public static function footer() { if (!self::$browser || !self::consent()) { return; } $config = self::$browser; $config['pixel'] = (string) NR_PMPRO_META_PIXEL; $config['ajax'] = admin_url('admin-ajax.php'); $config['nonce'] = wp_create_nonce('nr_pmpro_meta_ack_' . $config['order']); echo ''; self::$browser = null; } public static function ack() { $id = isset($_POST['order']) ? absint($_POST['order']) : 0; check_ajax_referer('nr_pmpro_meta_ack_' . $id); if (!class_exists('MemberOrder') || !self::consent()) { wp_send_json_error(null, 403); } $order = new MemberOrder($id); if (!self::eligible($order) || (int) $order->user_id !== (int) get_current_user_id()) { wp_send_json_error(null, 403); } update_option(self::key($id, 'browser'), time(), false); wp_send_json_success(); } public static function notice() { if (current_user_can('manage_options') && NR_PMPRO_META_TOKEN === '') { echo '

NR PMPro Meta: CAPI-Token fehlt. Nur Browser-Tracking ist verfügbar. NR_PMPRO_META_TOKEN in wp-config.php setzen. Einwilligung muss über mconsent=1 signalisiert werden.

'; } } } NR_PMPro_Meta_Events::init(); } public static function cleanup() { if (!class_exists('ActionScheduler') || !class_exists('ActionScheduler_Store')) { self::save_result(0, 'Action Scheduler ist nicht verfügbar.'); return; } if (get_transient('nr_asc_cleanup_lock')) { return; } set_transient('nr_asc_cleanup_lock', 1, 10 * MINUTE_IN_SECONDS); $deleted = 0; $errors = 0; $started = microtime(true); $limited = false; try { $store = ActionScheduler::store(); // Process the less frequent statuses first to avoid starving them. $rules = array('failed' => 3, 'canceled' => 0, 'complete' => 0); foreach ($rules as $status => $days) { if ($deleted >= self::BATCH) { break; } if (microtime(true) - $started >= self::MAX_SECONDS) { $limited = true; break; } $query = array( 'status' => $status, 'orderby' => 'modified', 'order' => 'ASC', 'per_page' => self::BATCH - $deleted, ); // No retention period for completed/canceled actions. if ($days > 0) { $query['modified'] = new DateTime('-' . $days . ' days', new DateTimeZone('UTC')); $query['modified_compare'] = '<'; } $ids = $store->query_actions($query); foreach ($ids as $id) { if (microtime(true) - $started >= self::MAX_SECONDS) { $limited = true; break 2; } try { // An action may have been retried since the query. if ($store->get_status((int) $id) !== $status) { continue; } $store->delete_action((int) $id); ++$deleted; } catch (Throwable $e) { ++$errors; error_log('NR Action Scheduler Cleaner: ' . $e->getMessage()); } } } $message = $errors ? $errors . ' Fehler; siehe PHP-Fehlerprotokoll.' : 'OK'; if ($limited) { $message .= ' Zeitbudget erreicht; Fortsetzung beim nächsten Lauf.'; } elseif ($deleted >= self::BATCH) { $message .= ' Löschlimit erreicht; weitere alte Aktionen ggf. beim nächsten Lauf.'; } self::save_result($deleted, $message); } catch (Throwable $e) { self::save_result($deleted, 'Fehler; siehe PHP-Fehlerprotokoll.'); error_log('NR Action Scheduler Cleaner: ' . $e->getMessage()); } finally { delete_transient('nr_asc_cleanup_lock'); } } private static function save_result($count, $message) { update_option(self::OPTION, array('time' => current_time('mysql'), 'deleted' => (int) $count, 'message' => $message), false); } public static function widget() { if (current_user_can('manage_options')) { wp_add_dashboard_widget('nr_asc_dashboard', 'NR – Action Scheduler Cleaner', array(__CLASS__, 'render')); } } public static function render() { $last = get_option(self::OPTION, array()); echo '

Automatische Bereinigung: alle 12 Stunden (WP-Cron; abhängig vom Cron-Aufruf)

'; echo '

Abgeschlossen/Abgebrochen: keine Aufbewahrung · Fehlgeschlagen: 3 Tage
Maximal ' . esc_html(self::BATCH) . ' Aktionen je Durchlauf; Zeitbudget ' . esc_html(self::MAX_SECONDS) . ' Sekunden.

'; if (!empty($last)) { echo '

Letzter Lauf: ' . esc_html($last['time']) . '
'; echo 'Gelöscht: ' . esc_html($last['deleted']) . '
'; echo 'Status: ' . esc_html($last['message']) . '

'; } else { echo '

Noch keine Bereinigung ausgeführt.

'; } $next = wp_next_scheduled(self::CRON); if ($next) { echo '

Nächster Lauf: ' . esc_html(wp_date('d.m.Y H:i', $next)) . '

'; } echo '
'; echo ''; wp_nonce_field(self::NONCE); submit_button('Jetzt bereinigen (max. ' . self::BATCH . ')', 'secondary', 'submit', false); echo '
'; if (isset($_GET['nr_asc_done'])) { echo '

Bereinigung ausgeführt. Ergebnis siehe oben.

'; } } public static function manual() { if (!current_user_can('manage_options')) { wp_die('Keine Berechtigung.'); } check_admin_referer(self::NONCE); self::cleanup(); wp_safe_redirect(add_query_arg('nr_asc_done', '1', admin_url('index.php'))); exit; } } NR_Action_Scheduler_Cleaner::init(); register_deactivation_hook(__FILE__, function () { wp_clear_scheduled_hook(NR_Action_Scheduler_Cleaner::CRON); });