fields = new FieldRenderer(); } /** * Render the screen. */ public function render(): void { if ( ! current_user_can( Menu::CAPABILITY ) ) { wp_die( esc_html__( 'شما اجازه دسترسی به این صفحه را ندارید.', 'rahkar-toolkit' ) ); } $notice = $this->handleSubmission(); Ui::pageOpen( __( 'درگاه‌های پیامک', 'rahkar-toolkit' ), __( 'سرویس‌دهنده‌هایی که فروشگاه از طریقشان پیامک می‌فرستد. اگر چند درگاه روشن باشد، به ترتیب اولویت امتحان می‌شوند و اگر اولی نپذیرفت، بعدی.', 'rahkar-toolkit' ), Help::link( self::MODULE, __( 'اطلاع‌رسانی', 'rahkar-toolkit' ) ) ); if ( '' !== $notice['message'] ) { echo wp_kses( Ui::notice( $notice['type'], $notice['message'] ), Ui::allowedHtml() ); } $gateways = Gateways::available(); if ( [] === $gateways ) { echo wp_kses( Ui::emptyState( __( 'هیچ درگاه پیامکی ثبت نشده است.', 'rahkar-toolkit' ), '', 'plug' ), Ui::allowedHtml() ); Ui::pageClose(); return; } $enabled = Gateways::enabledIds(); $values = Settings::all( self::MODULE ); Ui::formOpen(); wp_nonce_field( self::NONCE ); foreach ( $gateways as $id => $driver ) { $position = array_search( $id, $enabled, true ); $this->renderGateway( $id, $driver, false !== $position, false === $position ? count( $enabled ) + 1 : $position + 1, $values ); } $this->renderPolicy(); Ui::formClose(); Ui::pageClose(); } /** * One gateway's card. * * @param string $id Gateway id. * @param SmsDriver $driver The gateway. * @param bool $isOn Whether it is enabled. * @param int $priority Its place in the order, 1-based. * @param array $values Stored settings. */ private function renderGateway( string $id, SmsDriver $driver, bool $isOn, int $priority, array $values ): void { $badges = $driver->isConfigured() ? Ui::badge( __( 'تنظیم‌شده', 'rahkar-toolkit' ), 'success' ) : Ui::badge( __( 'تنظیمات ناقص', 'rahkar-toolkit' ), 'warning' ); if ( $driver instanceof VoiceOtpDriver ) { $badges .= Ui::badge( __( 'تماس صوتی', 'rahkar-toolkit' ), 'info' ); } Ui::panelOpen( $driver->name(), '', 'plug', $badges, 'gateway-' . $id ); Ui::rowOpen( __( 'استفاده از این درگاه', 'rahkar-toolkit' ), 'rahkar-gw-on-' . $id ); echo wp_kses( Ui::toggle( 'rahkar_gw[' . $id . '][enabled]', $isOn, [ 'id' => 'rahkar-gw-on-' . $id ] ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'اولویت', 'rahkar-toolkit' ), 'rahkar-gw-priority-' . $id, __( 'عدد کمتر زودتر امتحان می‌شود.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'rahkar_gw[' . $id . '][priority]', (string) $priority, [ 'id' => 'rahkar-gw-priority-' . $id, 'min' => '1', 'max' => '99' ] ), Ui::allowedHtml() ); Ui::rowClose(); foreach ( $driver->settingsSchema() as $key => $field ) { $name = 'rahkar_gw_field[' . $key . ']'; Ui::rowOpen( (string) ( $field['label'] ?? $key ), sanitize_html_class( str_replace( [ '[', ']' ], '-', $name ) ), (string) ( $field['help'] ?? '' ) ); $this->fields->render( $name, $field, $values[ $key ] ?? ( $field['default'] ?? '' ) ); Ui::rowClose(); } if ( $driver instanceof PatternLookupDriver ) { Ui::rowOpen( __( 'الگوهای پنل', 'rahkar-toolkit' ), 'rahkar-gw-lookup-' . $id, __( 'فهرست الگوهای پنل را ببینید، یا کد یک الگو را وارد کنید تا متن، متغیرها و وضعیتش خوانده شود. فقط برای دیدن است و چیزی ذخیره نمی‌کند.', 'rahkar-toolkit' ) ); // No name: this box is for looking, and must never be submitted. echo wp_kses( Ui::input( 'text', '', '', [ 'id' => 'rahkar-gw-lookup-' . $id, 'dir' => 'ltr' ], true ), Ui::allowedHtml() ); echo wp_kses( PatternPicker::controls( $id, 'rahkar-gw-lookup-' . $id ), Ui::allowedHtml() ); Ui::rowClose(); } $footer = Ui::button( __( 'ذخیره', 'rahkar-toolkit' ), 'primary', [ 'type' => 'submit', 'name' => 'rahkar_gw_action', 'value' => 'save' ], '', 'check' ); if ( $driver instanceof CheckableDriver ) { $footer .= Ui::button( __( 'ذخیره و تست اتصال', 'rahkar-toolkit' ), 'secondary', [ 'type' => 'submit', 'name' => 'rahkar_gw_action', 'value' => 'check:' . $id ], '', 'gauge' ); } Ui::panelClose( $footer ); } /** * What to do when a gateway refuses. */ private function renderPolicy(): void { Ui::panelOpen( __( 'وقتی درگاهی ارسال را نپذیرفت', 'rahkar-toolkit' ), '', 'refresh' ); Ui::rowOpen( __( 'رفتار', 'rahkar-toolkit' ), 'rahkar-gw-policy' ); echo wp_kses( Ui::select( 'rahkar_gw_policy', [ Gateways::POLICY_FAILOVER => __( 'درگاه بعدی امتحان شود (پیشنهادی)', 'rahkar-toolkit' ), Gateways::POLICY_FIRST_ONLY => __( 'فقط درگاه اول؛ خطای آن نتیجه نهایی است', 'rahkar-toolkit' ), ], Gateways::policy(), [ 'id' => 'rahkar-gw-policy' ] ), Ui::allowedHtml() ); Ui::rowClose(); Ui::panelClose( Ui::button( __( 'ذخیره', 'rahkar-toolkit' ), 'primary', [ 'type' => 'submit', 'name' => 'rahkar_gw_action', 'value' => 'save' ], '', 'check' ) ); } /** * Apply a submission. * * Every action saves first: a store owner who types a key and presses "test" * expects the key they typed to be the one tested. * * @return array{type: string, message: string} */ private function handleSubmission(): array { if ( ! isset( $_POST['rahkar_gw_action'] ) ) { return [ 'type' => '', 'message' => '', ]; } check_admin_referer( self::NONCE ); $action = sanitize_text_field( wp_unslash( $_POST['rahkar_gw_action'] ) ); $this->save(); [ $verb, $id ] = array_pad( explode( ':', $action, 2 ), 2, '' ); $driver = Gateways::available()[ $id ] ?? null; if ( 'check' === $verb && $driver instanceof CheckableDriver ) { $check = $driver->checkConnection(); return [ 'type' => $check->ok ? 'success' : 'error', 'message' => $driver->name() . ' — ' . $check->message, ]; } return [ 'type' => 'success', 'message' => __( 'تنظیمات درگاه‌ها ذخیره شد.', 'rahkar-toolkit' ), ]; } /** * Persist enabled gateways, their order, the policy and every driver field. */ private function save(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified in handleSubmission(), the only caller. $posted = isset( $_POST['rahkar_gw'] ) && is_array( $_POST['rahkar_gw'] ) ? map_deep( wp_unslash( $_POST['rahkar_gw'] ), 'sanitize_text_field' ) : []; $fields = isset( $_POST['rahkar_gw_field'] ) && is_array( $_POST['rahkar_gw_field'] ) ? map_deep( wp_unslash( $_POST['rahkar_gw_field'] ), 'sanitize_text_field' ) : []; $policy = isset( $_POST['rahkar_gw_policy'] ) ? sanitize_key( wp_unslash( $_POST['rahkar_gw_policy'] ) ) : Gateways::POLICY_FAILOVER; // phpcs:enable WordPress.Security.NonceVerification.Missing $order = []; $values = []; foreach ( Gateways::available() as $id => $driver ) { $row = is_array( $posted[ $id ] ?? null ) ? $posted[ $id ] : []; if ( ! empty( $row['enabled'] ) ) { $order[ $id ] = max( 1, (int) ( $row['priority'] ?? 99 ) ); } foreach ( $driver->settingsSchema() as $key => $field ) { if ( ! array_key_exists( $key, $fields ) ) { continue; } // An empty secret means "unchanged": the field is rendered empty on // purpose in some browsers' autofill handling, and wiping a working // API key because a box looked blank would stop every send. if ( 'password' === ( $field['type'] ?? '' ) && '' === trim( (string) $fields[ $key ] ) ) { continue; } $values[ $key ] = $this->fields->sanitize( $field, $fields[ $key ] ); } } // Sorted by priority, and by registry order where priorities tie. $ranked = []; foreach ( array_keys( $order ) as $index => $id ) { $ranked[] = [ $order[ $id ], $index, $id ]; } sort( $ranked ); $values['sms_gateways'] = array_column( $ranked, 2 ); $values['sms_policy'] = Gateways::POLICY_FIRST_ONLY === $policy ? Gateways::POLICY_FIRST_ONLY : Gateways::POLICY_FAILOVER; Settings::save( self::MODULE, $values ); // A new key can mean a different panel with different patterns. foreach ( array_keys( Gateways::available() ) as $id ) { PatternPicker::flush( $id ); } } }