Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/code-reference/META.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This file tracks code elements that need documentation.

- `acf/bidirectional/supported_field_types_for_post`
- `acf/bidirectional/supported_target_field_types`
- `scf/bidirectional/allow_target`

## acf-field-functions.php

Expand Down Expand Up @@ -350,6 +351,7 @@ This file tracks code elements that need documentation.
- `acf/validate_post_id`
- `acf/validate_setting`
- `acf/verify_ajax`
- `scf/upload/generate_previews`

## api/api-template.php

Expand Down
10 changes: 10 additions & 0 deletions docs/code-reference/acf-bidirectional-functions-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ Process updating bidirectional fields.
* @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.
* @return void

## `_scf_is_allowed_bidirectional_target()`

Check whether a submitted bidirectional target is a valid choice for the origin field.

* @since SCF 6.9.7
@internal
* @param integer|string $target ACF encoded target ID (e.g. 123, "user_1" or "term_5").
* @param array $field The origin field.
* @return boolean

## `_scf_prepare_bidirectional_update()`

Prepare a side-effect-free bidirectional update plan.
Expand Down
9 changes: 9 additions & 0 deletions docs/code-reference/api/api-helpers-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ acf_get_valid_post_id
* @param $post_id (mixed)
* @return $post_id (mixed)

## `scf_post_password_required()`

Checks whether field values stored on an ACF post ID belong to a password-protected
post whose password has not been entered yet.

* @since SCF 6.9.7
* @param integer|string $post_id ACF encoded post ID.
* @return boolean

## `acf_get_post_id_info()`

acf_get_post_id_info
Expand Down
10 changes: 10 additions & 0 deletions docs/code-reference/rest-api/class-acf-rest-api-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ and `sanitize_callback` for nested schema properties, see:
* @param string $param
* @return boolean|WP_Error

### `is_password_protected_content_hidden`

Checks whether a post's field values must stay out of the response because the
post is password protected and the caller has neither unlocked it nor can edit it.

* @since SCF 6.9.7
* @param integer $post_id The post ID.
* @param WP_REST_Request $request The current request.
* @return boolean

### `load_fields`

Load field values into the requested object. This method is not a part of any public API and is only public as
Expand Down
72 changes: 70 additions & 2 deletions includes/Blocks/Bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ public function get_value( array $source_attrs, \WP_Block $block_instance, strin
if ( ! isset( $source_attrs['key'] ) || ! is_string( $source_attrs['key'] ) ) {
$value = '';
} else {
$field = get_field_object( $source_attrs['key'], false, true, true, true );
$post_id = acf_get_valid_post_id( false );
if ( scf_post_password_required( $post_id ) ) {
return '';
}

$field = get_field_object( $source_attrs['key'], $post_id, true, true, true );

if ( ! $field ) {
return '';
Expand All @@ -79,7 +84,13 @@ public function get_value( array $source_attrs, \WP_Block $block_instance, strin
}
}

$field_value = $field['value'];
$raw_value = acf_get_value( $post_id, $field );
$field_value = scf_rest_sanitize_user_data( $field['value'], $raw_value, $field );

add_filter( 'acf/rest/expose_reference_target', array( $this, 'expose_reference_target' ), PHP_INT_MAX, 2 );
$field_value = acf_rest_sanitize_reference_data( $field_value, $raw_value, $field );
remove_filter( 'acf/rest/expose_reference_target', array( $this, 'expose_reference_target' ), PHP_INT_MAX );
$field_value = $this->strip_post_passwords( $field_value );

switch ( $attribute_name ) {
case 'id':
Expand Down Expand Up @@ -122,4 +133,61 @@ public function get_value( array $source_attrs, \WP_Block $block_instance, strin

return apply_filters( 'acf/blocks/binding_value', $value, $source_attrs, $block_instance, $attribute_name );
}

/**
* Removes the password from expanded post objects before they are output.
*
* @since SCF 6.9.7
*
* @param mixed $value The field value, possibly containing WP_Post objects.
* @return mixed
*/
private function strip_post_passwords( $value ) {
if ( $value instanceof \WP_Post ) {
$value = clone $value;
$value->post_password = '';
return $value;
}

if ( is_array( $value ) ) {
foreach ( $value as $key => $item ) {
$value[ $key ] = $this->strip_post_passwords( $item );
}
}

return $value;
}

/**
* Decides whether a referenced post is expanded in a block binding value.
*
* Bindings render on the front end, so a referenced post is exposed when the
* viewer can read it there rather than when the REST API would return it.
* Attachments are always expanded because their files are publicly served.
* This runs last so that callbacks written for REST output do not decide
* what a rendered block shows.
*
* @since SCF 6.9.7
*
* @param boolean $exposable Whether the REST rules would expose the target.
* @param integer $target_id The referenced post or attachment ID.
* @return boolean
*/
public function expose_reference_target( $exposable, $target_id ) {
$post = get_post( $target_id );

if ( ! $post instanceof \WP_Post ) {
return false;
}

if ( 'attachment' === $post->post_type ) {
return true;
}

if ( post_password_required( $post ) && ! current_user_can( 'edit_post', $post->ID ) ) {
return false;
}

return is_post_publicly_viewable( $post ) || current_user_can( 'read_post', $post->ID );
}
}
89 changes: 89 additions & 0 deletions includes/acf-bidirectional-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,26 @@ function acf_update_bidirectional_values( $target_item_ids, $post_id, $field, $t
return;
}

$decoded_origin = acf_decode_post_id( $post_id );
if ( 'post' === $decoded_origin['type'] && (int) $decoded_origin['id'] > 0 && wp_is_post_revision( (int) $decoded_origin['id'] ) ) {
return;
}

$update_plan = _scf_prepare_bidirectional_update( $target_item_ids, $post_id, $field, $target_prefix );
$valid_targets = $update_plan['target_fields'];
$item_id = $update_plan['item_id'];
$additions = $update_plan['additions'];
$subtractions = $update_plan['subtractions'];

if ( acf_get_data( 'scf_saving_submitted_values' ) ) {
$additions = array_filter(
$additions,
static function ( $addition ) use ( $field ) {
return _scf_is_allowed_bidirectional_target( $addition, $field );
}
);
}

if ( ! empty( $valid_targets ) ) {
acf_set_data( 'acf_doing_bidirectional_update', true );

Expand All @@ -60,6 +74,81 @@ function acf_update_bidirectional_values( $target_item_ids, $post_id, $field, $t
}
}

/**
* Check whether a submitted bidirectional target is a valid choice for the origin field.
*
* @since SCF 6.9.7
* @internal
*
* @param integer|string $target ACF encoded target ID (e.g. 123, "user_1" or "term_5").
* @param array $field The origin field.
* @return boolean
*/
function _scf_is_allowed_bidirectional_target( $target, $field ) {
$decoded = acf_decode_post_id( $target );
$id = (int) $decoded['id'];
$allowed = true;

switch ( $field['type'] ) {
case 'user':
$user = 'user' === $decoded['type'] && $id ? get_userdata( $id ) : false;
$roles = acf_get_array( $field['role'] ?? array() );
if ( ! $user ) {
$allowed = false;
} elseif ( empty( $roles ) ) {
// The field only lists users of the current site.
$allowed = ! is_multisite() || is_user_member_of_blog( $user->ID );
} else {
$allowed = (bool) array_intersect( $roles, (array) $user->roles );
}
break;

case 'relationship':
case 'post_object':
$post = 'post' === $decoded['type'] && $id ? get_post( $id ) : null;
if ( ! $post ) {
$allowed = false;
break;
}
$post_types = empty( $field['post_type'] ) ? acf_get_post_types() : acf_get_array( $field['post_type'] );
$post_statuses = acf_get_array( $field['post_status'] ?? array() );
$allowed = in_array( $post->post_type, $post_types, true )
&& ( empty( $post_statuses ) || in_array( 'any', $post_statuses, true ) || in_array( $post->post_status, $post_statuses, true ) )
&& ( is_post_publicly_viewable( $post ) || current_user_can( 'read_post', $post->ID ) );
$terms = $allowed && ! empty( $field['taxonomy'] ) ? acf_decode_taxonomy_terms( acf_get_array( $field['taxonomy'] ) ) : array();
if ( $terms ) {
// Match each field's choice query: Relationship ORs the taxonomies, Post Object ANDs them.
$matches = array();
foreach ( $terms as $taxonomy => $slugs ) {
$matches[] = has_term( $slugs, $taxonomy, $post );
}
$allowed = 'relationship' === $field['type'] ? in_array( true, $matches, true ) : ! in_array( false, $matches, true );
}
break;

case 'taxonomy':
$term = 'term' === $decoded['type'] && $id ? get_term( $id ) : null;
$allowed = $term instanceof WP_Term && $term->taxonomy === $field['taxonomy'];
break;
}

// Users may always link to objects they can edit themselves, such as choices widened by a query filter.
if ( ! $allowed && in_array( $decoded['type'], array( 'post', 'user', 'term' ), true ) && $id ) {
$allowed = acf_current_user_can_edit_in_context( $decoded );
}

/**
* Filters whether a submitted value may receive a bidirectional back-reference.
*
* @since SCF 6.9.7
*
* @param boolean $allowed Whether the target is a valid choice for the field.
* @param integer|string $target ACF encoded target ID.
* @param array $field The origin field.
*/
return (bool) apply_filters( 'scf/bidirectional/allow_target', (bool) $allowed, $target, $field );
}

/**
* Prepare a side-effect-free bidirectional update plan.
*
Expand Down
18 changes: 16 additions & 2 deletions includes/acf-form-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ function scf_restrict_uploaded_field_keys( array $allowed_keys ): void {
function acf_save_post( $post_id = 0, $values = null ) {

// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
// Values passed by code while no user is logged in, such as WP-CLI or cron, were not submitted by anyone.
$programmatic = null !== $values && ! is_user_logged_in();

// Override $_POST data with $values.
if ( $values !== null ) {
$_POST['acf'] = $values;
Expand All @@ -172,7 +175,12 @@ function acf_save_post( $post_id = 0, $values = null ) {
// phpcs:enable WordPress.Security.NonceVerification.Missing

// Do generic action.
do_action( 'acf/save_post', $post_id );
acf_set_form_data( 'scf_programmatic_save', $programmatic );
try {
do_action( 'acf/save_post', $post_id );
} finally {
acf_set_form_data( 'scf_programmatic_save', false );
}

// Return true.
return true;
Expand All @@ -194,7 +202,13 @@ function _acf_do_save_post( $post_id = 0 ) {

// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
if ( ! empty( $_POST['acf'] ) ) {
acf_update_values( $_POST['acf'], $post_id ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by WP when saved.
$was_saving_submitted = acf_get_data( 'scf_saving_submitted_values' );
acf_set_data( 'scf_saving_submitted_values', ! acf_get_form_data( 'scf_programmatic_save' ) );
try {
acf_update_values( $_POST['acf'], $post_id ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Sanitized by WP when saved.
} finally {
acf_set_data( 'scf_saving_submitted_values', $was_saving_submitted );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
Expand Down
43 changes: 43 additions & 0 deletions includes/api/api-helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2312,6 +2312,21 @@ function acf_get_valid_post_id( $post_id = 0 ) {
return $post_id;
}

/**
* Checks whether field values stored on an ACF post ID belong to a password-protected
* post whose password has not been entered yet.
*
* @since SCF 6.9.7
*
* @param integer|string $post_id ACF encoded post ID.
* @return boolean
*/
function scf_post_password_required( $post_id ) {
$decoded = acf_decode_post_id( $post_id );

return 'post' === $decoded['type'] && (int) $decoded['id'] > 0 && post_password_required( (int) $decoded['id'] );
}



/**
Expand Down Expand Up @@ -2543,9 +2558,37 @@ function acf_upload_file( $uploaded_file ) {
// Save the data
$id = wp_insert_attachment( $object, $file );

/**
* Filters whether a file uploaded through a basic File field gets preview images.
*
* @since SCF 6.9.7
*
* @param boolean $generate_previews Whether to generate preview images. Default true only for users who can upload files.
* @param string $file Path to the uploaded file.
* @param integer $id Attachment ID.
*/
$skip_previews = ! apply_filters( 'scf/upload/generate_previews', current_user_can( 'upload_files' ), $file, $id );
$no_cover_art = static function ( $metadata ) {
if ( is_array( $metadata ) ) {
unset( $metadata['image'] );
}
return $metadata;
};
if ( $skip_previews ) {
add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', PHP_INT_MAX );
add_filter( 'wp_read_audio_metadata', $no_cover_art, PHP_INT_MAX );
add_filter( 'wp_read_video_metadata', $no_cover_art, PHP_INT_MAX );
}

// Add the meta-data
wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );

if ( $skip_previews ) {
remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', PHP_INT_MAX );
remove_filter( 'wp_read_audio_metadata', $no_cover_art, PHP_INT_MAX );
remove_filter( 'wp_read_video_metadata', $no_cover_art, PHP_INT_MAX );
}

/** This action is documented in wp-admin/custom-header.php */
do_action( 'wp_create_file_in_uploads', $file, $id ); // For replication

Expand Down
Loading
Loading