sanitize_text_field( self::field( $posted, 'key_prefix' ) ), 'base_url' => esc_url_raw( self::field( $posted, 'base_url' ) ), 'pixel_url' => esc_url_raw( self::field( $posted, 'pixel_url' ) ), 'program_id' => (int) self::field( $posted, 'program_id' ), 'store_ref' => sanitize_text_field( self::field( $posted, 'store_ref' ) ), 'test_mode' => empty( $posted['test_mode'] ) ? 0 : 1, 'enabled' => empty( $posted['enabled'] ) ? 0 : 1, 'beacon' => empty( $posted['beacon'] ) ? 0 : 1, 'pixel' => empty( $posted['pixel'] ) ? 0 : 1, 'report_all' => empty( $posted['report_all'] ) ? 0 : 1, 'cookie_days' => max( 1, min( 365, (int) self::field( $posted, 'cookie_days' ) ) ), 'timeout' => max( 1, min( 30, (int) self::field( $posted, 'timeout' ) ) ), 'paid_statuses' => self::posted_statuses( $posted ), ); // A secret is only overwritten when a new one is typed. The field is // rendered empty with a placeholder, so saving the page without // touching it must not wipe the key. $api_key = trim( self::field( $posted, 'api_key' ) ); if ( '' !== $api_key ) { $values['api_key'] = sanitize_text_field( $api_key ); } $secret = trim( self::field( $posted, 'signing_secret' ) ); if ( '' !== $secret ) { $values['signing_secret'] = sanitize_text_field( $secret ); } Affiliatant_Settings::save( $values ); self::back( 'saved' ); } /** * The connection test. * * It round-trips a REAL test conversion against a reserved reference, so * it proves the key, the base URL, the programme id, the signature and * the network in one go rather than only that a hostname resolves. * * is_test is always set, whatever the test-mode setting says, so pressing * a button on a settings page can never create a real commission. * * @return void */ public static function handle_test() { self::guard( 'affiliatant_test' ); if ( ! Affiliatant_Settings::configured() ) { self::back( 'test_failed', __( 'Enter an API key and a programme id first.', 'affiliatant-for-woocommerce' ) ); } $reference = 'AFFTEST-' . gmdate( 'ymd-His' ); try { $payload = Affiliatant_Payload::connection_test( (int) Affiliatant_Settings::get( 'program_id' ), (string) Affiliatant_Settings::get( 'store_ref' ), function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'GBP', $reference, gmdate( 'c' ) ); $response = Affiliatant_Settings::client()->createConversion( $payload, $payload['event_id'] ); $detail = $reference; if ( isset( $response['status'] ) && is_string( $response['status'] ) ) { $detail .= ' (' . $response['status'] . ')'; } Affiliatant_Settings::note_event( 'connection_test', $payload['event_id'], 0 ); self::back( 'test_ok', $detail ); } catch ( \Throwable $e ) { // The message is Affiliatant's own error text. It never contains // the key: the SDK keeps it out of exception messages and URLs. Affiliatant_Settings::note_error( $e->getMessage(), 0 ); self::back( 'test_failed', $e->getMessage() ); } } /** * Drain the queue now. * * @return void */ public static function handle_retry() { self::guard( 'affiliatant_retry' ); $result = Affiliatant_Sender::drain( 50, 20 ); self::back( 'retried', sprintf( /* translators: 1: events sent, 2: events that failed */ __( '%1$d sent, %2$d still failing.', 'affiliatant-for-woocommerce' ), (int) $result['sent'], (int) $result['failed'] ) ); } /** * Delete the failed rows. * * @return void */ public static function handle_clear() { self::guard( 'affiliatant_clear' ); $deleted = Affiliatant_Queue::clear_failed(); self::back( 'cleared', sprintf( /* translators: %d: rows removed */ __( '%d failed event removed.', 'affiliatant-for-woocommerce' ), (int) $deleted ) ); } /** * Attach a referral to an order by hand, and report it. * * The back-office case: the order was taken over the telephone, or * created in wp-admin, so there was never a browser with an aftid in it. * * This is a form on THIS page rather than a field on the order screen, * because WooCommerce's own save hook for the order screen, * woocommerce_process_shop_order_meta, RETURNS EARLY WITHOUT FIRING WHEN * HPOS IS ENABLED. Hanging the save off it would work on a legacy install * and silently do nothing on a modern one, which is the worst of the two. * * @return void */ public static function handle_set_click() { self::guard( 'affiliatant_set_click' ); $order_id = isset( $_POST['order_id'] ) ? (int) $_POST['order_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- guard() ran check_admin_referer. $click_id = isset( $_POST['click_id'] ) ? Affiliatant_Capture::clean_click_id( sanitize_text_field( wp_unslash( $_POST['click_id'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $order = $order_id > 0 ? wc_get_order( $order_id ) : false; if ( ! is_a( $order, 'WC_Order' ) ) { self::back( 'manual_failed', __( 'No such order.', 'affiliatant-for-woocommerce' ) ); } if ( '' === $click_id ) { self::back( 'manual_failed', __( 'That is not a valid referral id.', 'affiliatant-for-woocommerce' ) ); } Affiliatant_Order::set_click( $order, $click_id ); $queued = Affiliatant_Reporter::report_sale( $order ); self::back( 'manual_ok', $queued ? sprintf( /* translators: %d: order id */ __( 'Order %d is now attributed and has been queued.', 'affiliatant-for-woocommerce' ), $order_id ) : sprintf( /* translators: %d: order id */ __( 'Order %d is now attributed. It was already reported, so nothing new was queued.', 'affiliatant-for-woocommerce' ), $order_id ) ); } // ----------------------------------------------------------------- // The page // ----------------------------------------------------------------- /** * Render the settings page. * * @return void */ public static function render() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to see this page.', 'affiliatant-for-woocommerce' ) ); } Affiliatant_Settings::flush(); $settings = Affiliatant_Settings::all(); $counts = Affiliatant_Queue::counts(); $events = Affiliatant_Queue::recent( 20 ); $action = esc_url( admin_url( 'admin-post.php' ) ); echo '
'; self::button( $action, 'affiliatant_test', __( 'Send a test conversion', 'affiliatant-for-woocommerce' ), 'button button-primary' ); self::button( $action, 'affiliatant_retry', __( 'Retry now', 'affiliatant-for-woocommerce' ), 'button' ); self::button( $action, 'affiliatant_clear', __( 'Clear failed', 'affiliatant-for-woocommerce' ), 'button' ); echo '
'; // ---- Settings. echo '' . esc_html__( 'For an order taken over the telephone or created in the admin, where there was never a browser carrying a referral.', 'affiliatant-for-woocommerce' ) . '
'; echo ''; // ---- Recent events. echo '| ' . esc_html__( 'When', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Action', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Order', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Status', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Attempts', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Reversed', 'affiliatant-for-woocommerce' ) . ' | ' . '' . esc_html__( 'Error', 'affiliatant-for-woocommerce' ) . ' | ' . '
|---|---|---|---|---|---|---|
| ' . esc_html__( 'Nothing yet.', 'affiliatant-for-woocommerce' ) . ' | ||||||
| ' . esc_html( self::when( (int) $row['updated'] ) ) . ' | ' . '' . esc_html( (string) $row['action'] ) . ' | ' . '' . esc_html( (string) $row['order_id'] ) . ' | ' . '' . esc_html( (string) $row['status'] ) . ' | ' . '' . esc_html( (string) $row['attempts'] ) . ' | ' . '' . esc_html( (int) $row['amount_minor'] > 0 ? (string) (int) $row['amount_minor'] . ' minor' : '-' ) . ' | ' . '' . esc_html( (string) $row['last_error'] ) . ' | ' . '
' . esc_html__( 'Referral', 'affiliatant-for-woocommerce' ) . ':
'
. ( array() === $context
? esc_html__( 'None recorded.', 'affiliatant-for-woocommerce' )
: '' . esc_html( $context['click_id'] ) . '' )
. '
' . esc_html__( 'Reported', 'affiliatant-for-woocommerce' ) . ':
'
. ( Affiliatant_Queue::has_conversion( (int) $order->get_id() )
? esc_html__( 'Yes', 'affiliatant-for-woocommerce' )
: esc_html__( 'Not yet', 'affiliatant-for-woocommerce' ) )
. '
' . esc_html__( 'Affiliatant settings and status', 'affiliatant-for-woocommerce' ) . '
'; } // ----------------------------------------------------------------- // Small helpers // ----------------------------------------------------------------- /** * Capability and nonce, in that order, before anything changes. * * @param string $nonce Action name. * @return void */ private static function guard( $nonce ) { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'affiliatant-for-woocommerce' ) ); } check_admin_referer( $nonce ); } /** * Back to the page with a message. * * @param string $code Message code. * @param string $detail Extra text. * @return void */ private static function back( $code, $detail = '' ) { $url = add_query_arg( array( 'page' => self::SLUG, 'affiliatant_notice' => $code, 'affiliatant_detail' => rawurlencode( substr( $detail, 0, 300 ) ), ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** * Print the notice carried back from an action. * * @return void */ private static function notice() { if ( empty( $_GET['affiliatant_notice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- printing a message, changing nothing. return; } $code = sanitize_key( wp_unslash( $_GET['affiliatant_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $detail = isset( $_GET['affiliatant_detail'] ) ? sanitize_text_field( wp_unslash( $_GET['affiliatant_detail'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $messages = array( 'saved' => array( 'success', __( 'Settings saved.', 'affiliatant-for-woocommerce' ) ), 'test_ok' => array( 'success', __( 'Connection test succeeded.', 'affiliatant-for-woocommerce' ) ), 'test_failed' => array( 'error', __( 'Connection test failed.', 'affiliatant-for-woocommerce' ) ), 'retried' => array( 'success', __( 'Queue drained.', 'affiliatant-for-woocommerce' ) ), 'cleared' => array( 'success', __( 'Failed events cleared.', 'affiliatant-for-woocommerce' ) ), 'manual_ok' => array( 'success', __( 'Order attributed.', 'affiliatant-for-woocommerce' ) ), 'manual_failed' => array( 'error', __( 'Could not attribute that order.', 'affiliatant-for-woocommerce' ) ), ); if ( ! isset( $messages[ $code ] ) ) { return; } echo ''; } /** * A one-button form, so every action is a POST with its own nonce. * * @param string $action Form action URL, already escaped. * @param string $name admin_post action name. * @param string $label Button text. * @param string $class CSS classes. * @return void */ private static function button( $action, $name, $label, $class ) { echo ''; } /** * One row of the status table. * * @param string $label Label. * @param string $value Value. * @return void */ private static function status_row( $label, $value ) { echo '' . esc_html( $description ) . '
'; } echo '' . esc_html( $description ) . '
'; } echo '