arg_prefix(); $filters = array(); foreach ( $d->filters() as $filter ) { $filters[ $filter->key ] = $p . $filter->key; } return array( 'page' => '' === $p ? 'paged' : $p . 'page', 'per_page' => $p . 'per_page', 'orderby' => $p . 'orderby', 'order' => $p . 'order', 'search' => '' === $p ? 's' : $p . 'search', 'after' => $p . 'after', 'filters' => $filters, ); } public static function from_args( TableDefinition $d, array $get, array $body = array() ): self { $names = self::arg_names( $d ); $filters = array(); foreach ( $names['filters'] as $key => $arg ) { if ( array_key_exists( $arg, $get ) ) { $filters[ $key ] = $get[ $arg ]; } } $search = 'post' === $d->search_transport() ? ( $body[ $names['search'] ] ?? '' ) : ( $get[ $names['search'] ] ?? ( $body[ $names['search'] ] ?? '' ) ); return self::build( $d, array( 'page' => $get[ $names['page'] ] ?? 1, 'per_page' => $get[ $names['per_page'] ] ?? 0, 'orderby' => $get[ $names['orderby'] ] ?? null, 'order' => $get[ $names['order'] ] ?? null, 'search' => $search, 'filters' => $filters, 'after' => $get[ $names['after'] ] ?? '', ), $d->context(), null ); } public static function from_state( TableDefinition $d, array $state, array $context, ?int $known_total = null ): self { $q = self::build( $d, array( 'page' => $state['page'] ?? 1, 'per_page' => $state['per_page'] ?? 0, 'orderby' => $state['orderby'] ?? null, 'order' => $state['order'] ?? null, 'search' => $state['search'] ?? '', 'filters' => is_array( $state['filters'] ?? null ) ? $state['filters'] : array(), 'after' => $state['after'] ?? '', ), $context, null ); $hash = isset( $state['hash'] ) && is_string( $state['hash'] ) ? $state['hash'] : ''; if ( null !== $known_total && $known_total >= 0 && '' !== $hash && hash_equals( $q->hash(), $hash ) ) { return $q->with( array( 'known_total' => $known_total ) ); } return $q; } public static function from_request( TableDefinition $d ): self { return self::from_args( $d, TableRequest::args( $d ), TableRequest::posted_search( $d ) ); } private static function build( TableDefinition $d, array $raw, array $context, ?int $known_total ): self { $options = array_values( array_filter( array_map( 'intval', $d->per_page_options() ), static fn( int $n ): bool => $n > 0 ) ); $default = in_array( 50, $options, true ) || array() === $options ? 50 : $options[0]; $per_page = is_scalar( $raw['per_page'] ) && is_numeric( $raw['per_page'] ) ? (int) $raw['per_page'] : 0; $per_page = in_array( $per_page, $options, true ) ? $per_page : $default; $page = is_scalar( $raw['page'] ) && is_numeric( $raw['page'] ) ? (int) $raw['page'] : 1; $page = max( 1, min( 1000000, $page ) ); list( $default_by, $default_order ) = $d->default_order(); $default_order = 'desc' === $default_order ? 'desc' : 'asc'; $sortable = array(); foreach ( $d->columns() as $column ) { if ( $column->sortable ) { $sortable[] = $column->key; } } $orderby = is_string( $raw['orderby'] ) ? sanitize_key( $raw['orderby'] ) : ''; $order = is_string( $raw['order'] ) ? strtolower( $raw['order'] ) : ''; if ( ! in_array( $orderby, $sortable, true ) ) { $orderby = (string) $default_by; } if ( ! in_array( $order, array( 'asc', 'desc' ), true ) ) { $order = $default_order; } $search = is_scalar( $raw['search'] ) ? trim( sanitize_text_field( (string) $raw['search'] ) ) : ''; $search = '' === $d->search_label() ? '' : mb_substr( $search, 0, self::SEARCH_MAX ); $filters = array(); foreach ( $d->filters() as $filter ) { if ( ! array_key_exists( $filter->key, $raw['filters'] ) ) { continue; } $value = $raw['filters'][ $filter->key ]; if ( Filter::MULTI === $filter->type ) { $values = array(); foreach ( is_array( $value ) ? $value : array( $value ) as $one ) { $one = is_scalar( $one ) ? (string) $one : ''; if ( $filter->allows( $one ) && ! in_array( $one, $values, true ) && count( $values ) < self::MULTI_MAX ) { $values[] = $one; } } if ( array() !== $values ) { $filters[ $filter->key ] = $values; } continue; } $value = is_scalar( $value ) ? (string) $value : ''; if ( $filter->allows( $value ) ) { $filters[ $filter->key ] = $value; } } $after = is_string( $raw['after'] ) ? $raw['after'] : ''; $after = 'keyset' === $d->paging() && null !== self::decode_after( $after ) ? $after : ''; return new self( $page, $per_page, $orderby, $order, $search, $filters, $after, $context, $known_total ); } public function with( array $change ): self { $filters = $this->filters; if ( isset( $change['filters'] ) && is_array( $change['filters'] ) ) { foreach ( $change['filters'] as $key => $value ) { if ( null === $value || '' === $value || array() === $value ) { unset( $filters[ $key ] ); } else { $filters[ $key ] = $value; } } } return new self( (int) ( $change['page'] ?? $this->page ), (int) ( $change['per_page'] ?? $this->per_page ), (string) ( $change['orderby'] ?? $this->orderby ), (string) ( $change['order'] ?? $this->order ), array_key_exists( 'search', $change ) ? (string) $change['search'] : $this->search, $filters, array_key_exists( 'after', $change ) ? (string) $change['after'] : $this->after, $this->context, array_key_exists( 'known_total', $change ) ? ( null === $change['known_total'] ? null : (int) $change['known_total'] ) : $this->known_total ); } public function args( TableDefinition $d, array $change = array() ): array { $q = array() === $change ? $this : $this->with( $change ); $names = self::arg_names( $d ); list( $default_by, $default_order ) = $d->default_order(); $default_order = 'desc' === $default_order ? 'desc' : 'asc'; $options = array_map( 'intval', $d->per_page_options() ); $default_size = in_array( 50, $options, true ) || array() === $options ? 50 : (int) $options[0]; $out = array(); if ( $q->page > 1 ) { $out[ $names['page'] ] = (string) $q->page; } if ( $q->per_page !== $default_size ) { $out[ $names['per_page'] ] = (string) $q->per_page; } if ( $q->orderby !== (string) $default_by || $q->order !== $default_order ) { $out[ $names['orderby'] ] = $q->orderby; $out[ $names['order'] ] = $q->order; } foreach ( $q->filters as $key => $value ) { if ( isset( $names['filters'][ $key ] ) ) { $out[ $names['filters'][ $key ] ] = is_array( $value ) ? array_values( array_map( 'strval', $value ) ) : (string) $value; } } if ( '' !== $q->after ) { $out[ $names['after'] ] = $q->after; } return $out; } public function url( TableDefinition $d, array $change = array() ): string { $args = array(); foreach ( $this->args( $d, $change ) as $name => $value ) { $args[ $name ] = is_array( $value ) ? array_map( 'rawurlencode', $value ) : rawurlencode( $value ); } return \RoosterLMS\Admin\View::link( add_query_arg( $args, self::strip( $d, $d->base_url() ) ) ); } public static function strip( TableDefinition $d, string $url ): string { $names = self::arg_names( $d ); $keys = array_merge( array( $names['page'], $names['per_page'], $names['orderby'], $names['order'], $names['search'], $names['after'] ), array_values( $names['filters'] ) ); return remove_query_arg( $keys, $url ); } public function hash(): string { $filters = $this->filters; ksort( $filters ); foreach ( $filters as $key => $value ) { if ( is_array( $value ) ) { sort( $value, SORT_STRING ); $filters[ $key ] = $value; } } return substr( hash( 'sha256', (string) wp_json_encode( array( $filters, $this->search, $this->context ) ) ), 0, 16 ); } public function filtered(): bool { return '' !== $this->search || array() !== $this->filters; } public function offset(): int { return ( $this->page - 1 ) * $this->per_page; } public function filter( string $key ): string|array { return $this->filters[ $key ] ?? ''; } public function values( string $key ): array { $value = $this->filters[ $key ] ?? ''; return array_values( array_filter( array_map( 'strval', (array) $value ), static fn( string $v ): bool => '' !== $v ) ); } public function ints( string $key ): array { return array_values( array_unique( array_filter( array_map( 'intval', $this->values( $key ) ), static fn( int $n ): bool => $n > 0 ) ) ); } public function after_key(): ?array { return '' === $this->after ? null : self::decode_after( $this->after ); } public static function encode_after( array $key ): string { return rtrim( strtr( base64_encode( (string) wp_json_encode( $key ) ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- an opaque URL-safe cursor, not obfuscation. } public static function decode_after( string $after ): ?array { if ( '' === $after || strlen( $after ) > self::AFTER_MAX || 1 !== preg_match( '/^[A-Za-z0-9_-]+$/', $after ) ) { return null; } $json = base64_decode( strtr( $after, '-_', '+/' ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- decodes the URL-safe cursor encode_after() made (not obfuscation). $data = is_string( $json ) ? json_decode( $json, true ) : null; if ( ! is_array( $data ) || array() === $data || array_is_list( $data ) ) { return null; } foreach ( $data as $value ) { if ( ! is_scalar( $value ) && null !== $value ) { return null; } } return $data; } }