handleSubmission(); // Self-heal: a lost queue row would otherwise stop retention for good. Housekeeping::schedule(); Ui::pageOpen( __( 'امنیت ورود', 'rahkar-toolkit' ), __( 'آیا کد ورود به دست مشتری‌ها می‌رسد؟ آمار ارسال، وضعیت هر درگاه، گزارش کامل رویدادها و هشدار پیامکی به مدیران وقتی مشکلی پیش می‌آید.', 'rahkar-toolkit' ), Help::link( OtpAuthModule::SLUG, __( 'ورود و عضویت پیامکی', 'rahkar-toolkit' ) ) ); Diagnostics::render( self::SLUG ); if ( '' !== $notice['message'] ) { echo wp_kses( Ui::notice( $notice['type'], $notice['message'] ), Ui::allowedHtml() ); } $tab = $this->currentTab(); echo wp_kses( Ui::tabs( [ self::TAB_OVERVIEW => [ 'label' => __( 'نمای کلی', 'rahkar-toolkit' ), 'href' => $this->url( self::TAB_OVERVIEW ), ], self::TAB_LOG => [ 'label' => __( 'گزارش رویدادها', 'rahkar-toolkit' ), 'href' => $this->url( self::TAB_LOG ), ], self::TAB_ALERTS => [ 'label' => __( 'هشدار و تنظیمات', 'rahkar-toolkit' ), 'href' => $this->url( self::TAB_ALERTS ), ], ], $tab, __( 'بخش‌های مانیتورینگ', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); if ( self::TAB_LOG === $tab ) { $this->renderLog(); } elseif ( self::TAB_ALERTS === $tab ) { $this->renderAlerts(); } else { $this->renderOverview(); } Ui::pageClose(); } /* * --------------------------------------------------------------------- * Overview * --------------------------------------------------------------------- */ /** * Figures, timeline and gateways. */ private function renderOverview(): void { $this->renderStatus(); $window = MonitorStats::window( DAY_IN_SECONDS ); $signIns = MonitorStats::signIns( DAY_IN_SECONDS ); $ok = $window['total'] - $window['failed']; $rate = $window['total'] > 0 ? (int) round( $ok * 100 / $window['total'] ) : 0; Ui::statsOpen(); echo wp_kses( Ui::stat( number_format_i18n( $window['total'] ), __( 'ارسال کد در ۲۴ ساعت', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); echo wp_kses( Ui::stat( $window['total'] > 0 ? number_format_i18n( $rate ) . '٪' : '—', __( 'موفق', 'rahkar-toolkit' ), '', $window['total'] > 0 ? ( $rate >= 90 ? 'success' : ( $rate >= 60 ? 'warning' : 'error' ) ) : '' ), Ui::allowedHtml() ); echo wp_kses( Ui::stat( number_format_i18n( $window['failed'] ), __( 'ناموفق', 'rahkar-toolkit' ), '', $window['failed'] > 0 ? 'error' : '' ), Ui::allowedHtml() ); echo wp_kses( Ui::stat( number_format_i18n( $signIns['logins'] ), __( 'ورود', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); echo wp_kses( Ui::stat( number_format_i18n( $signIns['signups'] ), __( 'عضویت جدید', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); Ui::statsClose(); $this->renderTimeline(); $this->renderGateways(); } /** * The failsafe's state, when there is something to say. */ private function renderStatus(): void { $status = Failsafe::status(); if ( null === $status ) { return; } $messages = [ 'no_gateway' => __( 'هیچ درگاه پیامکی فعال و تنظیم نشده است؛ هیچ کدی ارسال نمی‌شود. از صفحه «درگاه‌های پیامک» یک درگاه را روشن کنید.', 'rahkar-toolkit' ), 'test_only' => __( 'هیچ درگاهی تنظیم نشده و فقط حالت آزمایشی کار می‌کند. مشتری‌های واقعی کدی دریافت نمی‌کنند.', 'rahkar-toolkit' ), 'failing' => sprintf( /* translators: %d: failed sends in a row */ __( '%d ارسال آخر پشت سر هم ناموفق بوده است. اعتبار پنل و وضعیت درگاه را بررسی کنید — اگر ادامه پیدا کند، ورود پیامکی به‌طور خودکار کنار می‌رود.', 'rahkar-toolkit' ), $status['failures'] ), 'tripped' => sprintf( /* translators: 1: failed sends in a row, 2: time */ __( 'پس از %1$d ارسال ناموفق پیاپی، از %2$s ورود پیامکی کنار رفته و فرم ورود عادی قالب نشان داده می‌شود. هر چند دقیقه یک بار دوباره امتحان می‌شود و با اولین ارسال موفق خودکار برمی‌گردد.', 'rahkar-toolkit' ), $status['failures'], wp_date( 'H:i', $status['since'] ) ), 'retrying' => __( 'ورود پیامکی دوباره نشان داده می‌شود تا ارسال بعدی امتحان شود. اگر آن هم ناموفق باشد، دوباره کنار می‌رود.', 'rahkar-toolkit' ), ]; echo wp_kses( Ui::notice( $status['level'], $messages[ $status['key'] ] ?? '' ), Ui::allowedHtml() ); if ( $status['since'] > 0 ) { Ui::formOpen(); wp_nonce_field( self::NONCE ); echo wp_kses( Ui::button( __( 'برگرداندن ورود پیامکی همین حالا', 'rahkar-toolkit' ), 'secondary', [ 'type' => 'submit', 'name' => 'rahkar_mon_action', 'value' => 'reset' ], '', 'refresh' ), Ui::allowedHtml() ); Ui::formClose(); } } /** * Sends per hour over the last day, failures stacked on top. */ private function renderTimeline(): void { $slices = MonitorStats::timeline( 24, HOUR_IN_SECONDS ); $max = max( 1, ...array_map( static fn( array $slice ): int => $slice['total'], $slices ) ); Ui::panelOpen( __( '۲۴ ساعت گذشته', 'rahkar-toolkit' ), __( 'ارسال کد در هر ساعت. بخش قرمز ارسال‌های ناموفق است.', 'rahkar-toolkit' ), 'chart' ); echo ''; Ui::panelClose(); } /** * Each gateway's recent record. */ private function renderGateways(): void { $enabled = Gateways::enabledIds(); Ui::panelOpen( __( 'درگاه‌ها', 'rahkar-toolkit' ), __( 'آخرین ارسال موفق و آخرین خطای هر درگاه — کد ورود و هشدارها هر دو حساب می‌شوند.', 'rahkar-toolkit' ), 'plug' ); Ui::formOpen(); wp_nonce_field( self::NONCE ); Ui::tableOpen( [ __( 'درگاه', 'rahkar-toolkit' ), __( 'وضعیت', 'rahkar-toolkit' ), __( 'موفق در ۲۴ ساعت', 'rahkar-toolkit' ), __( 'آخرین موفق', 'rahkar-toolkit' ), __( 'آخرین خطا', 'rahkar-toolkit' ), '', ] ); foreach ( Gateways::available() as $id => $driver ) { $window = MonitorStats::window( DAY_IN_SECONDS, $id ); $last = MonitorStats::lastOutcomes( $id ); if ( ! in_array( $id, $enabled, true ) ) { $state = Ui::badge( __( 'خاموش', 'rahkar-toolkit' ), 'quiet' ); } elseif ( ! $driver->isConfigured() ) { $state = Ui::badge( __( 'تنظیمات ناقص', 'rahkar-toolkit' ), 'warning' ); } elseif ( $last['error_at'] > $last['ok_at'] ) { $state = Ui::badge( __( 'در حال خطا', 'rahkar-toolkit' ), 'error' ); } else { $state = Ui::badge( __( 'فعال', 'rahkar-toolkit' ), 'success' ); } $error = 0 === $last['error_at'] ? '—' : esc_html( $this->ago( $last['error_at'] ) ) . '
' . esc_html( $last['error'] ) . ''; $check = $driver instanceof CheckableDriver && $driver->isConfigured() ? Ui::button( __( 'تست اتصال', 'rahkar-toolkit' ), 'secondary', [ 'type' => 'submit', 'name' => 'rahkar_mon_action', 'value' => 'check:' . $id ], '', 'gauge' ) : ''; printf( '%s%s%s%s%s%s', esc_html( $driver->name() ), wp_kses( $state, Ui::allowedHtml() ), esc_html( $window['total'] > 0 /* translators: 1: successful sends, 2: all sends */ ? sprintf( __( '%1$s از %2$s', 'rahkar-toolkit' ), number_format_i18n( $window['total'] - $window['failed'] ), number_format_i18n( $window['total'] ) ) : '—' ), esc_html( 0 === $last['ok_at'] ? '—' : $this->ago( $last['ok_at'] ) ), wp_kses( $error, Ui::allowedHtml() ), wp_kses( $check, Ui::allowedHtml() ) ); } Ui::tableClose(); Ui::formClose(); Ui::panelClose( Ui::button( __( 'مدیریت درگاه‌ها', 'rahkar-toolkit' ), 'secondary', [], admin_url( 'admin.php?page=' . Menu::SLUG . '-gateways' ), 'settings' ) ); } /* * --------------------------------------------------------------------- * Log * --------------------------------------------------------------------- */ /** * Filters, rows and pages. */ private function renderLog(): void { $filters = $this->filters(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only paging. $page = max( 1, isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1 ); $total = OtpLog::count( $filters ); $rows = OtpLog::search( $filters, OtpLog::PER_PAGE, ( $page - 1 ) * OtpLog::PER_PAGE ); $events = self::eventLabels(); Ui::formOpen( 'get' ); printf( '', esc_attr( self::SLUG ), esc_attr( self::TAB_LOG ) ); Ui::toolbarOpen(); echo wp_kses( Ui::input( 'search', 'phone', $filters['phone'], [ 'placeholder' => __( 'شماره موبایل…', 'rahkar-toolkit' ), 'dir' => 'ltr' ] ), Ui::allowedHtml() ); echo wp_kses( Ui::select( 'event', [ '' => __( 'همه رویدادها', 'rahkar-toolkit' ) ] + $events, $filters['event'], [], true ), Ui::allowedHtml() ); echo wp_kses( Ui::select( 'result', [ '' => __( 'همه نتیجه‌ها', 'rahkar-toolkit' ) ] + self::resultLabels(), $filters['result'], [], true ), Ui::allowedHtml() ); echo wp_kses( Ui::select( 'gateway', [ '' => __( 'همه درگاه‌ها', 'rahkar-toolkit' ) ] + $this->gatewayLabels(), $filters['gateway'], [], true ), Ui::allowedHtml() ); echo wp_kses( Ui::button( __( 'فیلتر', 'rahkar-toolkit' ), 'primary', [ 'type' => 'submit' ], '', 'search' ), Ui::allowedHtml() ); echo wp_kses( Ui::toolbarSpacer(), Ui::allowedHtml() ); echo wp_kses( Ui::button( __( 'پاک کردن فیلترها', 'rahkar-toolkit' ), 'secondary', [], $this->url( self::TAB_LOG ), 'close' ), Ui::allowedHtml() ); Ui::toolbarClose(); Ui::formClose(); Ui::tableOpen( [ __( 'زمان', 'rahkar-toolkit' ), __( 'موبایل', 'rahkar-toolkit' ), __( 'رویداد', 'rahkar-toolkit' ), __( 'نتیجه', 'rahkar-toolkit' ), __( 'درگاه', 'rahkar-toolkit' ), __( 'روش', 'rahkar-toolkit' ), __( 'جزئیات', 'rahkar-toolkit' ), ], true ); if ( [] === $rows ) { echo wp_kses( Ui::tableEmptyRow( 7, __( 'رکوردی یافت نشد.', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); } foreach ( $rows as $row ) { $this->renderLogRow( $row, $events ); } Ui::tableClose(); echo wp_kses( Ui::pagination( $page, (int) ceil( $total / OtpLog::PER_PAGE ), fn( int $number ): string => add_query_arg( array_filter( $filters + [ 'paged' => (string) $number ] ), $this->url( self::TAB_LOG ) ), sprintf( /* translators: 1: matching rows, 2: retention days */ __( '%1$s رکورد — رکوردها پس از %2$s روز خودکار پاک می‌شوند. کد ورود هرگز ذخیره نمی‌شود.', 'rahkar-toolkit' ), number_format_i18n( $total ), number_format_i18n( OtpLog::retentionDays() ) ) ), Ui::allowedHtml() ); } /** * One log row. * * @param array $row Log row. * @param array $events Event labels. */ private function renderLogRow( array $row, array $events ): void { $result = (string) $row['result']; $phone = PhoneNumber::parse( (string) $row['phone'] ); $user = (int) $row['user_id']; $detail = esc_html( (string) $row['message'] ); if ( $user > 0 ) { $detail .= ( '' !== $detail ? ' — ' : '' ) . sprintf( '%s', esc_url( get_edit_user_link( $user ) ), /* translators: %d: user id */ esc_html( sprintf( __( 'کاربر #%d', 'rahkar-toolkit' ), $user ) ) ); } printf( '%s%s%s%s%s%s%s', wp_kses( Ui::badge( (string) wp_date( 'Y/m/d H:i:s', (int) strtotime( (string) $row['created_at'] . ' UTC' ) ), 'quiet', true ), Ui::allowedHtml() ), esc_html( null !== $phone ? $phone->national() : (string) $row['phone'] ), esc_html( $events[ (string) $row['event'] ] ?? (string) $row['event'] ), wp_kses( Ui::badge( self::resultLabels()[ $result ] ?? $result, self::resultVariant( $result ) ), Ui::allowedHtml() ), esc_html( $this->gatewayLabels()[ (string) $row['gateway'] ] ?? (string) $row['gateway'] ), esc_html( self::viaLabels()[ (string) $row['via'] ] ?? (string) $row['via'] ), wp_kses( $detail, Ui::allowedHtml() ) ); } /* * --------------------------------------------------------------------- * Alerts and settings * --------------------------------------------------------------------- */ /** * Alert rules, failsafe, retention and recent alerts. */ private function renderAlerts(): void { $s = Settings::all( OtpAuthModule::SLUG ); Ui::formOpen(); wp_nonce_field( self::NONCE ); Ui::panelOpen( __( 'هشدار پیامکی به مدیران', 'rahkar-toolkit' ), __( 'وقتی ورود پیامکی مشکل دارد، پیش از آنکه مشتری‌ها خبر بدهند به شما پیامک می‌زند. هشدار از درگاهی فرستاده می‌شود که در حال حاضر خطا ندارد، چون شایع‌ترین علت هشدار خود درگاه است.', 'rahkar-toolkit' ), 'alert' ); Ui::rowOpen( __( 'شماره موبایل مدیران', 'rahkar-toolkit' ), 'rahkar-mon-phones', __( 'هر شماره در یک خط یا با ویرگول جدا. خالی یعنی هیچ هشداری فرستاده نمی‌شود.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::textarea( 'alert_phones', (string) $s['alert_phones'], 3, [ 'id' => 'rahkar-mon-phones' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'ارسال‌های ناموفق', 'rahkar-toolkit' ), 'rahkar-mon-fail' ); echo wp_kses( Ui::toggle( 'alert_fail_on', (bool) $s['alert_fail_on'], [ 'id' => 'rahkar-mon-fail' ], __( 'وقتی ارسال کد پشت سر هم یا با نرخ بالا ناموفق است', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'ناموفق پیاپی', 'rahkar-toolkit' ), 'rahkar-mon-streak', __( 'این تعداد ارسال ناموفق پشت سر هم. صفر یعنی این قاعده خاموش.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'alert_fail_streak', (string) $s['alert_fail_streak'], [ 'id' => 'rahkar-mon-streak', 'min' => '0', 'max' => '100' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'یا درصد ناموفق', 'rahkar-toolkit' ), 'rahkar-mon-rate', __( 'درصد ارسال‌های ناموفق در بازه زیر. صفر یعنی این قاعده خاموش.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'alert_fail_rate', (string) $s['alert_fail_rate'], [ 'id' => 'rahkar-mon-rate', 'min' => '0', 'max' => '100' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'در بازه (دقیقه)', 'rahkar-toolkit' ), 'rahkar-mon-window' ); echo wp_kses( Ui::input( 'number', 'alert_fail_window', (string) $s['alert_fail_window'], [ 'id' => 'rahkar-mon-window', 'min' => '5', 'max' => '1440' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'با دست‌کم این تعداد ارسال', 'rahkar-toolkit' ), 'rahkar-mon-min', __( 'تا دو خطا از سه ارسالِ نیمه‌شب کسی را بیدار نکند.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'alert_fail_min', (string) $s['alert_fail_min'], [ 'id' => 'rahkar-mon-min', 'min' => '1', 'max' => '1000' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'کنار رفتن ورود پیامکی', 'rahkar-toolkit' ), 'rahkar-mon-failsafe' ); echo wp_kses( Ui::toggle( 'alert_failsafe_on', (bool) $s['alert_failsafe_on'], [ 'id' => 'rahkar-mon-failsafe' ], __( 'وقتی failsafe فروشگاه را به فرم ورود عادی برگرداند', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'تعداد غیرعادی کد', 'rahkar-toolkit' ), 'rahkar-mon-volume' ); echo wp_kses( Ui::toggle( 'alert_volume_on', (bool) $s['alert_volume_on'], [ 'id' => 'rahkar-mon-volume' ], __( 'وقتی در یک ساعت بیش از حد انتظار کد ارسال شود — نشانه سوءاستفاده از فرم و هزینه پیامک', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'سقف کد در یک ساعت', 'rahkar-toolkit' ), 'rahkar-mon-hour' ); echo wp_kses( Ui::input( 'number', 'alert_volume_hour', (string) $s['alert_volume_hour'], [ 'id' => 'rahkar-mon-hour', 'min' => '1', 'max' => '100000' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'فاصله تکرار هشدار (دقیقه)', 'rahkar-toolkit' ), 'rahkar-mon-cooldown', __( 'هر هشدار حداکثر یک بار در این بازه فرستاده می‌شود.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'alert_cooldown', (string) $s['alert_cooldown'], [ 'id' => 'rahkar-mon-cooldown', 'min' => '5', 'max' => '1440' ], true ), Ui::allowedHtml() ); Ui::rowClose(); foreach ( Gateways::available() as $id => $driver ) { $patternKey = AlertMonitor::patternKey( $id ); $variableKey = AlertMonitor::variableKey( $id ); Ui::rowOpen( /* translators: %s: gateway name */ sprintf( __( 'کد الگوی هشدار — %s', 'rahkar-toolkit' ), wp_kses( $driver->name(), Ui::allowedHtml() ) ), 'rahkar-mon-' . $patternKey, __( 'الگویی با یک متغیر برای متن هشدار. خالی یعنی متن آزاد، که برای شماره‌های عضو لیست سیاه اپراتور تحویل نمی‌شود.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'text', 'alert_pattern[' . $id . ']', (string) ( $s[ $patternKey ] ?? '' ), [ 'id' => 'rahkar-mon-' . $patternKey, 'dir' => 'ltr' ], true ), Ui::allowedHtml() ); echo wp_kses( PatternPicker::controls( $id, 'rahkar-mon-' . $patternKey, 'rahkar-mon-' . $variableKey ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( /* translators: %s: gateway name */ sprintf( __( 'نام متغیر متن — %s', 'rahkar-toolkit' ), wp_kses( $driver->name(), Ui::allowedHtml() ) ), 'rahkar-mon-' . $variableKey ); echo wp_kses( Ui::input( 'text', 'alert_var[' . $id . ']', (string) ( $s[ $variableKey ] ?? AlertMonitor::DEFAULT_VARIABLE ), [ 'id' => 'rahkar-mon-' . $variableKey, 'dir' => 'ltr' ], true ), Ui::allowedHtml() ); Ui::rowClose(); } Ui::panelClose( Ui::button( __( 'ذخیره', 'rahkar-toolkit' ), 'primary', [ 'type' => 'submit', 'name' => 'rahkar_mon_action', 'value' => 'save' ], '', 'check' ) . Ui::button( __( 'ذخیره و ارسال هشدار آزمایشی', 'rahkar-toolkit' ), 'secondary', [ 'type' => 'submit', 'name' => 'rahkar_mon_action', 'value' => 'test' ] ) ); Ui::panelOpen( __( 'failsafe و نگهداری', 'rahkar-toolkit' ), '', 'shield' ); Ui::rowOpen( __( 'کنار رفتن پس از چند خطای پیاپی', 'rahkar-toolkit' ), 'rahkar-mon-threshold', __( 'اگر این تعداد ارسال پشت سر هم ناموفق باشد — مثلاً اعتبار پنل تمام شده — ورود پیامکی کنار می‌رود و فرم ورود با ایمیل و رمز عبور قالب نشان داده می‌شود، به‌جای فرمی که هیچ کدی از آن نمی‌رسد. هر چند دقیقه دوباره امتحان می‌شود و با اولین ارسال موفق خودکار برمی‌گردد. صفر یعنی خاموش.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'failsafe_threshold', (string) $s['failsafe_threshold'], [ 'id' => 'rahkar-mon-threshold', 'min' => '0', 'max' => '100' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::rowOpen( __( 'نگهداری گزارش (روز)', 'rahkar-toolkit' ), 'rahkar-mon-days', __( 'صفر یعنی گزارش‌ها هرگز پاک نشوند.', 'rahkar-toolkit' ) ); echo wp_kses( Ui::input( 'number', 'log_days', (string) $s['log_days'], [ 'id' => 'rahkar-mon-days', 'min' => '0', 'max' => '365' ], true ), Ui::allowedHtml() ); Ui::rowClose(); Ui::panelClose( Ui::button( __( 'ذخیره', 'rahkar-toolkit' ), 'primary', [ 'type' => 'submit', 'name' => 'rahkar_mon_action', 'value' => 'save' ], '', 'check' ) ); Ui::formClose(); $this->renderRecentAlerts(); } /** * The last alerts sent. */ private function renderRecentAlerts(): void { $rows = MonitorStats::recentAlerts( 10 ); Ui::panelOpen( __( 'هشدارهای اخیر', 'rahkar-toolkit' ), '', 'info' ); Ui::tableOpen( [ __( 'زمان', 'rahkar-toolkit' ), __( 'موبایل', 'rahkar-toolkit' ), __( 'درگاه', 'rahkar-toolkit' ), __( 'نتیجه', 'rahkar-toolkit' ), __( 'متن یا خطا', 'rahkar-toolkit' ), ], true ); if ( [] === $rows ) { echo wp_kses( Ui::tableEmptyRow( 5, __( 'تا حالا هشداری فرستاده نشده است.', 'rahkar-toolkit' ) ), Ui::allowedHtml() ); } foreach ( $rows as $row ) { $phone = PhoneNumber::parse( (string) $row['phone'] ); printf( '%s%s%s%s%s', wp_kses( Ui::badge( (string) wp_date( 'Y/m/d H:i', (int) strtotime( (string) $row['created_at'] . ' UTC' ) ), 'quiet', true ), Ui::allowedHtml() ), esc_html( null !== $phone ? $phone->national() : (string) $row['phone'] ), esc_html( $this->gatewayLabels()[ (string) $row['gateway'] ] ?? (string) $row['gateway'] ), wp_kses( Ui::badge( self::resultLabels()[ (string) $row['result'] ] ?? (string) $row['result'], self::resultVariant( (string) $row['result'] ) ), Ui::allowedHtml() ), esc_html( (string) $row['message'] ) ); } Ui::tableClose(); Ui::panelClose(); } /* * --------------------------------------------------------------------- * Submission * --------------------------------------------------------------------- */ /** * Apply a submission. * * @return array{type: string, message: string} */ private function handleSubmission(): array { $none = [ 'type' => '', 'message' => '', ]; if ( ! isset( $_POST['rahkar_mon_action'] ) ) { return $none; } check_admin_referer( self::NONCE ); $action = sanitize_text_field( wp_unslash( $_POST['rahkar_mon_action'] ) ); if ( 'reset' === $action ) { Failsafe::reset(); return [ 'type' => 'success', 'message' => __( 'ورود پیامکی برگشت. اگر ارسال بعدی هم ناموفق باشد، دوباره کنار می‌رود.', 'rahkar-toolkit' ), ]; } if ( str_starts_with( $action, 'check:' ) ) { $driver = Gateways::available()[ substr( $action, 6 ) ] ?? null; if ( ! $driver instanceof CheckableDriver ) { return $none; } $check = $driver->checkConnection(); return [ 'type' => $check->ok ? 'success' : 'error', 'message' => $driver->name() . ' — ' . $check->message, ]; } $this->save(); if ( 'test' !== $action ) { return [ 'type' => 'success', 'message' => __( 'تنظیمات مانیتورینگ ذخیره شد.', 'rahkar-toolkit' ), ]; } if ( [] === AlertMonitor::recipients() ) { return [ 'type' => 'warning', 'message' => __( 'تنظیمات ذخیره شد، ولی هیچ شماره مدیری وارد نشده است.', 'rahkar-toolkit' ), ]; } $test = AlertMonitor::sendTest(); if ( $test['sent'] > 0 ) { return [ 'type' => [] === $test['errors'] ? 'success' : 'warning', 'message' => sprintf( /* translators: %d: number of administrators reached */ __( 'هشدار آزمایشی برای %d شماره فرستاده شد.', 'rahkar-toolkit' ), $test['sent'] ) . ( [] === $test['errors'] ? '' : ' ' . implode( ' — ', $test['errors'] ) ), ]; } return [ 'type' => 'error', 'message' => __( 'هشدار آزمایشی فرستاده نشد:', 'rahkar-toolkit' ) . ' ' . implode( ' — ', $test['errors'] ), ]; } /** * Persist the alerts form. */ private function save(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified in handleSubmission(), the only caller. $int = static function ( string $key, int $min, int $max, int $fallback ): int { if ( ! isset( $_POST[ $key ] ) ) { return $fallback; } return max( $min, min( $max, absint( wp_unslash( $_POST[ $key ] ) ) ) ); }; $phones = isset( $_POST['alert_phones'] ) ? sanitize_textarea_field( wp_unslash( $_POST['alert_phones'] ) ) : ''; $values = [ 'alert_phones' => $phones, 'alert_fail_on' => ! empty( $_POST['alert_fail_on'] ), 'alert_fail_streak' => $int( 'alert_fail_streak', 0, 100, 5 ), 'alert_fail_rate' => $int( 'alert_fail_rate', 0, 100, 50 ), 'alert_fail_window' => $int( 'alert_fail_window', 5, 1440, 30 ), 'alert_fail_min' => $int( 'alert_fail_min', 1, 1000, 10 ), 'alert_failsafe_on' => ! empty( $_POST['alert_failsafe_on'] ), 'alert_volume_on' => ! empty( $_POST['alert_volume_on'] ), 'alert_volume_hour' => $int( 'alert_volume_hour', 1, 100000, 100 ), 'alert_cooldown' => $int( 'alert_cooldown', 5, 1440, 60 ), 'failsafe_threshold' => $int( 'failsafe_threshold', 0, 100, Failsafe::DEFAULT_THRESHOLD ), 'log_days' => $int( 'log_days', 0, 365, OtpLog::DEFAULT_DAYS ), ]; $patterns = isset( $_POST['alert_pattern'] ) && is_array( $_POST['alert_pattern'] ) ? map_deep( wp_unslash( $_POST['alert_pattern'] ), 'sanitize_text_field' ) : []; $variables = isset( $_POST['alert_var'] ) && is_array( $_POST['alert_var'] ) ? map_deep( wp_unslash( $_POST['alert_var'] ), 'sanitize_text_field' ) : []; // phpcs:enable WordPress.Security.NonceVerification.Missing foreach ( array_keys( Gateways::available() ) as $id ) { $values[ AlertMonitor::patternKey( $id ) ] = trim( (string) ( $patterns[ $id ] ?? '' ) ); $values[ AlertMonitor::variableKey( $id ) ] = trim( (string) ( $variables[ $id ] ?? '' ) ); } Settings::save( OtpAuthModule::SLUG, $values ); } /* * --------------------------------------------------------------------- * Helpers * --------------------------------------------------------------------- */ /** * The tab being shown. */ private function currentTab(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; return in_array( $tab, [ self::TAB_LOG, self::TAB_ALERTS ], true ) ? $tab : self::TAB_OVERVIEW; } /** * URL of one tab. * * @param string $tab Tab id. */ private function url( string $tab ): string { return add_query_arg( [ 'page' => self::SLUG, 'tab' => $tab, ], admin_url( 'admin.php' ) ); } /** * The log filters from the request. * * @return array{phone: string, event: string, result: string, gateway: string} */ private function filters(): array { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only filters. $raw = isset( $_GET['phone'] ) ? sanitize_text_field( wp_unslash( $_GET['phone'] ) ) : ''; $parsed = PhoneNumber::parse( $raw ); $out = [ // A full number is matched in its stored form, so "0912…", "+98912…" // and Persian digits all find the same rows; a fragment is matched as // typed, digits only. 'phone' => null !== $parsed ? $parsed->canonical() : (string) preg_replace( '/\D+/', '', $raw ), 'event' => isset( $_GET['event'] ) ? sanitize_key( wp_unslash( $_GET['event'] ) ) : '', 'result' => isset( $_GET['result'] ) ? sanitize_key( wp_unslash( $_GET['result'] ) ) : '', 'gateway' => isset( $_GET['gateway'] ) ? sanitize_key( wp_unslash( $_GET['gateway'] ) ) : '', ]; // phpcs:enable WordPress.Security.NonceVerification.Recommended return $out; } /** * "5 minutes ago", in the admin's language. * * @param int $time Unix time. */ private function ago( int $time ): string { return sprintf( /* translators: %s: human-readable time difference */ __( '%s پیش', 'rahkar-toolkit' ), human_time_diff( $time ) ); } /** * Gateway names for the log, the test mode pseudo-gateway included. * * @return array */ private function gatewayLabels(): array { $labels = []; foreach ( Gateways::available() as $id => $driver ) { $labels[ $id ] = $driver->name(); } $labels['test_mode'] = __( 'حالت آزمایشی', 'rahkar-toolkit' ); return $labels; } /** * Event names. * * @return array */ private static function eventLabels(): array { return [ OtpLog::EVENT_SEND => __( 'ارسال کد', 'rahkar-toolkit' ), OtpLog::EVENT_VERIFY => __( 'بررسی کد', 'rahkar-toolkit' ), OtpLog::EVENT_LOGIN => __( 'ورود', 'rahkar-toolkit' ), OtpLog::EVENT_SIGNUP => __( 'عضویت', 'rahkar-toolkit' ), OtpLog::EVENT_PASSWORD => __( 'ورود با رمز', 'rahkar-toolkit' ), OtpLog::EVENT_THROTTLED => __( 'محدود شد', 'rahkar-toolkit' ), OtpLog::EVENT_ALERT => __( 'هشدار به مدیر', 'rahkar-toolkit' ), OtpLog::EVENT_FAILSAFE => __( 'failsafe', 'rahkar-toolkit' ), ]; } /** * Result names. * * @return array */ private static function resultLabels(): array { return [ OtpLog::RESULT_OK => __( 'موفق', 'rahkar-toolkit' ), OtpLog::RESULT_TEMPORARY => __( 'خطای موقت', 'rahkar-toolkit' ), OtpLog::RESULT_PERMANENT => __( 'خطای دائمی', 'rahkar-toolkit' ), OtpLog::RESULT_NO_GATEWAY => __( 'بدون درگاه', 'rahkar-toolkit' ), OtpLog::RESULT_TEST => __( 'آزمایشی', 'rahkar-toolkit' ), OtpLog::RESULT_WRONG => __( 'کد اشتباه', 'rahkar-toolkit' ), OtpLog::RESULT_EXPIRED => __( 'منقضی یا نامعتبر', 'rahkar-toolkit' ), OtpLog::RESULT_BLOCKED => __( 'مسدود', 'rahkar-toolkit' ), OtpLog::RESULT_FAILED => __( 'ناموفق', 'rahkar-toolkit' ), ]; } /** * How a result was delivered or attempted. * * @return array */ private static function viaLabels(): array { return [ 'sms' => __( 'پیامک', 'rahkar-toolkit' ), 'voice' => __( 'تماس صوتی', 'rahkar-toolkit' ), 'password' => __( 'رمز عبور', 'rahkar-toolkit' ), 'test' => __( 'آزمایشی', 'rahkar-toolkit' ), AlertMonitor::RULE_FAIL => __( 'ارسال ناموفق', 'rahkar-toolkit' ), AlertMonitor::RULE_FAILSAFE => __( 'failsafe', 'rahkar-toolkit' ), AlertMonitor::RULE_VOLUME => __( 'تعداد غیرعادی', 'rahkar-toolkit' ), ]; } /** * Badge colour for a result. * * @param string $result Result code. */ private static function resultVariant( string $result ): string { if ( OtpLog::RESULT_OK === $result ) { return 'success'; } if ( in_array( $result, [ OtpLog::RESULT_TEST, OtpLog::RESULT_WRONG, OtpLog::RESULT_EXPIRED ], true ) ) { return 'quiet'; } if ( in_array( $result, [ OtpLog::RESULT_BLOCKED, OtpLog::RESULT_TEMPORARY ], true ) ) { return 'warning'; } return 'error'; } }