Settings. * * @package GeniusCommerce */ namespace GeniusCommerce; defined( 'ABSPATH' ) || exit; /** * GC_Settings class. * * Handles the plugin settings page in WooCommerce admin. */ class GC_Settings { /** * Constructor. */ public function __construct() { add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_tab' ), 50 ); add_action( 'woocommerce_settings_tabs_genius_commerce', array( $this, 'settings_tab' ) ); add_action( 'woocommerce_update_options_genius_commerce', array( $this, 'update_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); // Vangnet: een winkel die gekoppeld is maar geen webhooks heeft, // herstelt zichzelf zodra een beheerder het dashboard opent. add_action( 'admin_init', array( GC_Webhook_Manager::class, 'maybe_schedule_bootstrap' ) ); // AJAX handlers. add_action( 'wp_ajax_genius_commerce_connect_store', array( $this, 'ajax_connect_store' ) ); add_action( 'wp_ajax_genius_commerce_disconnect_store', array( $this, 'ajax_disconnect_store' ) ); add_action( 'wp_ajax_genius_commerce_test_connection', array( $this, 'ajax_test_connection' ) ); add_action( 'wp_ajax_genius_commerce_update_webhook_status', array( $this, 'ajax_update_webhook_status' ) ); add_action( 'wp_ajax_genius_commerce_update_webhook_group', array( $this, 'ajax_update_webhook_group' ) ); add_action( 'wp_ajax_genius_commerce_refresh_webhooks', array( $this, 'ajax_refresh_webhooks' ) ); // Template editor AJAX handlers. add_action( 'wp_ajax_genius_commerce_save_template', array( $this, 'ajax_save_template' ) ); add_action( 'wp_ajax_genius_commerce_reset_template', array( $this, 'ajax_reset_template' ) ); // Story 8.3: force-refresh the scope cache on demand (helps tenant- // admins debug "why is this endpoint 403?" without waiting for the // 5-minute TTL). add_action( 'wp_ajax_genius_commerce_refresh_scopes', array( $this, 'ajax_refresh_scopes' ) ); } /** * Add Genius Commerce tab to WooCommerce settings. * * @param array $tabs Existing tabs. * @return array Modified tabs. */ public function add_settings_tab( array $tabs ): array { $tabs['genius_commerce'] = __( 'Genius Commerce', 'genius-commerce' ); return $tabs; } /** * Get available subtabs. * * @return array Subtabs configuration. */ private function get_subtabs(): array { return array( 'connection' => __( 'Verbinding', 'genius-commerce' ), 'display' => __( 'Weergave', 'genius-commerce' ), 'webhooks' => __( 'Webhook Configuratie', 'genius-commerce' ), 'scopes' => __( 'Scopes', 'genius-commerce' ), 'templates' => __( 'Template Aanpassingen', 'genius-commerce' ), 'mapping' => __( 'Sync Mapping', 'genius-commerce' ), ); } /** * Get the current subtab. * * @return string Current subtab slug. */ private function get_current_subtab(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $subtab = isset( $_GET['subtab'] ) ? sanitize_text_field( wp_unslash( $_GET['subtab'] ) ) : 'connection'; $valid_subtabs = array_keys( $this->get_subtabs() ); return in_array( $subtab, $valid_subtabs, true ) ? $subtab : 'connection'; } /** * Render subtab navigation. * * @return void */ private function render_subtab_navigation(): void { $subtabs = $this->get_subtabs(); $current_subtab = $this->get_current_subtab(); $base_url = admin_url( 'admin.php?page=wc-settings&tab=genius_commerce' ); echo ''; } /** * Render the settings tab content. * * @return void */ public function settings_tab(): void { $current_subtab = $this->get_current_subtab(); // Always show subtab navigation. $this->render_subtab_navigation(); // Warn loud when debug-logging is enabled — log-bestanden bevatten // (geredacteerde) request-data en horen niet maandenlang aan te staan. $this->render_logging_banner(); // Render content based on current subtab. switch ( $current_subtab ) { case 'connection': $this->render_connect_cta(); $this->render_status_indicator(); woocommerce_admin_fields( $this->get_connection_settings() ); break; case 'display': woocommerce_admin_fields( $this->get_display_settings() ); break; case 'webhooks': $this->render_webhook_config(); break; case 'scopes': $this->render_scopes_section(); break; case 'templates': $this->render_template_editor_section(); break; case 'mapping': $this->render_mapping_section(); break; } } /** * Save settings. * * @return void */ public function update_settings(): void { // Handle API key separately (custom validation). // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce handles nonce. if ( isset( $_POST['genius_commerce_api_key'] ) && ! empty( $_POST['genius_commerce_api_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $api_key = sanitize_text_field( wp_unslash( $_POST['genius_commerce_api_key'] ) ); $error = GC_API_Key::get_validation_error( $api_key ); if ( $error ) { \WC_Admin_Settings::add_error( $error ); } else { GC_API_Key::save( $api_key ); \WC_Admin_Settings::add_message( __( 'API key opgeslagen.', 'genius-commerce' ) ); } } // Handle backend URL validation. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['genius_commerce_backend_url'] ) && ! empty( $_POST['genius_commerce_backend_url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $url = esc_url_raw( wp_unslash( $_POST['genius_commerce_backend_url'] ) ); if ( ! empty( $url ) && strpos( $url, 'https://' ) !== 0 ) { \WC_Admin_Settings::add_error( __( 'Backend URL moet beginnen met https://', 'genius-commerce' ) ); // Don't save invalid URL. // phpcs:ignore WordPress.Security.NonceVerification.Missing unset( $_POST['genius_commerce_backend_url'] ); } } // Handle IP allowlist validation. Empty value is allowed (= disabled). // Any non-empty value is split on commas/whitespace; each entry must // parse as IPv4/IPv6 or `ip/prefix`. On any invalid entry we name it // in the error and drop the entire field so the old value survives. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST[ GC_IP_Allowlist::OPTION_ALLOWED_IPS ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $raw = sanitize_textarea_field( wp_unslash( $_POST[ GC_IP_Allowlist::OPTION_ALLOWED_IPS ] ) ); $invalid = array(); if ( '' !== trim( $raw ) ) { $tokens = preg_split( '/[\s,]+/', $raw ) ?: array(); foreach ( $tokens as $token ) { $token = trim( $token ); if ( '' === $token ) { continue; } if ( ! GC_IP_Allowlist::validate( $token ) ) { $invalid[] = $token; } } } if ( ! empty( $invalid ) ) { \WC_Admin_Settings::add_error( sprintf( /* translators: %s: comma-separated list of invalid IP/CIDR entries */ __( 'Toegestane IP-adressen: ongeldige invoer geweigerd (%s). Gebruik IPv4/IPv6-adressen of CIDR-notatie (bv. 10.0.0.0/8).', 'genius-commerce' ), implode( ', ', $invalid ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing unset( $_POST[ GC_IP_Allowlist::OPTION_ALLOWED_IPS ] ); } } // Save standard WooCommerce options. woocommerce_update_options( $this->get_settings() ); } /** * Get all settings (for backward compatibility with update_settings). * * @return array All settings configuration. */ public function get_settings(): array { return array_merge( $this->get_connection_settings(), $this->get_display_settings() ); } /** * Get connection settings array. * * @return array Connection settings configuration. */ public function get_connection_settings(): array { $api_configured = GC_API_Key::is_configured(); return array( array( 'title' => __( 'Geavanceerd: handmatige verbinding', 'genius-commerce' ), 'type' => 'title', 'desc' => __( 'Alleen nodig als je niet de "Verbinden"-knop hierboven gebruikt. Haal dan zelf een API key op uit je Genius Commerce account en plak deze hieronder.', 'genius-commerce' ), 'id' => 'genius_commerce_settings_section', ), array( 'title' => __( 'API Key', 'genius-commerce' ), 'type' => 'password', 'id' => 'genius_commerce_api_key', 'default' => '', 'placeholder' => $api_configured ? GC_API_Key::get_masked() : __( 'Voer de API key in van Genius Commerce', 'genius-commerce' ), 'desc' => $api_configured ? __( 'Key is geconfigureerd. Laat leeg om huidige key te behouden, of voer nieuwe key in om te wijzigen.', 'genius-commerce' ) : __( 'Kopieer de API key uit Genius Commerce en plak deze hier.', 'genius-commerce' ), 'desc_tip' => true, 'custom_attributes' => array( 'autocomplete' => 'off', 'data-configured' => $api_configured ? 'true' : 'false', ), ), array( 'title' => __( 'Backend URL', 'genius-commerce' ), 'type' => 'url', 'id' => 'genius_commerce_backend_url', 'desc' => __( 'Genius Commerce API URL', 'genius-commerce' ), 'default' => GC_API_Client::DEFAULT_BACKEND_URL, ), array( 'title' => __( 'Debug logging', 'genius-commerce' ), 'type' => 'checkbox', 'id' => 'genius_commerce_enable_logging', 'desc' => __( 'Log API-calls en errors naar WooCommerce-logs', 'genius-commerce' ), // Longer description rendered below the checkbox. 'desc_tip' => __( 'Logs landen in wp-content/uploads/wc-logs/. Alleen tijdelijk aanzetten voor debugging; log-bestanden kunnen op nginx-hosts publiek toegankelijk zijn als de webserver .htaccess niet evalueert. Klant-PII (e-mail, NAW, telefoon) wordt automatisch geredacteerd, maar overige debug-data niet.', 'genius-commerce' ), 'default' => 'no', ), array( 'title' => __( 'Toegestane IP-adressen', 'genius-commerce' ), 'type' => 'textarea', 'id' => GC_IP_Allowlist::OPTION_ALLOWED_IPS, 'desc' => __( 'Comma- of nieuwe-regel-gescheiden lijst van IPv4/IPv6-adressen of CIDR-ranges die toegang krijgen tot de REST-API. Laat leeg om de allowlist uit te schakelen (alleen API-key-check). Productie moet altijd het Genius Commerce egress-IP bevatten. Loopback (127.0.0.1, ::1) wordt NIET automatisch toegestaan — voeg het expliciet toe voor lokale tests.', 'genius-commerce' ), 'desc_tip' => true, 'default' => '', 'placeholder' => '34.241.0.0/16, 203.0.113.5', 'css' => 'min-width: 480px; min-height: 80px;', ), array( 'title' => __( 'Vertrouw X-Forwarded-For', 'genius-commerce' ), 'type' => 'checkbox', 'id' => GC_IP_Allowlist::OPTION_TRUST_XFF, 'desc' => __( 'Alleen aanzetten als WordPress achter een vertrouwde reverse-proxy draait (Cloudflare, AWS ALB, eigen nginx). Anders is dit een spoofing-risico — een aanvaller kan de header zelf zetten.', 'genius-commerce' ), 'default' => 'no', ), array( 'type' => 'sectionend', 'id' => 'genius_commerce_settings_section', ), array( 'title' => __( 'Scope-enforcement', 'genius-commerce' ), 'type' => 'title', 'desc' => __( 'Beperk REST-routes tot wat in de Genius Commerce-configuratie staat. Zet pas aan na verificatie in het tabblad "Scopes".', 'genius-commerce' ), 'id' => 'genius_commerce_scopes_section', ), array( 'title' => __( 'Scope-enforcement aan', 'genius-commerce' ), 'type' => 'checkbox', 'id' => GC_Scopes::FLAG_OPTION, 'desc' => __( 'Wijst REST-calls af die buiten de scope van de tenant-configuratie vallen (403 INSUFFICIENT_SCOPE). Laat dit uit zolang de scopes nog niet zijn geverifieerd in het tabblad "Scopes".', 'genius-commerce' ), 'default' => 'no', ), array( 'type' => 'sectionend', 'id' => 'genius_commerce_scopes_section', ), ); } /** * Get display settings array. * * @return array Display settings configuration. */ public function get_display_settings(): array { return array( // Discount Display Settings. array( 'title' => __( 'Korting Weergave', 'genius-commerce' ), 'type' => 'title', 'desc' => __( 'Instellingen voor het weergeven van klantkorting op product pagina\'s.', 'genius-commerce' ), 'id' => 'genius_commerce_discount_section', ), array( 'title' => __( 'Korting weergeven', 'genius-commerce' ), 'type' => 'checkbox', 'id' => 'genius_commerce_enable_discount_display', 'desc' => __( 'Toon gepersonaliseerde klantkorting op product pagina\'s voor ingelogde gebruikers', 'genius-commerce' ), 'default' => 'yes', ), array( 'type' => 'sectionend', 'id' => 'genius_commerce_discount_section', ), // Delivery Display Settings. array( 'title' => __( 'Levertijd Weergave', 'genius-commerce' ), 'type' => 'title', 'desc' => __( 'Instellingen voor het weergeven van levertijd informatie op product pagina\'s.', 'genius-commerce' ), 'id' => 'genius_commerce_delivery_section', ), array( 'title' => __( 'Levertijd weergeven', 'genius-commerce' ), 'type' => 'checkbox', 'id' => 'genius_commerce_enable_delivery_display', 'desc' => __( 'Toon verwachte levertijd op product pagina\'s', 'genius-commerce' ), 'default' => 'yes', ), array( 'title' => __( 'Standaard levertijd', 'genius-commerce' ), 'type' => 'text', 'id' => 'genius_commerce_default_delivery_time', 'desc' => __( 'Wordt getoond als een product geen specifieke levertijd heeft. Laat leeg om niets te tonen.', 'genius-commerce' ), 'default' => '', 'placeholder' => __( 'bijv. 2-3 werkdagen', 'genius-commerce' ), ), array( 'title' => __( 'Positie', 'genius-commerce' ), 'type' => 'select', 'id' => 'genius_commerce_delivery_position', 'desc' => __( 'Waar de levertijd wordt getoond op de product pagina.', 'genius-commerce' ), 'default' => 'after_price', 'options' => array( 'after_price' => __( 'Na de prijs', 'genius-commerce' ), 'after_excerpt' => __( 'Na de korte beschrijving', 'genius-commerce' ), 'manual' => __( 'Handmatig (alleen via shortcode)', 'genius-commerce' ), ), ), array( 'type' => 'sectionend', 'id' => 'genius_commerce_delivery_section', ), ); } /** * Render warning banner when `genius_commerce_enable_logging` is on. * * Story 8.5 / F-004: debug-logging schrijft (geredacteerde) request-data * naar `wc-logs/`. Op nginx-hosts kan die directory publiek bereikbaar * zijn; vandaar deze blijvende reminder om logging na debugging weer uit * te zetten via de toggle in de Verbinding-tab. * * @return void */ private function render_logging_banner(): void { if ( 'yes' !== get_option( 'genius_commerce_enable_logging', 'no' ) ) { return; } ?>

__( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } $backend_url = GC_API_Client::get_backend_url(); if ( '' === $backend_url || 0 !== strpos( $backend_url, 'https://' ) ) { wp_send_json_error( array( 'message' => __( 'Backend URL ontbreekt of is geen https-URL. Vul deze eerst in bij de geavanceerde instellingen.', 'genius-commerce' ) ) ); } // Mint a fresh 32-hex plugin key and persist it BEFORE the backend callback, // so the backend's ownership callback (X-GC-API-Key) can validate against it. try { $api_key = bin2hex( random_bytes( 16 ) ); } catch ( \Exception $e ) { wp_send_json_error( array( 'message' => __( 'Kon geen veilige sleutel genereren.', 'genius-commerce' ) ) ); } if ( ! GC_API_Key::save( $api_key ) ) { wp_send_json_error( array( 'message' => __( 'Kon de sleutel niet opslaan.', 'genius-commerce' ) ) ); } $handshake_nonce = wp_generate_password( 32, false ); set_transient( 'genius_commerce_connect_nonce', $handshake_nonce, 5 * MINUTE_IN_SECONDS ); $response = wp_remote_post( $backend_url . '/token-exchange', array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', ), 'body' => wp_json_encode( array( 'storeUrl' => untrailingslashit( site_url() ), 'apiKey' => $api_key, 'timestamp' => (int) round( microtime( true ) * 1000 ), 'nonce' => $handshake_nonce, ) ), ) ); if ( is_wp_error( $response ) ) { GC_API_Key::delete(); wp_send_json_error( array( 'message' => __( 'Kon Genius Commerce niet bereiken. Probeer het opnieuw.', 'genius-commerce' ) ) ); } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code < 200 || $code >= 300 || empty( $data['redirectUrl'] ) ) { // Roll back the key so the CTA reappears and the store isn't left half-coupled. GC_API_Key::delete(); $message = is_array( $data ) && isset( $data['error'] ) ? (string) $data['error'] : __( 'Verbinden mislukt. Controleer of de winkel publiek bereikbaar is.', 'genius-commerce' ); wp_send_json_error( array( 'message' => $message ) ); } // Persist the URL we just proved to work, so the status-overzicht and the // geavanceerde velden show the same backend the plugin actually calls. if ( '' === trim( (string) get_option( 'genius_commerce_backend_url', '' ) ) ) { update_option( 'genius_commerce_backend_url', $backend_url ); } // Key verified by the backend round-trip — record local connection status. update_option( 'genius_commerce_connection_status', 'connected' ); update_option( 'genius_commerce_last_connection_check', time() ); // The backend config only exists once the merchant finishes linking in // Genius Commerce, so the webhook secret cannot be fetched here yet. // Clear the throttle instead: the first admin page load or backend call // after linking then registers the webhooks without any manual step. GC_Webhook_Manager::clear_bootstrap_throttle(); wp_send_json_success( array( 'redirectUrl' => (string) $data['redirectUrl'] ) ); } /** * AJAX handler for disconnecting the store (Story 4.11). * * Wipes the local coupling state so the "Verbinden"-CTA reappears: deletes the * stored API key, removes the webhook secret, and unregisters the WooCommerce * webhooks. Needed because a backend-side disconnect leaves the plugin showing * "Gekoppeld" with no UI to clear the {@see GC_API_Key::OPTION_KEY} option. * * @return void */ public function ajax_disconnect_store(): void { if ( ! check_ajax_referer( 'genius_commerce_disconnect_store', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // Unregister the WooCommerce webhooks (and clear the stored webhook IDs) // before we drop the key, so the deletions still authenticate locally. $webhook_manager = new GC_Webhook_Manager(); $removed = $webhook_manager->cleanup_webhooks(); // Drop the webhook secret and the API key so is_configured() → false. delete_option( 'genius_commerce_webhook_secret' ); GC_API_Key::delete(); // Reset the derived connection status so the status-overview isn't stale. update_option( 'genius_commerce_connection_status', 'disconnected' ); delete_option( 'genius_commerce_last_connection_check' ); // Without the key there is nothing to heal; a stale throttle would only // delay the registration after the next "Verbinden". GC_Webhook_Manager::clear_bootstrap_throttle(); wp_send_json_success( array( 'message' => __( 'Koppeling verbroken. Je kunt opnieuw verbinden.', 'genius-commerce' ), 'webhooks_removed' => $removed, ) ); } /** * Render status indicator at top of settings page. * * @return void */ private function render_status_indicator(): void { $api_configured = GC_API_Key::is_configured(); $backend_configured = '' !== GC_API_Client::get_backend_url(); $connection_status = get_option( 'genius_commerce_connection_status', 'unknown' ); $last_check = get_option( 'genius_commerce_last_connection_check', 0 ); $can_test = $api_configured && $backend_configured; $has_webhook_secret = GC_API_Client::has_webhook_secret(); $webhook_count = GC_Webhook_Manager::get_webhook_count(); $has_webhooks = GC_Webhook_Manager::has_registered_webhooks(); // Determine connection status class and text. $connection_class = 'gc-status-unknown'; $connection_icon = 'dashicons-warning'; $connection_text = __( 'Niet getest', 'genius-commerce' ); if ( 'connected' === $connection_status ) { $connection_class = 'gc-status-ok'; $connection_icon = 'dashicons-yes-alt'; $connection_text = __( 'Verbonden', 'genius-commerce' ); if ( $last_check ) { $connection_text .= ' (' . human_time_diff( $last_check ) . ' ' . __( 'geleden', 'genius-commerce' ) . ')'; } } elseif ( 'disconnected' === $connection_status ) { $connection_class = 'gc-status-error'; $connection_icon = 'dashicons-dismiss'; $connection_text = __( 'Niet verbonden', 'genius-commerce' ); } // Determine webhook secret status. $secret_class = $has_webhook_secret ? 'gc-status-ok' : 'gc-status-missing'; $secret_icon = $has_webhook_secret ? 'dashicons-yes-alt' : 'dashicons-warning'; $secret_text = $has_webhook_secret ? __( 'Opgehaald', 'genius-commerce' ) : __( 'Niet beschikbaar', 'genius-commerce' ); // Determine webhooks registration status. $webhooks_class = $has_webhooks ? 'gc-status-ok' : 'gc-status-missing'; $webhooks_icon = $has_webhooks ? 'dashicons-yes-alt' : 'dashicons-warning'; $webhooks_text = $has_webhooks ? sprintf( /* translators: %d: number of registered webhooks */ _n( '%d webhook geregistreerd', '%d webhooks geregistreerd', $webhook_count, 'genius-commerce' ), $webhook_count ) : __( 'Niet geregistreerd', 'genius-commerce' ); ?>

get_registered_webhooks(); $problematic = $webhook_manager->get_problematic_webhooks(); $has_problems = ! empty( $problematic ); $paused_count = count( array_filter( $problematic, fn( $w ) => 'paused' === $w['status'] ) ); $with_failures_count = count( array_filter( $problematic, fn( $w ) => $w['failure_count'] > 0 ) ); // Build lookup by topic. $webhook_by_topic = array(); foreach ( $webhooks as $webhook ) { $webhook_by_topic[ $webhook['topic'] ] = $webhook; } ?>

0 ) : ?> 0 ) : ?>

$topics ) : ?>

get_webhook_status_text( $status ); $status_class = $this->get_webhook_status_class( $status ); $failure_count = $webhook ? $webhook['failure_count'] : 0; $date_modified = $webhook && $webhook['date_modified'] ? $webhook['date_modified'] : null; $webhook_id = $webhook ? $webhook['id'] : 0; ?>
format_webhook_date( $date_modified ) ); ?> - 0 ) : ?> 0
diff( $datetime ); // If less than 1 hour ago, show relative time. if ( $diff->days === 0 && $diff->h === 0 ) { if ( $diff->i === 0 ) { return __( 'Zojuist', 'genius-commerce' ); } return sprintf( /* translators: %d: number of minutes */ _n( '%d minuut geleden', '%d minuten geleden', $diff->i, 'genius-commerce' ), $diff->i ); } // If less than 24 hours ago, show hours. if ( $diff->days === 0 ) { return sprintf( /* translators: %d: number of hours */ _n( '%d uur geleden', '%d uur geleden', $diff->h, 'genius-commerce' ), $diff->h ); } // Otherwise show date and time. return wp_date( 'j M Y, H:i', $datetime->getTimestamp() ); } catch ( \Exception $e ) { return $date; } } /** * Get human-readable status text. * * @param string $status Webhook status. * @return string Status text. */ private function get_webhook_status_text( string $status ): string { $texts = array( 'active' => __( 'Actief', 'genius-commerce' ), 'paused' => __( 'Gepauzeerd', 'genius-commerce' ), 'disabled' => __( 'Uitgeschakeld', 'genius-commerce' ), 'missing' => __( 'Ontbreekt', 'genius-commerce' ), ); return $texts[ $status ] ?? $status; } /** * Get CSS class for status badge. * * @param string $status Webhook status. * @return string CSS class. */ private function get_webhook_status_class( string $status ): string { $classes = array( 'active' => 'gc-badge-active', 'paused' => 'gc-badge-paused', 'disabled' => 'gc-badge-disabled', 'missing' => 'gc-badge-missing', ); return $classes[ $status ] ?? 'gc-badge-unknown'; } /** * Enqueue admin scripts and styles. * * @param string $hook Current admin page hook. * @return void */ public function enqueue_admin_scripts( string $hook ): void { // Only load on WooCommerce settings page. if ( 'woocommerce_page_wc-settings' !== $hook ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; // Only load on our tab. if ( 'genius_commerce' !== $tab ) { return; } wp_enqueue_style( 'genius-commerce-admin', GENIUS_COMMERCE_PLUGIN_URL . 'admin/css/admin.css', array(), GENIUS_COMMERCE_VERSION ); wp_enqueue_script( 'genius-commerce-admin', GENIUS_COMMERCE_PLUGIN_URL . 'admin/js/admin.js', array( 'jquery' ), GENIUS_COMMERCE_VERSION, true ); // Enqueue CodeMirror for template editing. $code_editor_settings = wp_enqueue_code_editor( array( 'type' => 'text/html' ) ); // Localize script with AJAX data. wp_localize_script( 'genius-commerce-admin', 'geniusCommerceAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'genius_commerce_test_connection' ), 'connectNonce' => wp_create_nonce( 'genius_commerce_connect_store' ), 'disconnectNonce' => wp_create_nonce( 'genius_commerce_disconnect_store' ), 'webhookNonce' => wp_create_nonce( 'genius_commerce_webhook_action' ), 'templateNonce' => wp_create_nonce( 'genius_commerce_template_action' ), 'mappingNonce' => wp_create_nonce( 'genius_commerce_mapping_action' ), 'codeEditor' => $code_editor_settings, 'i18n' => array( 'connecting' => __( 'Verbinden...', 'genius-commerce' ), 'connectError' => __( 'Verbinden mislukt', 'genius-commerce' ), 'disconnect' => __( 'Ontkoppelen', 'genius-commerce' ), 'disconnecting' => __( 'Ontkoppelen...', 'genius-commerce' ), 'disconnectConfirm' => __( 'Weet je zeker dat je de koppeling met Genius Commerce wilt verbreken? De lokale sleutel en webhooks worden verwijderd. Je kunt daarna opnieuw verbinden.', 'genius-commerce' ), 'disconnectError' => __( 'Ontkoppelen mislukt', 'genius-commerce' ), 'testing' => __( 'Verbinding testen...', 'genius-commerce' ), 'connected' => __( 'Verbonden', 'genius-commerce' ), 'error' => __( 'Fout', 'genius-commerce' ), 'testAgain' => __( 'Test opnieuw', 'genius-commerce' ), 'updating' => __( 'Bijwerken...', 'genius-commerce' ), 'active' => __( 'Actief', 'genius-commerce' ), 'paused' => __( 'Gepauzeerd', 'genius-commerce' ), 'activate' => __( 'Activeren', 'genius-commerce' ), 'pause' => __( 'Pauzeren', 'genius-commerce' ), 'saving' => __( 'Opslaan...', 'genius-commerce' ), 'saved' => __( 'Opgeslagen', 'genius-commerce' ), 'save' => __( 'Opslaan', 'genius-commerce' ), 'resetting' => __( 'Resetten...', 'genius-commerce' ), 'resetConfirm' => __( 'Weet je zeker dat je deze template wilt resetten naar de standaard?', 'genius-commerce' ), 'templateSaved' => __( 'Template opgeslagen', 'genius-commerce' ), 'templateReset' => __( 'Template gereset naar standaard', 'genius-commerce' ), 'mappingSaved' => __( 'Mapping opgeslagen', 'genius-commerce' ), 'mappingSave' => __( 'Mapping Opslaan', 'genius-commerce' ), 'refreshing' => __( 'Bezig met verversen...', 'genius-commerce' ), 'refreshFailed' => __( 'Verversen mislukt', 'genius-commerce' ), 'networkError' => __( 'Netwerkfout', 'genius-commerce' ), ), ) ); } /** * AJAX handler for connection test. * * @return void */ public function ajax_test_connection(): void { try { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_test_connection', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // Clear cache for fresh test. GC_API_Client::clear_connection_cache(); // Test connection. $client = new GC_API_Client(); $result = $client->test_connection( true ); if ( $result['success'] ) { // Store connection status. update_option( 'genius_commerce_connection_status', 'connected' ); update_option( 'genius_commerce_last_connection_check', time() ); // Fetch the secret and register the webhooks — the same path the // self-healing cron uses, so both routes can't drift apart. GC_Webhook_Manager::clear_bootstrap_throttle(); $webhook_result = GC_Webhook_Manager::run_bootstrap(); $webhook_secret_fetched = GC_API_Client::has_webhook_secret(); $webhooks_registered = $webhook_result['registered']; $webhook_errors = $webhook_result['errors'] ?? array(); wp_send_json_success( array( 'message' => __( 'Verbinding succesvol!', 'genius-commerce' ), 'status' => 'connected', 'webhook_secret_status' => $webhook_secret_fetched ? 'fetched' : 'failed', 'webhooks_registered' => $webhooks_registered, 'webhook_errors' => $webhook_errors, ) ); } else { // Store disconnected status. update_option( 'genius_commerce_connection_status', 'disconnected' ); wp_send_json_error( array( 'message' => $result['error'] ?? __( 'Onbekende fout', 'genius-commerce' ), 'status' => 'disconnected', ) ); } } catch ( \Throwable $e ) { Genius_Commerce::log( 'Connection test exception: ' . $e->getMessage(), 'error' ); wp_send_json_error( array( /* translators: %s: PHP exception message. */ 'message' => sprintf( __( 'PHP fout: %s', 'genius-commerce' ), $e->getMessage() ), 'status' => 'error', ) ); } } /** * AJAX handler for updating individual webhook status. * * @return void */ public function ajax_update_webhook_status(): void { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_webhook_action', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // Get and validate parameters. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified above. $topic = isset( $_POST['topic'] ) ? sanitize_text_field( wp_unslash( $_POST['topic'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; if ( empty( $topic ) || empty( $status ) ) { wp_send_json_error( array( 'message' => __( 'Ontbrekende parameters', 'genius-commerce' ) ) ); } $webhook_manager = new GC_Webhook_Manager(); $success = $webhook_manager->update_webhook_status( $topic, $status ); if ( $success ) { wp_send_json_success( array( 'message' => sprintf( /* translators: 1: topic, 2: status */ __( 'Webhook %1$s is nu %2$s', 'genius-commerce' ), GC_Webhook_Manager::TOPIC_LABELS[ $topic ] ?? $topic, 'active' === $status ? __( 'actief', 'genius-commerce' ) : __( 'gepauzeerd', 'genius-commerce' ) ), 'topic' => $topic, 'status' => $status, ) ); } else { wp_send_json_error( array( 'message' => __( 'Kon webhook status niet bijwerken', 'genius-commerce' ) ) ); } } /** * AJAX handler for updating webhook group status. * * @return void */ public function ajax_update_webhook_group(): void { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_webhook_action', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // Get and validate parameters. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified above. $group = isset( $_POST['group'] ) ? sanitize_text_field( wp_unslash( $_POST['group'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; if ( empty( $group ) || empty( $status ) ) { wp_send_json_error( array( 'message' => __( 'Ontbrekende parameters', 'genius-commerce' ) ) ); } $webhook_manager = new GC_Webhook_Manager(); $updated = $webhook_manager->update_group_status( $group, $status ); if ( $updated > 0 ) { wp_send_json_success( array( 'message' => sprintf( /* translators: 1: count, 2: group name */ __( '%1$d webhooks in %2$s bijgewerkt', 'genius-commerce' ), $updated, GC_Webhook_Manager::GROUP_LABELS[ $group ] ?? $group ), 'group' => $group, 'status' => $status, 'updated' => $updated, ) ); } else { wp_send_json_error( array( 'message' => __( 'Geen webhooks bijgewerkt', 'genius-commerce' ) ) ); } } /** * AJAX handler for refreshing webhook status. * * @return void */ public function ajax_refresh_webhooks(): void { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_webhook_action', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } $webhook_manager = new GC_Webhook_Manager(); $webhooks = $webhook_manager->get_registered_webhooks(); $problematic = $webhook_manager->get_problematic_webhooks(); // Format dates for display. $formatted_webhooks = array(); foreach ( $webhooks as $webhook ) { $webhook['date_modified_formatted'] = $webhook['date_modified'] ? $this->format_webhook_date( $webhook['date_modified'] ) : '-'; $webhook['admin_url'] = GC_Webhook_Manager::get_webhook_admin_url( $webhook['id'] ); $formatted_webhooks[ $webhook['topic'] ] = $webhook; } wp_send_json_success( array( 'webhooks' => $formatted_webhooks, 'has_problems' => ! empty( $problematic ), 'problematic_count' => count( $problematic ), 'message' => __( 'Webhook status vernieuwd', 'genius-commerce' ), ) ); } /** * Render template editor section. * * @return void */ private function render_template_editor_section(): void { $template_engine = new GC_Template_Engine(); $templates = array( 'discount-price.php' => array( 'title' => __( 'Korting Weergave', 'genius-commerce' ), 'description' => __( 'Template voor korting weergave op product pagina\'s.', 'genius-commerce' ), 'placeholders' => array( 'original_price' => __( 'Originele prijs (geformatteerd)', 'genius-commerce' ), 'discount_price' => __( 'Kortingsprijs (geformatteerd)', 'genius-commerce' ), 'discount_percentage' => __( 'Kortingspercentage (bijv. "20%")', 'genius-commerce' ), 'discount_amount' => __( 'Kortingsbedrag (geformatteerd)', 'genius-commerce' ), 'reason' => __( 'Reden voor korting', 'genius-commerce' ), ), ), 'delivery-product.php' => array( 'title' => __( 'Levertijd (Product)', 'genius-commerce' ), 'description' => __( 'Template voor levertijd weergave op product pagina\'s.', 'genius-commerce' ), 'placeholders' => array( 'delivery_time' => __( 'Levertijd tekst (bijv. "2-3 werkdagen")', 'genius-commerce' ), 'delivery_date' => __( 'Verwachte leverdatum', 'genius-commerce' ), 'icon' => __( 'Levertijd icoon HTML', 'genius-commerce' ), ), ), 'delivery-cart.php' => array( 'title' => __( 'Levertijd (Cart)', 'genius-commerce' ), 'description' => __( 'Template voor levertijd in cart/checkout (via shortcode).', 'genius-commerce' ), 'placeholders' => array( 'prefix' => __( 'Tekst voor de datum', 'genius-commerce' ), 'delivery_date' => __( 'Verwachte leverdatum', 'genius-commerce' ), ), ), ); ?>

$config ) : ?> get_template_source( $template_name ); $is_theme_override = 'theme' === $template_source; $theme_path = $is_theme_override ? $template_engine->get_theme_template_path( $template_name ) : ''; $custom_content = $template_engine->get_custom_template( $template_name ); $default_content = $template_engine->get_default_template_content( $template_name ); $current_content = $custom_content ?? $default_content; $is_custom = null !== $custom_content; $template_key = sanitize_key( str_replace( '.php', '', $template_name ) ); ?>

' . esc_html( $theme_path ) . '' ); ?>

$desc ) : ?> {{}}
render_content(); } /** * AJAX handler for saving a template. * * @return void */ public function ajax_save_template(): void { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_template_action', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above. $template = isset( $_POST['template'] ) ? sanitize_file_name( wp_unslash( $_POST['template'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above. $content = isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : ''; if ( empty( $template ) ) { wp_send_json_error( array( 'message' => __( 'Template naam ontbreekt', 'genius-commerce' ) ) ); } // Validate template name. $valid_templates = array( 'discount-price.php', 'delivery-product.php', 'delivery-cart.php' ); if ( ! in_array( $template, $valid_templates, true ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige template', 'genius-commerce' ) ) ); } $template_engine = new GC_Template_Engine(); $success = $template_engine->save_custom_template( $template, $content ); if ( $success ) { wp_send_json_success( array( 'message' => __( 'Template opgeslagen', 'genius-commerce' ), 'is_custom' => ! empty( trim( $content ) ), ) ); } else { wp_send_json_error( array( 'message' => __( 'Opslaan mislukt', 'genius-commerce' ) ) ); } } /** * AJAX handler for resetting a template to default. * * @return void */ public function ajax_reset_template(): void { // Verify nonce. if ( ! check_ajax_referer( 'genius_commerce_template_action', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } // Check permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above. $template = isset( $_POST['template'] ) ? sanitize_file_name( wp_unslash( $_POST['template'] ) ) : ''; if ( empty( $template ) ) { wp_send_json_error( array( 'message' => __( 'Template naam ontbreekt', 'genius-commerce' ) ) ); } // Validate template name. $valid_templates = array( 'discount-price.php', 'delivery-product.php', 'delivery-cart.php' ); if ( ! in_array( $template, $valid_templates, true ) ) { wp_send_json_error( array( 'message' => __( 'Ongeldige template', 'genius-commerce' ) ) ); } // Delete custom template option to revert to default. $option_key = 'genius_commerce_template_' . sanitize_key( str_replace( '.php', '', $template ) ); delete_option( $option_key ); // Get default template content. $template_engine = new GC_Template_Engine(); $default_content = $template_engine->get_default_template_content( $template ); wp_send_json_success( array( 'message' => __( 'Template gereset naar standaard', 'genius-commerce' ), 'content' => $default_content, ) ); } /** * Render the "Actuele scopes" subtab (Story 8.3). * * Shows the cached scope-list, the cache's `updated_at` timestamp, a * "Nu verversen"-button (AJAX) and — when the feature-flag is off — a * banner explaining that enforcement is in fallback-mode. The banner * is the loudest signal so a tenant-admin notices the cutover hasn't * happened yet. * * @return void */ private function render_scopes_section(): void { $enforcement_on = GC_Scopes::is_enforcement_enabled(); $cached = GC_Scopes::get_cached(); ?>


__( 'Ongeldige beveiligingstoken', 'genius-commerce' ) ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Geen rechten', 'genius-commerce' ) ) ); } $result = GC_Scopes::force_refresh(); if ( $result instanceof \WP_Error ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s: backend error message */ __( 'Verversen mislukt: %s', 'genius-commerce' ), $result->get_error_message() ), ) ); } wp_send_json_success( array( 'message' => __( 'Scopes ververst', 'genius-commerce' ), 'version' => $result['version'], 'scopes' => $result['scopes'], ) ); } }