*/ public static function request_fields(): array { $fields = []; foreach ( [ 'encoded', 'checksum' ] as $name ) { foreach ( [ $name, strtoupper( $name ) ] as $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Server-to-server callback from ePay.bg, authenticated by the HMAC checksum over these exact bytes. if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) { $fields[ $name ] = wp_unslash( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Same: the checksum covers these exact bytes. break; } } } return $fields; } public static function mark_uncacheable(): void { if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- cache plugins read this exact name. } } public static function send( string $body ): never { self::mark_uncacheable(); nocache_headers(); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Content-Type-Options: nosniff' ); echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text/plain reply built by Codec from digits and fixed tokens; HTML escaping would corrupt it. exit; } /** * @param array $post */ public static function respond( array $post, string $secret ): string { $fields = array_change_key_case( $post, CASE_LOWER ); $encoded = isset( $fields['encoded'] ) && is_string( $fields['encoded'] ) ? trim( $fields['encoded'] ) : ''; $checksum = isset( $fields['checksum'] ) && is_string( $fields['checksum'] ) ? trim( $fields['checksum'] ) : ''; if ( '' === $encoded || '' === $checksum ) { return Codec::error_reply( 'MISSING' ); } if ( strlen( $encoded ) > self::MAX_ENCODED_BYTES ) { Log::write( Log::fields( [ 'notification rejected' => 'oversized', 'bytes' => (string) strlen( $encoded ) ] ) ); return Codec::error_reply( 'MALFORMED' ); } if ( ! Codec::verify( $encoded, $checksum, $secret ) ) { return Codec::error_reply( 'BAD_CHECKSUM' ); } $entries = Codec::parse_notification( $encoded ); if ( [] === $entries ) { return Codec::error_reply( 'MALFORMED' ); } $results = []; foreach ( $entries as $entry ) { $result = self::result_for( $entry ); Log::write( 'notification ' . self::log_fields( $entry ) . ' result=' . $result ); $invoice = $entry['invoice']; if ( ! isset( $results[ $invoice ] ) || 'ERR' === $result ) { $results[ $invoice ] = isset( $results[ $invoice ] ) ? 'ERR' : $result; } } $recorded = end( $entries ); foreach ( $entries as $entry ) { if ( 'ERR' === $results[ $entry['invoice'] ] ) { $recorded = $entry; break; } } Notification_Health::record( $recorded['invoice'], $results[ $recorded['invoice'] ] ); return Codec::reply( $results ); } /** * @param array $entry */ public static function process_line( array $entry ): string { $invoice = $entry['invoice']; try { $resolved = Invoice_Resolver::resolve( $invoice ); if ( in_array( $resolved['kind'], [ 'order', 'legacy' ], true ) && null !== $resolved['order'] ) { return self::apply_locked( $resolved['order']->get_id(), $entry ); } if ( 'legacy_unmatched' === $resolved['kind'] ) { if ( null === Legacy::cutoff() ) { return 'ERR'; } if ( 'PAID' === $entry['status'] ) { return Alerts::missing_order( $invoice ) && Legacy::is_confirmed() ? 'OK' : 'ERR'; } return Legacy::is_confirmed() ? 'NO' : 'ERR'; } if ( 'unknown' === $resolved['kind'] && 'PAID' !== $entry['status'] ) { return 'NO'; } return self::missing( $entry ); } catch ( \Throwable $e ) { Log::write( Log::fields( [ 'invoice' => $invoice, 'exception' => get_class( $e ) ] ) ); return 'ERR'; } } /** * @param array $entry */ private static function missing( array $entry ): string { if ( 'PAID' !== $entry['status'] ) { return 'OK'; } return Alerts::missing_order( $entry['invoice'] ) ? 'OK' : 'ERR'; } /** * @param array $entry */ private static function result_for( array $entry ): string { if ( ! in_array( $entry['status'], [ 'PAID', 'DENIED', 'EXPIRED' ], true ) ) { return 'ERR'; } return self::process_line( $entry ); } /** * @param array $entry */ private static function apply_locked( int $order_id, array $entry ): string { return (string) Order_Lock::with( $order_id, static function () use ( $order_id, $entry ): string { $order = wc_get_order( $order_id ); if ( ! $order instanceof \WC_Order ) { return self::missing( $entry ); } return Order_Outcome::apply( $order, $entry ); } ); } /** * @param array $entry */ private static function log_fields( array $entry ): string { return Log::fields( [ 'invoice' => $entry['invoice'], 'status' => $entry['status'], 'pay_time' => $entry['pay_time'] ?? null, 'stan' => $entry['stan'] ?? null, 'bcode' => $entry['bcode'] ?? null, ] ); } }