' . esc_html__( 'برای ورود و ثبت‌نام با کد پیامکی، شماره موبایل شما همراه با حساب کاربری‌تان نگه‌داری می‌شود. کد یک‌بار مصرف به‌صورت رمزشده و فقط تا چند دقیقه ذخیره می‌شود و خود کد هرگز ثبت نمی‌شود.', 'rahkar-toolkit' ) . '

' . '

' . esc_html__( 'برای جلوگیری از سوءاستفاده و پیگیری مشکلات ارسال، هر درخواست کد، بررسی کد و ورود با شماره موبایل و نشانی IP ثبت می‌شود و این گزارش پس از مدتی که مدیر سایت تعیین کرده (به‌طور پیش‌فرض ۳۰ روز) پاک می‌شود.', 'rahkar-toolkit' ) . '

' . '

' . esc_html__( 'برای فرستادن پیامک، شماره موبایل و متن پیام (از جمله کد ورود) به سرویس پیامکی IPPanel ارسال می‌شود.', 'rahkar-toolkit' ) . '

'; wp_add_privacy_policy_content( __( 'راهکار وردپرس', 'rahkar-toolkit' ), $content ); } /** * Register the exporter. * * @param mixed $exporters Registered exporters. * * @return mixed * * @internal Hooked to `wp_privacy_personal_data_exporters`. */ public static function registerExporter( $exporters ) { if ( is_array( $exporters ) ) { $exporters[ self::ID ] = [ 'exporter_friendly_name' => __( 'ورود پیامکی', 'rahkar-toolkit' ), 'callback' => [ self::class, 'export' ], ]; } return $exporters; } /** * Register the eraser. * * @param mixed $erasers Registered erasers. * * @return mixed * * @internal Hooked to `wp_privacy_personal_data_erasers`. */ public static function registerEraser( $erasers ) { if ( is_array( $erasers ) ) { $erasers[ self::ID ] = [ 'eraser_friendly_name' => __( 'ورود پیامکی', 'rahkar-toolkit' ), 'callback' => [ self::class, 'erase' ], ]; } return $erasers; } /** * Export one page of a person's data. * * @param string $email Their email address. * @param int $page 1-based page. * * @return array{data: list>, done: bool} */ public static function export( string $email, int $page = 1 ): array { $user = get_user_by( 'email', $email ); $phones = self::phonesOf( $user instanceof \WP_User ? $user->ID : 0 ); $data = []; if ( 1 === $page && [] !== $phones ) { $data[] = [ 'group_id' => 'rahkar-wp-phone', 'group_label' => __( 'شماره موبایل ورود', 'rahkar-toolkit' ), 'item_id' => 'rahkar-wp-phone', 'data' => [ [ 'name' => __( 'شماره موبایل', 'rahkar-toolkit' ), 'value' => implode( '، ', $phones ), ], ], ]; } $rows = self::logRows( $user instanceof \WP_User ? $user->ID : 0, $phones, $page ); foreach ( $rows as $row ) { $data[] = [ 'group_id' => 'rahkar-wp-log', 'group_label' => __( 'گزارش ورود پیامکی', 'rahkar-toolkit' ), 'item_id' => 'rahkar-wp-log-' . $row['id'], 'data' => [ [ 'name' => __( 'زمان', 'rahkar-toolkit' ), 'value' => (string) $row['created_at'] . ' UTC', ], [ 'name' => __( 'رویداد', 'rahkar-toolkit' ), 'value' => (string) $row['event'] . ' / ' . (string) $row['result'], ], [ 'name' => __( 'شماره موبایل', 'rahkar-toolkit' ), 'value' => (string) $row['phone'], ], [ 'name' => __( 'نشانی IP', 'rahkar-toolkit' ), 'value' => (string) $row['ip'], ], ], ]; } return [ 'data' => $data, 'done' => count( $rows ) < self::PAGE, ]; } /** * Erase one page of a person's data. * * @param string $email Their email address. * @param int $page 1-based page; each call deletes a batch, so it restarts at the top. * * @return array{items_removed: bool, items_retained: bool, messages: list, done: bool} */ public static function erase( string $email, int $page = 1 ): array { global $wpdb; unset( $page ); $user = get_user_by( 'email', $email ); $userId = $user instanceof \WP_User ? $user->ID : 0; $phones = self::phonesOf( $userId ); $ids = array_map( static fn( array $row ): int => (int) $row['id'], self::logRows( $userId, $phones, 1 ) ); $removed = 0; if ( [] !== $ids ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- One %d per id, built from their count. $removed = (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ')', array_merge( [ Schema::table( 'otp_log' ) ], $ids ) ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter } foreach ( $phones as $phone ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $removed += (int) $wpdb->delete( Schema::table( 'otp' ), [ 'identifier' => $phone ], [ '%s' ] ); } $done = count( $ids ) < self::PAGE; return [ 'items_removed' => $removed > 0, 'items_retained' => $done && [] !== $phones, 'messages' => $done && [] !== $phones ? [ __( 'شماره موبایل حساب نگه داشته شد، چون راه ورود کاربر به همین حساب است. با حذف حساب کاربری، این شماره هم حذف می‌شود.', 'rahkar-toolkit' ) ] : [], 'done' => $done, ]; } /** * The mobile numbers linked to an account, canonical. * * @param int $userId User id; 0 for none. * * @return list */ private static function phonesOf( int $userId ): array { global $wpdb; if ( $userId <= 0 ) { return []; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery $phones = $wpdb->get_col( $wpdb->prepare( 'SELECT value FROM %i WHERE user_id = %d AND type = %s', Schema::table( 'identity' ), $userId, 'phone' ) ); $meta = (string) get_user_meta( $userId, 'rahkar_phone', true ); return array_values( array_unique( array_filter( array_merge( array_map( 'strval', (array) $phones ), [ $meta ] ) ) ) ); } /** * One page of sign-in log rows about a person: by account or by number. * * @param int $userId User id; 0 for none. * @param list $phones Their numbers. * @param int $page 1-based page. * * @return list> */ private static function logRows( int $userId, array $phones, int $page ): array { global $wpdb; if ( $userId <= 0 && [] === $phones ) { return []; } $where = [ 'user_id = %d' ]; $args = [ max( 0, $userId ) ]; if ( [] !== $phones ) { $where[] = 'phone IN (' . implode( ',', array_fill( 0, count( $phones ), '%s' ) ) . ')'; $args = array_merge( $args, $phones ); } // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Fixed fragments; one placeholder per value. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id, event, result, phone, ip, created_at FROM %i WHERE (' . implode( ' OR ', $where ) . ') ORDER BY id LIMIT %d OFFSET %d', array_merge( [ Schema::table( 'otp_log' ) ], $args, [ self::PAGE, ( max( 1, $page ) - 1 ) * self::PAGE ] ) ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter return is_array( $rows ) ? array_values( $rows ) : []; } }