PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / src / AI / GEO / FieldSettings.php
secure-custom-fields / src / AI / GEO Last commit date
Outputs 2 months ago data 2 months ago FieldSettings.php 2 months ago GEO.php 2 months ago Schema.php 2 months ago SchemaData.php 2 months ago
FieldSettings.php
363 lines
1 <?php
2 /**
3 * ACF 6.8.0 feature port.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 // phpcs:disable -- Upstream ACF 6.8.0 feature-port files are kept close to source.
9
10 namespace SCF\AI\GEO;
11
12 // Exit if accessed directly.
13 use WP_Error;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ACF GEO Field Settings
19 *
20 * Adds JSON-LD field role settings to ACF fields.
21 */
22 class FieldSettings {
23
24 /**
25 * Cache for schema properties by field type
26 *
27 * @var array
28 */
29 private static $properties_cache = array();
30
31 /**
32 * Constructs the FieldSettings class.
33 *
34 * @since 6.8.0
35 *
36 * @return void
37 */
38 public function __construct() {
39 $this->init();
40 }
41
42 /**
43 * Initialize the field settings
44 *
45 * @since 6.8.0
46 *
47 * @return void
48 */
49 public function init() {
50 // Add the Schema.org Property setting to field types that support it.
51 add_action( 'acf/render_field_general_settings', array( $this, 'render_field_schema_settings' ) );
52
53 // AJAX output format handler (needs to be added early for AJAX requests).
54 add_action( 'wp_ajax_acf/schema/get_output_formats', array( $this, 'ajax_get_output_formats' ) );
55 }
56
57 /**
58 * AJAX handler to get output format choices for a field type + property combination.
59 *
60 * @since 6.8.0
61 *
62 * @return void
63 */
64 public function ajax_get_output_formats() {
65 // Verify request.
66 if ( ! acf_verify_ajax() ) {
67 wp_send_json_error(
68 new WP_Error(
69 'acf_invalid_nonce',
70 __( 'Invalid nonce.', 'secure-custom-fields' )
71 )
72 );
73 }
74
75 // Verify user can admin.
76 if ( ! acf_current_user_can_admin() ) {
77 wp_send_json_error(
78 new WP_Error(
79 'acf_invalid_permissions',
80 __( 'Sorry, you do not have permission to do that.', 'secure-custom-fields' )
81 )
82 );
83 }
84
85 $field_type = acf_request_arg( 'field_type', '' );
86 $qualified_property = acf_request_arg( 'property', '' );
87 $property = Schema::get_property_name( $qualified_property );
88
89 if ( empty( $field_type ) || empty( $property ) ) {
90 wp_send_json_error(
91 new WP_Error(
92 'acf_invalid_param',
93 __( 'Missing required parameters', 'secure-custom-fields' )
94 )
95 );
96 }
97
98 $choices = array();
99 $valid_formats = Schema::get_valid_output_formats( $field_type, $property );
100
101 foreach ( $valid_formats as $format ) {
102 $choices[] = array(
103 'id' => $format,
104 'text' => $format,
105 );
106 }
107
108 $default = Schema::get_default_output_format( $field_type, $property );
109
110 wp_send_json_success(
111 array(
112 'choices' => $choices,
113 'default' => $default,
114 )
115 );
116 }
117
118 /**
119 * Render the field-level schema settings.
120 *
121 * @since 6.8.0
122 *
123 * @param array $field The field being edited.
124 * @return void
125 */
126 public function render_field_schema_settings( $field ) {
127 $field_type = $field['type'] ?? '';
128
129 // Check if field type supports JSON-LD output.
130 $supported_ranges = Schema::get_field_type_ranges( $field_type );
131 if ( empty( $supported_ranges ) ) {
132 // Field type doesn't support JSON-LD output (e.g., tab, accordion).
133 return;
134 }
135
136 // Get available Schema.org properties filtered by field type compatibility.
137 $parent_id = (int) ( $field['parent'] ?? 0 );
138
139 acf_render_field_setting(
140 $field,
141 array(
142 'label' => __( 'Schema.org Property', 'secure-custom-fields' ),
143 'instructions' => __( 'Map this field to a Schema.org property instead of using additionalProperty.', 'secure-custom-fields' ),
144 'type' => 'select',
145 'name' => 'schema_property',
146 'class' => 'acf-schema-property',
147 'wrapper' => array(
148 'class' => 'acf-field-meta-box',
149 ),
150 'choices' => $this->get_schema_properties( $field_type, $parent_id ),
151 'allow_null' => 1,
152 'ui' => 1,
153 'experimental' => 1,
154 )
155 );
156
157 $output_choices = $this->get_output_format_choices( $field );
158
159 // Get the default format for this field type + property.
160 $qualified_property = $field['schema_property'] ?? '';
161 $property_name = Schema::get_property_name( $qualified_property );
162 $default_format = Schema::get_default_output_format( $field_type, $property_name );
163
164 acf_render_field_setting(
165 $field,
166 array(
167 'label' => __( 'Schema.org Output Format', 'secure-custom-fields' ),
168 'type' => 'select',
169 'name' => 'schema_output_format',
170 'class' => 'acf-schema-output-format',
171 'wrapper' => array(
172 'class' => 'acf-field-meta-box',
173 ),
174 'choices' => $output_choices,
175 'default_value' => $default_format,
176 'ui' => 1,
177 'experimental' => 1,
178 'conditions' => array(
179 'field' => 'schema_property',
180 'operator' => '!=',
181 'value' => '',
182 ),
183 )
184 );
185 }
186
187 /**
188 * Get available Schema.org properties for a field type
189 *
190 * Returns a hierarchical array of Schema.org properties organized by type,
191 * filtered to only include properties compatible with the field type.
192 *
193 * Uses pre-computed compatibility data for fast lookups.
194 *
195 * @since 6.8.0
196 *
197 * @param string $field_type The ACF field type name.
198 * @param integer $context_id Optional field group ID for context-aware priority ordering.
199 * @return array Array of properties grouped by Schema.org type.
200 */
201 public function get_schema_properties( string $field_type = '', int $context_id = 0 ): array {
202 // Build cache key including context.
203 $cache_key = $field_type . '_' . $context_id;
204
205 // Return cached result if available.
206 if ( isset( self::$properties_cache[ $cache_key ] ) ) {
207 return self::$properties_cache[ $cache_key ];
208 }
209
210 $roles = array();
211
212 // Get compatible properties using pre-computed data.
213 $compatible_set = $this->get_compatible_properties_set( $field_type );
214
215 if ( empty( $compatible_set ) ) {
216 self::$properties_cache[ $cache_key ] = $roles;
217 return $roles;
218 }
219
220 // Get all properties grouped by type from schema.org vocabulary.
221 $properties_by_type = Schema::get_properties_by_type();
222
223 // Get priority types with context-aware ordering.
224 $priority_types = Schema::get_priority_types( $context_id );
225
226 // Add priority types first.
227 foreach ( $priority_types as $type ) {
228 if ( isset( $properties_by_type[ $type ] ) ) {
229 $type_compatible = array();
230 foreach ( $properties_by_type[ $type ] as $property ) {
231 if ( isset( $compatible_set[ $property ] ) ) {
232 $type_compatible[ $type . '.' . $property ] = $property;
233 }
234 }
235
236 if ( ! empty( $type_compatible ) ) {
237 $type_label = sprintf( '%s Properties', $type );
238 $roles[ $type_label ] = $type_compatible;
239 }
240 }
241 }
242
243 // Add remaining types alphabetically.
244 foreach ( $properties_by_type as $type => $properties ) {
245 // Skip priority types (already processed).
246 if ( in_array( $type, $priority_types, true ) ) {
247 continue;
248 }
249
250 // Skip types with no properties.
251 if ( empty( $properties ) ) {
252 continue;
253 }
254
255 $type_compatible = array();
256 foreach ( $properties as $property ) {
257 if ( isset( $compatible_set[ $property ] ) ) {
258 $type_compatible[ $type . '.' . $property ] = $property;
259 }
260 }
261
262 if ( ! empty( $type_compatible ) ) {
263 $type_label = sprintf( '%s Properties', $type );
264 $roles[ $type_label ] = $type_compatible;
265 }
266 }
267
268 /**
269 * Filter the available Schema.org properties.
270 *
271 * Allows developers to add custom Schema.org properties or modify existing ones.
272 *
273 * @param array $properties The Schema.org role mappings grouped by type.
274 * @param string $field_type The ACF field type being configured.
275 */
276 $roles = apply_filters( 'acf/schema/schema_properties', $roles, $field_type );
277
278 // Cache the result.
279 self::$properties_cache[ $cache_key ] = $roles;
280
281 return $roles;
282 }
283
284 /**
285 * Get compatible properties as a set (for fast lookup)
286 *
287 * Uses pre-computed data from SchemaData::get_compatible_properties().
288 *
289 * @since 6.8.0
290 *
291 * @param string $field_type The ACF field type name.
292 * @return array Properties as keys for O(1) lookup.
293 */
294 private function get_compatible_properties_set( $field_type ) {
295 // Get field type's output types.
296 $field_ranges = Schema::get_field_type_ranges( $field_type );
297
298 if ( empty( $field_ranges ) ) {
299 return array();
300 }
301
302 // Get pre-computed compatible properties mapping.
303 $compatible_by_type = SchemaData::get_compatible_properties();
304
305 // Merge compatible properties for all output types.
306 $compatible = array();
307 foreach ( $field_ranges as $output_type ) {
308 if ( isset( $compatible_by_type[ $output_type ] ) ) {
309 foreach ( $compatible_by_type[ $output_type ] as $property ) {
310 $compatible[ $property ] = true;
311 }
312 }
313 }
314
315 return $compatible;
316 }
317
318 /**
319 * Get output format choices for a field
320 *
321 * Returns the valid output formats for the field's type and selected property.
322 * For example, an Image field mapped to the 'image' property can output
323 * either 'URL' or 'ImageObject'.
324 *
325 * @since 6.8.0
326 *
327 * @param array $field The field being edited.
328 * @return array Array of format => label pairs.
329 */
330 private function get_output_format_choices( $field ) {
331 $field_type = $field['type'] ?? '';
332 $qualified_property = $field['schema_property'] ?? '';
333
334 // If no property selected yet, return empty choices.
335 if ( empty( $qualified_property ) ) {
336 return array();
337 }
338
339 // Extract just the property name from qualified property (e.g., "Recipe.recipeYield" -> "recipeYield").
340 $property = Schema::get_property_name( $qualified_property );
341 $valid_formats = Schema::get_valid_output_formats( $field_type, $property );
342
343 if ( empty( $valid_formats ) ) {
344 return array();
345 }
346
347 // Build choices array with format as both key and label.
348 $choices = array();
349 foreach ( $valid_formats as $format ) {
350 $choices[ $format ] = $format;
351 }
352
353 /**
354 * Filter the available output format choices.
355 *
356 * @param array $choices The output format choices.
357 * @param string $field_type The ACF field type.
358 * @param string $property The selected Schema.org property.
359 */
360 return apply_filters( 'acf/schema/output_format_choices', $choices, $field_type, $property );
361 }
362 }
363