PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.1
10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Api / Infrastructure / MetadataController.php
woocommerce / src / Api / Infrastructure Last commit date
Schema 4 weeks ago ClassResolver.php 4 weeks ago GraphQLControllerBase.php 4 weeks ago Main.php 4 weeks ago MetadataController.php 4 weeks ago Principal.php 4 weeks ago PrincipalResolver.php 4 weeks ago QueryInfoExtractor.php 4 weeks ago ResolverHelpers.php 4 weeks ago
MetadataController.php
397 lines
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Automattic\WooCommerce\Api\Infrastructure;
6
7 use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
8 use Automattic\WooCommerce\Api\Infrastructure\Schema\Error;
9 use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
10 use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
11 use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
12 use Automattic\WooCommerce\Api\Utils\SchemaHandle;
13 use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
14 use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
15 use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
16 use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
17 use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
18
19 /**
20 * Hand-written controller that contributes the `_apiMetadata` root query
21 * field and the supporting `MetadataEntry`, `MetadataTarget`, `MetadataValue`
22 * and `AuthEntry` types to the generated schema.
23 *
24 * The autogenerated `RootQueryType` references this controller alongside the
25 * autogenerated query resolvers, so the field appears on the root `Query`
26 * type without any special wiring at the controller level. The resolver
27 * delegates to {@see SchemaHandle::find_metadata()} for the schema walk and
28 * filter application, then reshapes the rows so each entry is exposed as the
29 * `{ name, value }` pair that `MetadataEntry` expects. Authorization
30 * descriptors (each row's `authorization` list) pass through unchanged.
31 *
32 * Access is gated by {@see self::can_query_metadata()}; once allowed, the
33 * returned content is principal-independent — the full declared shape of the
34 * schema, irrespective of who is calling.
35 */
36 class MetadataController {
37 /**
38 * Memoised `MetadataValue` scalar type.
39 *
40 * @var ?CustomScalarType
41 */
42 private static ?CustomScalarType $value_scalar = null;
43
44 /**
45 * Memoised `MetadataEntry` output type.
46 *
47 * @var ?ObjectType
48 */
49 private static ?ObjectType $entry_type = null;
50
51 /**
52 * Memoised `MetadataTarget` output type.
53 *
54 * @var ?ObjectType
55 */
56 private static ?ObjectType $target_type = null;
57
58 /**
59 * Memoised `AuthEntry` output type — describes one authorization
60 * attribute attached to a schema target.
61 *
62 * @var ?ObjectType
63 */
64 private static ?ObjectType $auth_entry_type = null;
65
66 /**
67 * GraphQL field name used on the root `Query` type.
68 */
69 public const FIELD_NAME = '_apiMetadata';
70
71 /**
72 * Field definition for the root `_apiMetadata` query, in the shape the
73 * autogenerated `RootQueryType` expects (same as every autogenerated
74 * resolver's `get_field_definition()`).
75 *
76 * @return array<string, mixed>
77 */
78 public static function get_field_definition(): array {
79 return array(
80 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_target_type() ) ) ),
81 'description' => __(
82 'Lists metadata attached to elements of this schema. All filter arguments are optional; supplying multiple narrows the result. Use this to discover internal-use APIs, beta features, ownership, etc., or to ask "can I use this specific element?".',
83 'woocommerce'
84 ),
85 'args' => array(
86 'name' => array(
87 'type' => Type::string(),
88 'description' => __( 'Match rows that carry a metadata entry with this name. Surviving rows have their entries trimmed to the matching one.', 'woocommerce' ),
89 ),
90 'type' => array(
91 'type' => Type::string(),
92 'description' => __( 'Match rows whose target type equals this name.', 'woocommerce' ),
93 ),
94 'field' => array(
95 'type' => Type::string(),
96 'description' => __( 'Match rows whose target field equals this name.', 'woocommerce' ),
97 ),
98 'attribute' => array(
99 'type' => Type::string(),
100 'description' => __( 'Match rows whose authorization carries an attribute with this class short name. Surviving rows have their authorization trimmed to the matching descriptors.', 'woocommerce' ),
101 ),
102 ),
103 'resolve' => array( self::class, 'resolve' ),
104 );
105 }
106
107 /**
108 * Resolver for the `_apiMetadata` root field. Signature matches the
109 * engine's resolver contract; `$root` is unused here (root operations
110 * have no parent). `$context` is read for the principal so the
111 * `can_query_metadata` ladder can run.
112 *
113 * @param ?array $root The engine passes null for root resolvers.
114 * @param array $args GraphQL arguments (`name`, `type`, `field`, `attribute`).
115 * @param mixed $context Per-request context — an ArrayObject wrapping {`principal`, `_query_metadata`}.
116 * @param ResolveInfo $info Carries the schema instance to walk.
117 * @return list<array<string, mixed>>
118 * @throws Error When the principal is not allowed to query `_apiMetadata`.
119 */
120 public static function resolve( ?array $root, array $args, mixed $context, ResolveInfo $info ): array {
121 unset( $root );
122
123 $principal = is_object( $context ) || is_array( $context ) ? ( $context['principal'] ?? null ) : null;
124 if ( ! self::can_query_metadata( $principal ) ) {
125 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Static error message + machine code; serialized as JSON, not HTML.
126 throw self::build_metadata_query_authorization_error( $principal );
127 }
128
129 // Wrap the resolver's engine-typed schema into the same handle clients
130 // receive from `GraphQLControllerBase::get_schema()`, so the resolver and
131 // PHP-side callers share a single inspection surface.
132 $schema = new SchemaHandle( $info->schema );
133
134 $rows = $schema->find_metadata(
135 $args['name'] ?? null,
136 $args['type'] ?? null,
137 $args['field'] ?? null,
138 $args['attribute'] ?? null,
139 );
140
141 // SchemaHandle returns entries as an associative `name => value` map,
142 // which is the natural shape for filtering and PHP-side consumers. The
143 // GraphQL `MetadataEntry` type instead exposes each entry as a
144 // `{ name, value }` object so clients can `entries { name value }` over
145 // a list. Reshape here.
146 return array_map(
147 static function ( array $row ): array {
148 $row['entries'] = array_map(
149 static fn( string $entry_name, $entry_value ): array => array(
150 'name' => $entry_name,
151 'value' => $entry_value,
152 ),
153 array_keys( $row['entries'] ),
154 array_values( $row['entries'] ),
155 );
156 return $row;
157 },
158 $rows
159 );
160 }
161
162 /**
163 * Whether the principal may run the `_apiMetadata` query.
164 *
165 * Tri-tier ladder, deliberately fail-closed:
166 *
167 * 1. If the principal declares `can_query_metadata(): bool`, use it.
168 * Plugins distinguish metadata-query access from native
169 * introspection access by declaring this method.
170 * 2. Else if the principal declares `can_introspect(): bool`, fall
171 * back to it — one switch then gates both metadata and
172 * introspection, which is the common case.
173 * 3. Else (neither method declared) deny. Plugin authors that don't
174 * opt their principal in get a locked-down endpoint rather than
175 * leaking schema shape and gate descriptors by default.
176 *
177 * The principal-derived decision is then passed through the
178 * {@see 'woocommerce_graphql_can_query_metadata'} filter so sites
179 * can grant or revoke access without subclassing the principal —
180 * useful for per-request rules (specific IPs, headers, query
181 * parameters, etc.).
182 *
183 * Fail-closed contract: null principal denies before the filter is
184 * consulted; either method's return is checked with `=== true`; any
185 * throw from the principal method or the filter denies; the filter
186 * must likewise return strictly `true` to allow.
187 *
188 * @param ?object $principal The resolved principal, or null when principal resolution failed.
189 */
190 private static function can_query_metadata( ?object $principal ): bool {
191 if ( null === $principal ) {
192 return false;
193 }
194
195 try {
196 if ( method_exists( $principal, 'can_query_metadata' ) ) {
197 $allowed = true === $principal->can_query_metadata();
198 } elseif ( method_exists( $principal, 'can_introspect' ) ) {
199 $allowed = true === $principal->can_introspect();
200 } else {
201 $allowed = false;
202 }
203
204 /**
205 * Filters whether the current principal may run the `_apiMetadata` query.
206 *
207 * The filter receives the principal-derived decision (see the tri-tier
208 * ladder in {@see MetadataController::can_query_metadata()}) and must
209 * return strictly `true` to grant access; any other return value
210 * denies. The filter is not invoked when principal resolution failed
211 * (i.e. when the resolver receives a null principal) — that case
212 * denies outright.
213 *
214 * @since 10.9.0
215 *
216 * @internal
217 *
218 * @param bool $allowed Whether the principal may query `_apiMetadata`.
219 * @param object $principal The resolved principal.
220 */
221 $allowed = apply_filters( 'woocommerce_graphql_can_query_metadata', $allowed, $principal );
222 } catch ( \Throwable $e ) {
223 return false;
224 }
225
226 return true === $allowed;
227 }
228
229 /**
230 * Build the GraphQL error thrown when `_apiMetadata` is queried by a
231 * principal that cannot. Mirrors
232 * {@see ResolverHelpers::build_authorization_error()}'s
233 * UNAUTHORIZED / FORBIDDEN distinction so clients can branch on
234 * `extensions.code` the same way they do for field-level denies.
235 *
236 * @param ?object $principal The resolved principal (null when principal resolution failed).
237 */
238 private static function build_metadata_query_authorization_error( ?object $principal ): Error {
239 $is_anonymous = null === $principal
240 || ( method_exists( $principal, 'is_authenticated' ) && ! $principal->is_authenticated() );
241 return new Error(
242 $is_anonymous ? 'Authentication required.' : 'You do not have permission to perform this action.',
243 extensions: array( 'code' => $is_anonymous ? 'UNAUTHORIZED' : 'FORBIDDEN' )
244 );
245 }
246
247 /**
248 * The `MetadataTarget` output type, lazily built and cached.
249 */
250 private static function get_target_type(): ObjectType {
251 if ( null === self::$target_type ) {
252 self::$target_type = new ObjectType(
253 array(
254 'name' => 'MetadataTarget',
255 'description' => __(
256 'One element of the schema with its attached metadata. Type-level rows have `field`, `argument` and `enumValue` set to null; field-level rows set `field` (and `argument` when the target is a field argument); enum-value rows set `enumValue`.',
257 'woocommerce'
258 ),
259 'fields' => fn() => array(
260 'type' => array(
261 'type' => Type::nonNull( Type::string() ),
262 'description' => __( 'Name of the GraphQL type this row describes.', 'woocommerce' ),
263 ),
264 'field' => array(
265 'type' => Type::string(),
266 'description' => __( 'Field name when this row describes a field (or a field argument); null for type-level rows.', 'woocommerce' ),
267 ),
268 'argument' => array(
269 'type' => Type::string(),
270 'description' => __( 'Argument name when this row describes a field argument; null otherwise.', 'woocommerce' ),
271 ),
272 'enumValue' => array(
273 'type' => Type::string(),
274 'description' => __( 'Enum value name when this row describes one specific enum value; null otherwise.', 'woocommerce' ),
275 ),
276 'entries' => array(
277 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_entry_type() ) ) ),
278 'description' => __( 'Metadata entries attached to the target.', 'woocommerce' ),
279 ),
280 'authorization' => array(
281 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_auth_entry_type() ) ) ),
282 'description' => __( 'Authorization attributes attached to the target (e.g. `RequiredCapability`, `PublicAccess`, or plugin-defined). Empty when the target carries no authorization attributes.', 'woocommerce' ),
283 ),
284 ),
285 )
286 );
287 }
288 return self::$target_type;
289 }
290
291 /**
292 * The `AuthEntry` output type — one authorization attribute attached
293 * to a target. Carries the attribute's short class name and the
294 * scalar args supplied at the usage site.
295 */
296 private static function get_auth_entry_type(): ObjectType {
297 if ( null === self::$auth_entry_type ) {
298 self::$auth_entry_type = new ObjectType(
299 array(
300 'name' => 'AuthEntry',
301 'description' => __( 'One authorization attribute attached to a schema target.', 'woocommerce' ),
302 'fields' => fn() => array(
303 'attribute' => array(
304 'type' => Type::nonNull( Type::string() ),
305 'description' => __( 'Short class name of the authorization attribute (e.g. `RequiredCapability`).', 'woocommerce' ),
306 ),
307 'args' => array(
308 'type' => Type::nonNull( Type::listOf( self::get_value_scalar() ) ),
309 'description' => __( 'Constructor arguments supplied at the usage site, in source order. Element type is the same scalar union as `MetadataValue`.', 'woocommerce' ),
310 ),
311 ),
312 )
313 );
314 }
315 return self::$auth_entry_type;
316 }
317
318 /**
319 * The `MetadataEntry` output type, lazily built and cached.
320 */
321 private static function get_entry_type(): ObjectType {
322 if ( null === self::$entry_type ) {
323 self::$entry_type = new ObjectType(
324 array(
325 'name' => 'MetadataEntry',
326 'description' => __( 'One metadata entry: a `name` plus a scalar `value`.', 'woocommerce' ),
327 'fields' => fn() => array(
328 'name' => array(
329 'type' => Type::nonNull( Type::string() ),
330 'description' => __( 'Identifier of the entry (e.g. `internal`, `beta`).', 'woocommerce' ),
331 ),
332 'value' => array(
333 // Nullable: `MetadataValue` itself permits a null payload (e.g.
334 // `#[Metadata( 'deprecated_reason', null )]`), so the wrapping
335 // must allow it through.
336 'type' => self::get_value_scalar(),
337 'description' => __( 'Scalar payload associated with the entry. Null when the metadata entry carries a null value.', 'woocommerce' ),
338 ),
339 ),
340 )
341 );
342 }
343 return self::$entry_type;
344 }
345
346 /**
347 * The `MetadataValue` custom scalar, accepting any GraphQL-compatible scalar.
348 *
349 * The autogenerated scalar template hard-codes acceptance of string
350 * literals only, so this scalar is hand-built rather than going through
351 * ApiBuilder. `parseLiteral` walks the AST node types and `parseValue`
352 * accepts the already-decoded PHP scalar that variables-mode delivers.
353 */
354 private static function get_value_scalar(): CustomScalarType {
355 if ( null === self::$value_scalar ) {
356 self::$value_scalar = new CustomScalarType(
357 array(
358 'name' => 'MetadataValue',
359 'description' => __(
360 'Scalar payload of a metadata entry. Accepts a string, integer, float, boolean, or null.',
361 'woocommerce'
362 ),
363 // Resolvers return the raw PHP scalar; webonyx serialises it as JSON directly.
364 'serialize' => static fn( $value ) => $value,
365 'parseValue' => static function ( $value ) {
366 if ( null === $value || is_bool( $value ) || is_int( $value ) || is_float( $value ) || is_string( $value ) ) {
367 return $value;
368 }
369 throw new Error( 'MetadataValue must be a string, integer, float, boolean, or null.' );
370 },
371 'parseLiteral' => static function ( $value_node, ?array $variables = null ) {
372 unset( $variables );
373
374 if ( $value_node instanceof StringValueNode ) {
375 return $value_node->value;
376 }
377 if ( $value_node instanceof BooleanValueNode ) {
378 return $value_node->value;
379 }
380 if ( $value_node instanceof IntValueNode ) {
381 return (int) $value_node->value;
382 }
383 if ( $value_node instanceof FloatValueNode ) {
384 return (float) $value_node->value;
385 }
386 if ( $value_node instanceof NullValueNode ) {
387 return null;
388 }
389 throw new Error( 'MetadataValue must be a string, integer, float, boolean, or null literal.' );
390 },
391 )
392 );
393 }
394 return self::$value_scalar;
395 }
396 }
397