PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.9.5
Secure Custom Fields v6.9.5
6.9.5 6.9.4 6.9.3 6.9.2 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 / includes / acf-bidirectional-functions.php
secure-custom-fields / includes Last commit date
Blocks 1 month ago Datastore 3 days ago Meta 1 year ago abilities 1 month ago admin 1 week ago ajax 2 months ago api 3 days ago fields 3 days ago forms 1 week ago legacy 1 year ago locations 2 weeks ago post-types 3 months ago rest-api 3 days ago walkers 1 year ago acf-bidirectional-functions.php 3 days ago acf-field-functions.php 3 months ago acf-field-group-functions.php 8 months ago acf-form-functions.php 1 year ago acf-helper-functions.php 1 year ago acf-hook-functions.php 1 year ago acf-input-functions.php 8 months ago acf-internal-post-type-functions.php 8 months ago acf-meta-functions.php 1 month ago acf-post-functions.php 1 year ago acf-post-type-functions.php 1 year ago acf-taxonomy-functions.php 1 year ago acf-user-functions.php 1 month ago acf-utility-functions.php 1 year ago acf-value-functions.php 1 year ago acf-wp-functions.php 1 month ago assets.php 1 month ago blocks-auto-inline-editing.php 2 weeks ago blocks.php 1 week ago class-acf-data.php 11 months ago class-acf-internal-post-type.php 1 month ago class-acf-options-page.php 1 year ago class-acf-site-health.php 5 months ago class-scf-json-schema-validator.php 7 months ago class-scf-schema-builder.php 3 months ago compatibility.php 1 year ago datastore.php 2 months ago deprecated.php 1 year ago fields.php 11 months ago index.php 1 year ago l10n.php 1 year ago local-fields.php 1 year ago local-json.php 1 week ago local-meta.php 1 year ago locations.php 1 year ago loop.php 11 months ago media.php 1 year ago rest-api.php 11 months ago revisions.php 2 months ago scf-ui-options-page-functions.php 1 year ago third-party.php 8 months ago upgrades.php 1 month ago validation.php 11 months ago wpml.php 1 year ago
acf-bidirectional-functions.php
435 lines
1 <?php
2
3 /**
4 * General functions relating to the bidirectional feature of some fields.
5 *
6 * @package wordpress/secure-custom-fields
7 */
8
9 /**
10 * Process updating bidirectional fields.
11 *
12 * @since ACF 6.2
13 *
14 * @param array $target_item_ids The post, user or term IDs which should be updated with the origin item ID.
15 * @param integer|string $post_id The ACF encoded origin post, user or term ID.
16 * @param array $field The field being updated on the origin post, user or term ID.
17 * @param string|false $target_prefix The ACF prefix for a post, user or term ID required for the update_field call for this field type.
18 *
19 * @return void
20 */
21 function acf_update_bidirectional_values( $target_item_ids, $post_id, $field, $target_prefix = false ) {
22
23 // Bail early if we're already updating a bidirectional relationship to prevent recursion.
24 if ( acf_get_data( 'acf_doing_bidirectional_update' ) ) {
25 return;
26 }
27
28 // Support disabling bidirectionality globally.
29 if ( ! acf_get_setting( 'enable_bidirection' ) ) {
30 return;
31 }
32
33 if ( empty( $field['bidirectional'] ) || empty( $field['bidirectional_target'] ) ) {
34 return;
35 }
36
37 $update_plan = _scf_prepare_bidirectional_update( $target_item_ids, $post_id, $field, $target_prefix );
38 $valid_targets = $update_plan['target_fields'];
39 $item_id = $update_plan['item_id'];
40 $additions = $update_plan['additions'];
41 $subtractions = $update_plan['subtractions'];
42
43 if ( ! empty( $valid_targets ) ) {
44 acf_set_data( 'acf_doing_bidirectional_update', true );
45
46 // Loop over each target, processing additions and removals.
47 foreach ( $valid_targets as $target_field ) {
48 foreach ( $additions as $addition ) {
49 $current_value = acf_get_array( get_field( $target_field, $addition, false ) );
50 update_field( $target_field, array_unique( array_merge( $current_value, array( $item_id ) ) ), $addition );
51 }
52
53 foreach ( $subtractions as $subtraction ) {
54 $current_value = acf_get_array( get_field( $target_field, $subtraction, false ) );
55 update_field( $target_field, array_unique( array_diff( $current_value, array( $item_id ) ) ), $subtraction );
56 }
57 }
58
59 acf_set_data( 'acf_doing_bidirectional_update', false );
60 }
61 }
62
63 /**
64 * Prepare a side-effect-free bidirectional update plan.
65 *
66 * @since SCF 6.9.5
67 * @internal
68 *
69 * @param array $target_item_ids Target post, user, or term IDs.
70 * @param integer|string $post_id Encoded origin object ID.
71 * @param array $field Field being updated on the origin object.
72 * @param string|false $target_prefix Target object prefix, or false for posts.
73 * @param array|null $current_values Optional current raw field values.
74 * @return array
75 */
76 function _scf_prepare_bidirectional_update( $target_item_ids, $post_id, $field, $target_prefix = false, $current_values = null ) {
77 $decoded = acf_decode_post_id( $post_id );
78 $valid_types = acf_get_valid_bidirectional_target_types( $decoded['type'] );
79 $target_fields = array();
80 foreach ( $field['bidirectional_target'] as $target_field ) {
81 $target_field_object = get_field_object( $target_field );
82 if ( is_array( $target_field_object ) && in_array( $target_field_object['type'], $valid_types, true ) ) {
83 $target_fields[] = $target_field;
84 }
85 }
86 if ( empty( $target_fields ) ) {
87 $current_values = array();
88 } elseif ( null === $target_prefix && empty( $decoded['id'] ) ) {
89 // Creating the origin object: the saver applies the field default as the current
90 // value once the object exists, so mirror that instead of treating current as empty.
91 $current_values = acf_get_array( $field['default_value'] ?? array() );
92 } elseif ( null === $current_values ) {
93 $current_values = get_field( $field['key'], $post_id, false );
94 }
95 $target_item_ids = empty( $target_fields ) ? array() : $target_item_ids;
96 $additions = array_diff( array_filter( acf_get_array( $target_item_ids ) ), array_filter( acf_get_array( $current_values ) ) );
97 $subtractions = array_diff( array_filter( acf_get_array( $current_values ) ), array_filter( acf_get_array( $target_item_ids ) ) );
98 // A null prefix means the REST preflight is inferring the destination context, which is
99 // only reliable for core types. A third-party field type owns its own target context;
100 // guessing it (e.g. checking the ID as post/user/term) would raise false 403s on ID
101 // collisions and still miss custom contexts, so such fields are deliberately left out of
102 // the preflight. They keep working via their explicit prefix on the actual write path.
103 $infer = null === $target_prefix;
104 $known_type = in_array( $field['type'], array( 'relationship', 'post_object', 'user', 'taxonomy' ), true );
105 $target_prefix = $infer ? ( 'user' === $field['type'] ? 'user' : ( 'taxonomy' === $field['type'] ? 'term' : false ) ) : $target_prefix;
106 if ( $target_prefix ) {
107 $prefix = fn( $value ) => $target_prefix . '_' . $value;
108 $additions = array_map( $prefix, $additions );
109 $subtractions = array_map( $prefix, $subtractions );
110 }
111 if ( $infer && ! $known_type ) {
112 $destinations = array();
113 } else {
114 $destination_exists = static function ( $destination ) {
115 $destination = acf_decode_post_id( $destination );
116 return ! in_array( $destination['type'], array( 'post', 'user', 'term', 'comment' ), true ) || '' !== get_object_subtype( $destination['type'], $destination['id'] );
117 };
118 $permission_subtractions = array_filter( $subtractions, $destination_exists );
119 $destinations = array_values( array_unique( array_merge( $additions, $permission_subtractions ) ) );
120 }
121 $item_id = $decoded['id'];
122 return compact( 'target_fields', 'item_id', 'additions', 'subtractions', 'destinations' );
123 }
124
125 /**
126 * Collect the bidirectional destinations that a field update will write.
127 *
128 * @since SCF 6.9.5
129 * @internal
130 *
131 * @param array $field Resolved root or nested field.
132 * @param mixed $value Incoming value for the field.
133 * @param integer|string $post_id Encoded origin object ID.
134 * @param array|null $current Optional current values, or null to load them.
135 * @param boolean $resave_paginated Whether paginated repeater rows will be re-saved.
136 * @return array
137 */
138 function _scf_collect_bidirectional_destinations( $field, $value, $post_id, $current = null, $resave_paginated = false ) {
139 if ( ! empty( $field['bidirectional'] ) && ! empty( $field['bidirectional_target'] ) ) {
140 return _scf_prepare_bidirectional_update( $value, $post_id, $field, null, $current )['destinations'];
141 }
142 if ( ! is_array( $value ) ) {
143 return array();
144 }
145 if ( $resave_paginated && 'repeater' === $field['type'] && ! empty( $field['pagination'] ) ) {
146 // A paginated save re-saves every surviving stored row plus submitted changes.
147 // Client-controlled aliases can collapse onto the same row index and resurrect a
148 // deleted-then-edited row, so collect the conservative union of everything it may write.
149 $rows = null === $current ? acf_get_value( $post_id, $field ) : $current;
150 $rows = is_array( $rows ) ? $rows : array();
151 unset( $value['acfcloneindex'] );
152 $submitted = array();
153 $reordered_rows = array();
154 foreach ( $value as $key => $row ) {
155 if ( is_array( $row ) && false !== strpos( (string) $key, 'row' ) && ! isset( $row['acf_deleted'] ) && isset( $row['acf_reordered'] ) ) {
156 $reordered_rows[ (int) str_replace( 'row-', '', (string) $key ) ] = true;
157 }
158 }
159 foreach ( $value as $key => $row ) {
160 if ( ! is_array( $row ) ) {
161 continue;
162 }
163 if ( false !== strpos( (string) $key, 'row' ) && isset( $row['acf_deleted'] ) ) {
164 // The saver drops the stored row this deletion targets and does not re-fire it.
165 $row_number = (int) str_replace( 'row-', '', (string) $key );
166 unset( $rows[ $row_number ] );
167 if ( 'row-' . $row_number === (string) $key && isset( $reordered_rows[ $row_number ] ) ) {
168 $submitted[] = $row;
169 }
170 continue;
171 }
172 $submitted[] = $row;
173 }
174 $value = array_merge( array_values( $rows ), $submitted );
175 }
176 $extract_sub_value = static function ( $row, $sub_field, $container_type ) {
177 // Group and Clone use key/_name with isset(); Repeater and Flexible Content use key/name and preserve explicit nulls.
178 $uses_isset = in_array( $container_type, array( 'group', 'clone' ), true );
179 $selectors = $uses_isset
180 ? array( $sub_field['key'] ?? null, $sub_field['_name'] ?? null )
181 : array( $sub_field['key'] ?? null, $sub_field['name'] ?? null );
182 foreach ( $selectors as $selector ) {
183 if ( null !== $selector && ( $uses_isset ? isset( $row[ $selector ] ) : array_key_exists( $selector, $row ) ) ) {
184 return array( true, $row[ $selector ] );
185 }
186 }
187 return array( false, null );
188 };
189 $destinations = array();
190 if ( 'flexible_content' === $field['type'] && ! empty( $field['layouts'] ) ) {
191 foreach ( $value as $row ) {
192 if ( ! is_array( $row ) || ! isset( $row['acf_fc_layout'] ) ) {
193 continue;
194 }
195 foreach ( $field['layouts'] as $layout ) {
196 if ( empty( $layout['sub_fields'] ) || $layout['name'] !== $row['acf_fc_layout'] ) {
197 continue;
198 }
199 foreach ( $layout['sub_fields'] as $sub_field ) {
200 list( $exists, $sub_value ) = $extract_sub_value( $row, $sub_field, $field['type'] );
201 if ( $exists ) {
202 $destinations = array_merge( $destinations, _scf_collect_bidirectional_destinations( $sub_field, $sub_value, $post_id, $current, $resave_paginated ) );
203 }
204 }
205 break;
206 }
207 }
208 return $destinations;
209 }
210 // Only traverse the core container shapes mirrored here; third-party containers may save different value structures.
211 if ( ! in_array( $field['type'], array( 'group', 'clone', 'repeater' ), true ) || empty( $field['sub_fields'] ) ) {
212 return $destinations;
213 }
214 $rows = ( 'repeater' === $field['type'] ) ? $value : array( $value );
215 foreach ( $rows as $row ) {
216 if ( ! is_array( $row ) ) {
217 continue;
218 }
219 foreach ( $field['sub_fields'] as $sub_field ) {
220 list( $exists, $sub_value ) = $extract_sub_value( $row, $sub_field, $field['type'] );
221 if ( $exists ) {
222 if ( 'clone' === $field['type'] && ! empty( $sub_field['_clone'] ) && isset( $sub_field['__key'] ) ) {
223 $sub_field['key'] = $sub_field['__key'];
224 }
225 if ( 'clone' === $field['type'] && isset( $field['_name'] ) && $field['name'] !== $field['_name'] ) {
226 $name_length = strlen( $field['_name'] );
227 $name_prefix = substr( $field['name'], 0, -$name_length );
228 if ( $name_prefix . $field['_name'] === $field['name'] ) {
229 $sub_field['name'] = $name_prefix . $sub_field['name'];
230 }
231 }
232 // A Group stores its children under a prefixed meta name; mirror that so a
233 // nested paginated repeater reads its existing rows from the right key.
234 if ( 'group' === $field['type'] ) {
235 $sub_field['name'] = $field['name'] . '_' . ( $sub_field['_name'] ?? $sub_field['name'] );
236 }
237 $destinations = array_merge( $destinations, _scf_collect_bidirectional_destinations( $sub_field, $sub_value, $post_id, $current, $resave_paginated ) );
238 }
239 }
240 }
241 return $destinations;
242 }
243
244 /**
245 * Allows third party fields to enable support as a target field type for a particular object type
246 *
247 * @since ACF 6.2
248 *
249 * @param string $object_type The object type that will be updated on the target field, such as 'term', 'user' or 'post'.
250 *
251 * @return array An array of valid field type names (slugs) for the target of the bidirectional field.
252 */
253 function acf_get_valid_bidirectional_target_types( $object_type ) {
254 $valid_target_types = array();
255 switch ( $object_type ) {
256 case 'term':
257 $valid_target_types = array( 'taxonomy' );
258 break;
259 case 'user':
260 $valid_target_types = array( 'user' );
261 break;
262 case 'post':
263 $valid_target_types = array( 'relationship', 'post_object' );
264 break;
265 }
266 return apply_filters( 'acf/bidirectional/supported_field_types_for_post', $valid_target_types, $object_type );
267 }
268
269 /**
270 * Build the complete choices argument for rendering the select2 field for bidirectional target based on the currently selected choices
271 *
272 * @since ACF 6.2
273 *
274 * @param array $choices The currently selected choices (as an array of field keys).
275 *
276 * @return array
277 */
278 function acf_build_bidirectional_target_current_choices( $choices ) {
279 if ( empty( $choices ) ) {
280 return array();
281 }
282
283 $results = array();
284 foreach ( $choices as $choice ) {
285 if ( empty( $choice ) || ! is_string( $choice ) ) {
286 continue;
287 }
288
289 $field_object = get_field_object( $choice );
290 if ( is_array( $field_object ) && ! empty( $field_object['label'] ) ) {
291 $results[ $choice ] = $field_object['label'];
292 } else {
293 $results[ $choice ] = $choice;
294 }
295 }
296
297 return $results;
298 }
299
300 /**
301 * Build valid fields for a bidirectional relationship for select2 display
302 *
303 * @since ACF 6.2
304 *
305 * @param array $results The original results array.
306 * @param array $options The options provided to the select2 AJAX search.
307 *
308 * @return array
309 */
310 function acf_build_bidirectional_relationship_field_target_args( $results, $options ) {
311 $valid_field_types = apply_filters( 'acf/bidirectional/supported_target_field_types', array( 'relationship', 'post_object', 'user', 'taxonomy' ) );
312
313 $field_groups = array_filter(
314 acf_get_field_groups(),
315 function ( $field_group ) {
316 return $field_group['active'];
317 }
318 );
319
320 $valid_fields = array();
321 foreach ( $field_groups as $field_group ) {
322 $fields = acf_get_fields( $field_group );
323 foreach ( $fields as $field ) {
324 if ( in_array( $field['type'], $valid_field_types, true ) ) {
325 if ( empty( $valid_fields[ $field_group['title'] ] ) ) {
326 $valid_fields[ $field_group['title'] ] = array();
327 }
328 $valid_fields[ $field_group['title'] ][ $field['key'] ] = array(
329 'type' => $field['type'],
330 'label' => $field['label'],
331 );
332
333 if ( isset( $options['parent_key'] ) && $options['parent_key'] === $field['key'] ) {
334 $valid_fields[ $field_group['title'] ][ $field['key'] ]['this_field'] = true;
335 }
336 }
337 }
338 }
339
340 foreach ( $valid_fields as $field_group_name => $fields ) {
341 $field_group = array(
342 'text' => $field_group_name,
343 'children' => array(),
344 );
345 foreach ( $fields as $key => $data ) {
346 $field_group['children'][] = array(
347 'id' => $key,
348 'text' => $data['label'],
349 'field_type' => $data['type'],
350 /* translators: %s A field type name, such as "Relationship" */
351 'human_field_type' => sprintf( __( '%s Field', 'secure-custom-fields' ), acf_get_field_type_prop( $data['type'], 'label' ) ),
352 'this_field' => ! empty( $data['this_field'] ),
353 );
354 }
355 $results['results'][] = $field_group;
356 }
357
358 return $results;
359 }
360
361 add_filter( 'acf/fields/select/query/key=_acf_bidirectional_target', 'acf_build_bidirectional_relationship_field_target_args', 10, 2 );
362
363 /**
364 * Renders the field settings required for bidirectional fields
365 *
366 * @since ACF 6.2
367 *
368 * @param array $field The field object passed into field setting functions.
369 *
370 * @return void
371 */
372 function acf_render_bidirectional_field_settings( $field ) {
373 if ( ! acf_get_setting( 'enable_bidirection' ) ) {
374 return;
375 }
376
377 acf_render_field_setting(
378 $field,
379 array(
380 'label' => __( 'Bidirectional', 'secure-custom-fields' ),
381 'instructions' => __( 'Update a field on the selected values, referencing back to this ID', 'secure-custom-fields' ),
382 'type' => 'true_false',
383 'name' => 'bidirectional',
384 'ui' => 1,
385 )
386 );
387
388 acf_render_field_setting(
389 $field,
390 array(
391 'name' => 'bidirectional_notes',
392 'type' => 'message',
393 'message' => acf_get_bidirectional_field_settings_instruction_text(),
394 'conditions' => array(
395 'field' => 'bidirectional',
396 'operator' => '==',
397 'value' => 1,
398 ),
399 )
400 );
401
402 acf_render_field_setting(
403 $field,
404 array(
405 'type' => 'select',
406 'name' => 'bidirectional_target',
407 'label' => __( 'Target Field', 'secure-custom-fields' ),
408 'instructions' => __( 'Select field(s) to store the reference back to the item being updated. You may select this field. Target fields must be compatible with where this field is being displayed. For example, if this field is displayed on a Taxonomy, your target field should be of type Taxonomy', 'secure-custom-fields' ),
409 'class' => 'bidrectional_target',
410 'choices' => acf_build_bidirectional_target_current_choices( $field['bidirectional_target'] ),
411 'conditions' => array(
412 'field' => 'bidirectional',
413 'operator' => '==',
414 'value' => 1,
415 ),
416 'ui' => 1,
417 'multiple' => 1,
418 'ajax' => 1,
419 )
420 );
421 }
422
423 /**
424 * Returns the translated instructional text for the message field for the bidirectional field settings.
425 *
426 * @since ACF 6.2
427 *
428 * @return string The html containing the instructional message.
429 */
430 function acf_get_bidirectional_field_settings_instruction_text() {
431 /* translators: %s the URL to ACF's bidirectional relationship documentation */
432 $message = '<p class="acf-feature-notice with-warning-icon">' . __( 'Enabling the bidirectional setting allows you to update a value in the target fields for each value selected for this field, adding or removing the Post ID, Taxonomy ID or User ID of the item being updated.', 'secure-custom-fields' ) . '</p>';
433 return $message;
434 }
435