` can * only end the comment. * * @param string $markup Serialized blocks. * @return string[] */ function airowp_query_ids_in_markup( $markup ) { if ( false === strpos( $markup, 'wp:airo-wp/query ' ) ) { return array(); } $ids = array(); if ( preg_match_all( '##', $markup, $m ) ) { foreach ( $m[1] as $json ) { $attrs = json_decode( $json, true ); if ( is_array( $attrs ) && ! empty( $attrs['queryId'] ) && is_string( $attrs['queryId'] ) ) { $ids[] = sanitize_key( $attrs['queryId'] ); } } } return array_values( array_unique( $ids ) ); } endif; if ( ! function_exists( 'airowp_query_known_ids' ) ) : /** * Query ids known for this request: those in the current post's content * (found up front, so the first Query already knows about later ones) * plus any rendered so far (templates, patterns). * * @return string[] */ function airowp_query_known_ids() { $sources = array(); $post = get_post(); if ( $post instanceof WP_Post ) { $sources[] = $post->post_content; } // Block themes: the resolved template (not its template parts). if ( isset( $GLOBALS['_wp_current_template_content'] ) && is_string( $GLOBALS['_wp_current_template_content'] ) ) { $sources[] = $GLOBALS['_wp_current_template_content']; } static $cache = array(); $ids = array(); foreach ( $sources as $source ) { $hash = md5( $source ); if ( ! isset( $cache[ $hash ] ) ) { $cache[ $hash ] = airowp_query_ids_in_markup( $source ); } $ids = array_merge( $ids, $cache[ $hash ] ); } if ( isset( $GLOBALS['airowp_query_rendered_ids'] ) && is_array( $GLOBALS['airowp_query_rendered_ids'] ) ) { $ids = array_merge( $ids, array_map( 'strval', array_keys( $GLOBALS['airowp_query_rendered_ids'] ) ) ); } return array_values( array_unique( $ids ) ); } endif; if ( ! function_exists( 'airowp_query_page_is_multi' ) ) : /** * Whether this request renders more than one Query, so controls must * write scoped keys. One Query keeps plain keys: `?q=` stays readable and * site-search analytics (which look for `q`) keep working. * * @return bool */ function airowp_query_page_is_multi() { if ( ! airowp_query_scoping_enabled() ) { return false; } // A REST refresh renders one Query alone; the browser, which can see // the whole page, says whether it shares it with others. if ( ! empty( $GLOBALS['airowp_query_force_multi'] ) ) { return true; } return count( airowp_query_known_ids() ) > 1; } endif; if ( ! function_exists( 'airowp_query_split_param_key' ) ) : /** * Split a URL key into its bare name and the Query it is scoped for. * * Only the text after the LAST `__` can be a Query id, and only when it * is one: the caller's own id, an id known for this request, or the shape * DSGo generates (`q` + 8 hex from the editor, `q-` + 10 hex from a * template import). So a custom taxonomy such as `filter_my__tax` stays * an ordinary bare key. * * @param string $key Sanitized URL key. * @param string $own_id The caller's queryId, if any. * @return array{0:string,1:string} Bare name, and the Query id or ''. */ function airowp_query_split_param_key( $key, $own_id = '' ) { $pos = strrpos( $key, '__' ); if ( false === $pos || 0 === $pos ) { return array( $key, '' ); } $id = substr( $key, $pos + 2 ); if ( '' === $id ) { return array( $key, '' ); } if ( ( '' !== $own_id && $id === $own_id ) || preg_match( '/^q(?:[0-9a-f]{8}|-[0-9a-f]{10})$/', $id ) || in_array( $id, airowp_query_known_ids(), true ) ) { return array( substr( $key, 0, $pos ), $id ); } return array( $key, '' ); } endif; if ( ! function_exists( 'airowp_query_scoped_param_name' ) ) : /** * The URL key a filter control for this Query writes. * * @param string $param Bare param name. * @param string $query_id Sanitized queryId. * @return string `{param}__{queryId}`, or the bare name when scoping is off. */ function airowp_query_scoped_param_name( $param, $query_id ) { if ( '' === $query_id || ! airowp_query_scoping_enabled() ) { return $param; } return $param . '__' . $query_id; } endif; if ( ! function_exists( 'airowp_query_scalar_param' ) ) : /** * Reduce a raw request value to a scalar string or a flat list of scalar * strings. Nested arrays, objects and other non-scalars (`?x[a][]=1`) are * dropped so callers can safely run string functions over the result. * * @param mixed $value Raw (possibly array) request value. * @return string|string[] A scalar string, or a flat list of strings. */ function airowp_query_scalar_param( $value ) { if ( is_array( $value ) ) { $out = array(); foreach ( $value as $item ) { if ( is_scalar( $item ) ) { $out[] = (string) $item; } } return $out; } return is_scalar( $value ) ? (string) $value : ''; } endif; if ( ! function_exists( 'airowp_query_owned_request_params' ) ) : /** * The $_GET entries that apply to one Query, keyed by bare name. * * A key scoped for this Query wins over the same bare key, whatever the * order in $_GET. A key scoped for another Query is skipped. Values are * exactly as in $_GET (slashed, unsanitized), so each caller unslashes * and sanitizes as it needs. * * @param string $query_id Sanitized queryId. * @return array */ function airowp_query_owned_request_params( $query_id ) { $owned = array(); foreach ( (array) $_GET as $raw_key => $raw_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $key = sanitize_key( (string) $raw_key ); if ( '' === $key ) { continue; } list( $bare, $scope ) = airowp_query_split_param_key( $key, $query_id ); if ( '' !== $scope && $scope !== $query_id ) { continue; } $scoped = '' !== $scope; // A scoped entry is never replaced; a bare one only by a scoped one. if ( isset( $owned[ $bare ] ) && ( $owned[ $bare ]['scoped'] || ! $scoped ) ) { continue; } $owned[ $bare ] = array( 'value' => $raw_value, 'key' => (string) $raw_key, 'scoped' => $scoped, ); } return $owned; } endif; if ( ! function_exists( 'airowp_query_page_param_name' ) ) : /** * The URL key numbered pagination uses for this Query. * * With one Query on the page, WordPress's own `paged` (pretty `/page/2/` * links). With several, `qpage__{queryId}`, so paging one Query doesn't * page the others. * * @param string $query_id Sanitized queryId. * @return string */ function airowp_query_page_param_name( $query_id ) { return airowp_query_page_is_multi() && '' !== $query_id ? 'qpage__' . $query_id : 'paged'; } endif; if ( ! function_exists( 'airowp_query_current_page' ) ) : /** * The current page for a Query: its own `qpage__{queryId}` when present, * else WordPress's `paged` / `page` query vars. * * @param string $query_id Sanitized queryId. * @return int */ function airowp_query_current_page( $query_id ) { if ( '' !== $query_id && isset( $_GET[ 'qpage__' . $query_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return max( 1, absint( wp_unslash( $_GET[ 'qpage__' . $query_id ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- absint(). } $page = max( 1, absint( get_query_var( 'paged' ) ) ); if ( 1 === $page ) { $page = max( 1, absint( get_query_var( 'page' ) ) ); } return $page; } endif;