Encryption Photo by Alina Grubnyak on Unsplash
John Reed

Written by John Reed

Share

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.

Tags
Advanced Custom FieldsEncryptionFile AccessPHPWordPress