string[], 'query' => string[], 'currentUser' => string[], * 'userRole' => bool, 'postMeta' => string[], 'userMeta' => string[], 'cookie' => string[] ]. */ public static function collect_refs( array $rules ) { $refs = [ 'urlParam' => [], 'query' => [], 'currentUser' => [], 'userRole' => false, 'postMeta' => [], 'userMeta' => [], 'cookie' => [], ]; foreach ( $rules as $rule ) { self::walk_node( $rule['trigger'] ?? null, $refs ); } foreach ( $refs as $key => $value ) { if ( is_array( $value ) ) { $refs[ $key ] = array_values( array_unique( $value ) ); } } return $refs; } private static function walk_node( $node, array &$refs ) { if ( empty( $node ) ) { return; } if ( 'condition' === ( $node['type'] ?? null ) ) { $source = $node['source'] ?? 'field'; if ( 'userRole' === $source ) { $refs['userRole'] = true; return; } if ( isset( $refs[ $source ] ) && is_array( $refs[ $source ] ) && ! empty( $node['ref'] ) ) { $refs[ $source ][] = $node['ref']; } return; } foreach ( $node['children'] ?? [] as $child ) { self::walk_node( $child, $refs ); } } /** * Builds the full `extra` object for a set of rules — every source * they reference, real data, nothing more. `$post_id` is the post/ * page the form is embedded on (0 if unknown, e.g. a REST-only * context) — required for postMeta to resolve to anything. */ public static function build_extra( array $rules, $post_id = 0 ) { $refs = self::collect_refs( $rules ); $extra = []; if ( ! empty( $refs['urlParam'] ) || ! empty( $refs['query'] ) ) { $extra['urlParams'] = self::referer_query_params(); } if ( ! empty( $refs['currentUser'] ) ) { $extra['currentUser'] = self::current_user_fields( $refs['currentUser'] ); } if ( $refs['userRole'] ) { $extra['userRoles'] = is_user_logged_in() ? wp_get_current_user()->roles : []; } if ( ! empty( $refs['postMeta'] ) && $post_id ) { $extra['postMeta'] = self::scoped_meta( 'post', $post_id, $refs['postMeta'] ); } if ( ! empty( $refs['userMeta'] ) && is_user_logged_in() ) { $extra['userMeta'] = self::scoped_meta( 'user', get_current_user_id(), $refs['userMeta'] ); } if ( ! empty( $refs['cookie'] ) ) { $extra['cookies'] = self::scoped_cookies( $refs['cookie'] ); } return $extra; } /** * Best-effort query-string params from the page that submitted this * request — a form POSTs to admin-post.php, so the ORIGINAL page's * own URL isn't otherwise available server-side at submission time. * Mirrors the same HTTP_REFERER-parsing approach already established * for Entry's `source_url` tracking (public/class-submission- * handler.php) — not a new pattern, reusing a proven one. At front- * end RENDER time (not submission), the caller should prefer * $_SERVER['REQUEST_URI'] instead (see Shortcode::render_form_html()) * since that IS the real current page for that request. */ public static function referer_query_params() { $referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; if ( ! $referer ) { return []; } $parts = wp_parse_url( $referer ); if ( empty( $parts['query'] ) ) { return []; } parse_str( $parts['query'], $params ); return array_map( 'sanitize_text_field', $params ); } /** Query-string params from the CURRENT request's own URL — used at front-end render time, where (unlike submission) the real page URL is directly available. */ public static function current_request_query_params() { if ( empty( $_SERVER['QUERY_STRING'] ) ) { return []; } parse_str( wp_unslash( $_SERVER['QUERY_STRING'] ), $params ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitized below. return array_map( 'sanitize_text_field', $params ); } /** A small, explicit whitelist — never reflects arbitrary WP_User properties, only the specific fields a rule could plausibly need. */ private static function current_user_fields( array $wanted_refs ) { if ( ! is_user_logged_in() ) { return []; } $user = wp_get_current_user(); $allowed = [ 'id' => $user->ID, 'login' => $user->user_login, 'email' => $user->user_email, 'display_name' => $user->display_name, 'first_name' => $user->first_name, 'last_name' => $user->last_name, ]; $out = []; foreach ( $wanted_refs as $ref ) { if ( array_key_exists( $ref, $allowed ) ) { $out[ $ref ] = $allowed[ $ref ]; } } return $out; } /** Shared post/user meta scoping — only the referenced keys, never `_`-prefixed (WordPress's own "private/internal" convention) meta, full stop. */ private static function scoped_meta( $type, $object_id, array $keys ) { $out = []; foreach ( $keys as $key ) { if ( '_' === substr( $key, 0, 1 ) ) { continue; } $out[ $key ] = 'post' === $type ? get_post_meta( $object_id, $key, true ) : get_user_meta( $object_id, $key, true ); } return $out; } private static function scoped_cookies( array $keys ) { $out = []; foreach ( $keys as $key ) { if ( isset( $_COOKIE[ $key ] ) ) { $out[ $key ] = sanitize_text_field( wp_unslash( $_COOKIE[ $key ] ) ); } } return $out; } }