$config Allow-lists and defaults: * - orderby: array columns a caller may sort by (first is default) * - status: array statuses a caller may filter by * - post_type: array post types a caller may filter by * - default_orderby: string * - default_order: 'ASC'|'DESC' * - default_per_page: int (must be in PER_PAGE_ALLOWED) * @return self|WP_Error Validated query, or a 400 WP_Error on a bad value. */ public static function from_request( WP_REST_Request $request, array $config = array() ) { $q = new self(); $page = (int) $request->get_param( 'page' ); $q->page = $page > 0 ? $page : 1; $default_pp = (int) ( $config['default_per_page'] ?? 50 ); if ( ! in_array( $default_pp, self::PER_PAGE_ALLOWED, true ) ) { $default_pp = 50; } $requested_pp = $request->get_param( 'per_page' ); $pp = null === $requested_pp || '' === $requested_pp ? $default_pp : (int) $requested_pp; // A value off the allow-list is not honoured — the ceiling is a hard cap, // not a suggestion, so no request can pull the whole table. $q->per_page = in_array( $pp, self::PER_PAGE_ALLOWED, true ) ? $pp : $default_pp; $q->search = self::clean_text( (string) $request->get_param( 'search' ), 200 ); // orderby: only a column the caller declared, else default; a foreign // value is a client bug and is refused rather than quietly changing sort. $allowed_orderby = array_values( (array) ( $config['orderby'] ?? array() ) ); $default_orderby = (string) ( $config['default_orderby'] ?? ( $allowed_orderby[0] ?? '' ) ); $orderby = (string) $request->get_param( 'orderby' ); if ( '' === $orderby ) { $q->orderby = $default_orderby; } elseif ( in_array( $orderby, $allowed_orderby, true ) ) { $q->orderby = $orderby; } else { return self::bad( 'orderby', $allowed_orderby ); } $order = strtoupper( (string) $request->get_param( 'order' ) ); if ( '' === $order ) { $order = strtoupper( (string) ( $config['default_order'] ?? 'DESC' ) ); } if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) { return self::bad( 'order', array( 'ASC', 'DESC' ) ); } $q->order = $order; $q->status = self::validated( $request->get_param( 'status' ), (array) ( $config['status'] ?? array() ), 'status', $error ); if ( $error instanceof WP_Error ) { return $error; } $q->post_type = self::validated( $request->get_param( 'post_type' ), (array) ( $config['post_type'] ?? array() ), 'post_type', $error ); if ( $error instanceof WP_Error ) { return $error; } $q->lang = self::clean_lang( (string) $request->get_param( 'lang' ) ); $q->from = self::clean_date( (string) $request->get_param( 'from' ) ); $q->to = self::clean_date( (string) $request->get_param( 'to' ) ); return $q; } /** SQL LIMIT offset for classic pagination. */ public function offset(): int { return ( $this->page - 1 ) * $this->per_page; } /** * Validate an optional value against an allow-list. * * @param mixed $value Raw param. * @param array $allowed Allowed values ('' always allowed = no filter). * @param string $field Field name for the error. * @param WP_Error|null $error Set to a 400 when the value is not allowed. */ private static function validated( $value, array $allowed, string $field, &$error ): string { $error = null; $value = sanitize_key( (string) $value ); if ( '' === $value ) { return ''; } if ( in_array( $value, array_map( 'sanitize_key', $allowed ), true ) ) { return $value; } $error = self::bad( $field, $allowed ); return ''; } private static function clean_text( string $value, int $max ): string { return mb_substr( sanitize_text_field( $value ), 0, $max ); } /** * A language code, or '' (all), 'all', or 'untranslated'. A code is accepted * only when it is one the site actually runs, so a stray value cannot scope a * query to a language that does not exist. */ private static function clean_lang( string $value ): string { $value = sanitize_key( $value ); if ( '' === $value || 'all' === $value || 'untranslated' === $value ) { return $value; } if ( class_exists( Multilingual::class ) ) { $state = Multilingual::state(); $codes = array_map( 'strval', array_column( (array) ( $state['languages'] ?? array() ), 'code' ) ); if ( in_array( $value, array_map( 'sanitize_key', $codes ), true ) ) { return $value; } return ''; } return $value; } private static function clean_date( string $value ): string { return 1 === preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ? $value : ''; } /** * @param array $allowed */ private static function bad( string $field, array $allowed ): WP_Error { return new WP_Error( 'airanksuite_invalid_' . $field, sprintf( /* translators: 1: query field name, 2: comma-separated allowed values */ __( 'Invalid %1$s. Allowed: %2$s', 'ai-rank-suite' ), $field, implode( ', ', $allowed ) ), array( 'status' => 400 ) ); } }