Written by John Reed
Of all the powerful features in the Advanced Custom Fields (ACF) plugin’s toolkit, the ability to add custom settings to fields can really take your WordPress site to the next level. Integrated with ACF’s vast array of actions and hooks, it’s possible to use these custom settings to modify data before it is saved into the database or to modify the data before returning it to your templates.
The excellent Encrypt Field Option plugin adds a setting to text fields which encrypts data upon save, which is critical for storing any sensitive user data or system information. The heavy lifting for this is primarily handled on the acf/update_value
and acf/load_value
filters (code sample simplified):
<?php add_action('acf/update_value', function($value, $post_id, $field) { if( isset($field['_is_encrypted']) && $field['_is_encrypted'] ) { $iv = openssl_random_pseudo_bytes(16); $enc_str = openssl_encrypt( $value, 'AES-256-CBC', $ENCRYPTION_KEY, 0, $iv ); // return encrypted value return base64_encode($iv.$enc_str); } return $value; }, 10, 3); add_action('acf/load_value', function($value, $post_id, $field) { if( isset($field['_is_encrypted']) && $field['_is_encrypted'] ) { $str = base64_decode($value); $iv_len = openssl_cipher_iv_length('AES-256-CBC'); $iv = substr($str, 0, $iv_len); // return decrypted value return openssl_decrypt( substr($str, $iv_len), 'AES-256-CBC', $ENCRYPTION_KEY, 0, $iv ); } return $value; }, 10, 3);
Add encrypt option to multiple field types
We wanted to take things a step further to encrypt and secure additional user data (i.e., field types beyond simple text), as well as provide a method for users to upload private files via forms created with the ACF plugin. The Encrypt Field Option plugin calls the acf/render_field_settings
hook for text fields in its initialize()
function:
<?php public function initialize() { add_action('acf/render_field_settings/type=text', [$this, 'render_field_settings'], 10, 3); // … }
We can extend this to support additional field types by iterating over as many types as we’d like…
<?php public function initialize() { foreach( [ 'text', 'textarea', 'number', 'email', 'url', 'password', 'select', 'checkbox', 'radio', 'true_false', 'date_picker', 'date_time_picker', ] as $type ) { add_action( sprintf( 'acf/render_field_settings/type=%s', $type ), [ $this, 'render_field_settings' ], 10, 3 ); } // … }
Private Files
Next, we needed a way to target file uploads to limit access to administrators and the user who uploaded the file. To add the setting, extend the initialize()
function with another render setting hook for image and file fields:
<?php public function initialize() { // … foreach( [ 'image', 'file', ] as $type ) { add_action( sprintf( 'acf/render_field_settings/type=%s', $type ), [ $this, 'render_file_field_settings' ], 10, 3 ); } } public function render_file_field_settings( $field ) { acf_render_field_setting( $field, [ 'label' => __( 'Private File' ), 'instructions' => '', 'name' => '_is_private_file', 'type' => 'true_false', 'ui' => 1, ] ); }
Redirect uploads using acf/upload_prefilter
To silo private files from the default WordPress upload directory, we need to utilize the acf/upload_prefilter
hook on all fields with _is_private_file
setting equal to TRUE
. The most efficient way to do this is to run a custom query on the acf/init
hook:
<?php global $wpdb; // find all 'acf-field' entries with "_is_private_file" = 1 $results = $wpdb->get_results( "SELECT `ID`, `post_excerpt` FROM `{$wpdb->prefix}posts` WHERE `post_type` = 'acf-field' AND `post_content` LIKE '%\"_is_private_file\";i:1%'" );
File names are stored in the post_excerpt
field, so we can map those into an array which we can iterate over to apply the upload filter:
<?php add_action( 'acf/init', function() { global $wpdb; // find all 'acf-field' entries with "_is_private_file" = 1 $results = $wpdb->get_results( "SELECT `ID`, `post_excerpt` FROM `{$wpdb->prefix}posts` WHERE `post_type` = 'acf-field' AND `post_content` LIKE '%\"_is_private_file\";i:1%'" ); // map results to an array of file names (ACF uses `post_excerpt`) $private_files = array_unique( array_map( function($result) { return $result->post_excerpt; }, $results ) ); // iterate over $private_files and filter using _modify_upload_dir() foreach( $private_files as $name ) { add_filter( sprintf('acf/upload_prefilter/name=%s', $name), function($errors, $file, $field) { add_filter( 'upload_dir', '_modify_upload_dir' ); }, 10, 3 ); } } ); function _modify_upload_dir( $param ) { $user = wp_get_current_user(); // individual folders for each user $subdir = sprintf( '/_private/%s', $user->ID ); return array_merge( $param, [ 'path' => sprintf( '%s/uploads%s', WP_CONTENT_DIR, $subdir ), 'url' => sprintf( '%s/uploads%s', WP_CONTENT_URL, $subdir ), 'subdir' => $subdir, ] ); }
Each file in $private_files is passed to acf/upload_prefilter
which in turn fires the WordPress upload_dir
hook where we set the upload directory for the specified file to a subdirectory (in this case, /wp-content/uploads/_private/{user_ID}
)
.htaccess control
Now that we have private files in their own directory, with each user having their own dedicated directory, we can begin the work of restricting access. First, we add a rule to our .htaccess file to redirect all private directory requests to a PHP script:
<IfModule mod_rewrite.c> RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} -s RewriteRule ^wp-content/uploads/(_private/.*)$ path/to/file-access.php?file=$1 [QSA,L] </IfModule>
With all requests to our private directory now routed through file-access.php, we can check permissions and redirect if necessary:
<?php // load WordPress require( sprintf( '%swp-load.php', preg_replace( '/wp-content.*$/', '', __DIR__ ) ) ); // redirect if not logged in is_user_logged_in() || auth_redirect(); // set $basedir list($basedir) = array_values( array_intersect_key( wp_upload_dir(), ['basedir' => 1] ) ) + [ NULL ]; // attempt to read file $file = rtrim( $basedir, '/' ) . '/' . str_replace( '..', '', isset( $_GET[ 'file' ] ) ? $_GET[ 'file' ] : '' ); // parse URL into segments $URL_SEGMENTS = explode( '/', trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' ) ); // not in '_private' directory OR no $basedir OR we aren't dealing with a file = send a 404 if( ! isset( $URL_SEGMENTS[2] ) || ! in_array( $URL_SEGMENTS[2], ['_private'] ) || ! $basedir || ! is_file( $file ) ) { status_header(404); wp_redirect( home_url() ); exit(); } // retrieve user data $user = wp_get_current_user(); // if this user isn't an admin, check their ID if( empty( array_intersect( $user->roles, [ 'administrator', ] ) ) ) { if( isset( $URL_SEGMENTS[3] ) && (int) $URL_SEGMENTS[3] !== $user->ID ) { auth_redirect(); } } // if we've made it this far, the request is from // an admin or authorized user, so serve the file
Customized settings = customized application
The power to modify custom data before and after it is stored in the database allows for next-level application development within WordPress. With custom field settings, a few hooks, and a simple .htaccess rule, the Advanced Custom Fields plugin can help encrypt data and secure user-uploaded files, helping with customer privacy and data security.
Have a similar need on one of your projects? Contact us for help with encryption or other custom theme development on your website.