Categories
Programming Server Web Wordpress

Creating an ACF filter on the WordPress Admin Page

WordPress Advanced Custom Fields (ACF) is fantastic, allows you a simple way to add data and metadata to each post, but by default it doesn’t display or allow you to query on it.

I wanted a type to allow me to select different types for a post – specifically for internal review status of all posts. Nothing will be displayed on the public post, it will only affect post selection.

I created a group containing a select field with the following options as the choices:

LabelReview Status
Namereview_status
Choicescurrent : Current
update : To update
review : To Review
in_progress : In progress
deprecated : Deprecated
ReturnBoth (array)

Other settings are available, but shouldn’t affect the way this code works.

There are many posts about how to add the choice values in a static array. But if you add a choice to the list, you would have to change code to get it to update. (“Requested changes” anybody?) There is a way to get these values by code, although the field name and slug will have to remain hardcoded:

// This is for including a custom field type - and be able to filter on it
function snoo_add_reviewstatus_filters() {
    global $typenow;    

    // Change these two values to whatever field values you used when creating the select
    $type_slug = "review_status";
    $type_description = "Review status";
    
    // Only add filter to post type you want.
    // If you attach to a custom post type then you need to alter that here
    if ($typenow == 'post'){
        ?>
        <select name='<?php echo $type_slug ?>' class="postform">
        
        <!-- Ideally, get the plural description from the field definition, but for now -->
        <option value=''>Show All <?php echo $type_description ?></option>
        <?php
            // Get all ACF field types
            $acf_types = get_posts(['post_type' => 'acf-field']);
            
            // Look up the specific ACF field
            //  should return an array, probably only of one field.  
            //  That's all we need though
            $type = array_values(
                array_filter($acf_types, function ($t) {
                    return $t->post_title == $type_description;
                })
            );

            // Let's dig into the post containing the field data
            //  the ACF function get_field_object() will give us all the details
            $fielddata = get_field_object(get_post($type[0]->ID)->post_name);

            $current = isset($_GET[$type_slug]) ? $_GET[$type_slug] : '';
            foreach ($fielddata['choices'] as $key => $value) {
                printf('<option value="%s"%s>%s</option>',
                    $key,
                    $key == $current ? ' selected="selected"' : '',
                    $value
                );
            }
        ?>
        </select>
        <?php
    }
}

// Add the filter fields to the Admin page
add_action('restrict_manage_posts', 'snoo_add_reviewstatus_filters');

You should now have the dropdown in the admin area, with your choices available.

The filter won’t quite work yet, though. You need to alter the query. Do that this way:

/** if submitted filter by post meta 
 * 
 * see https://pineco.de/filter-by-acf-meta-data-in-wordpress-admin/
 *
 */ 
function snoo_reviewstatus_posts_filter($query) {

    $type_slug = "review_status";
    
    if (is_admin() && $query->is_main_query())
    {
        $scr = get_current_screen();
        if ($scr->base != 'edit' && $scr->post_type !== 'post') {
            return;
        }

        if (isset($_GET[$type_slug]) && $_GET[$type_slug] != '') { 
            $query->set('meta_query', [
                [
                    'key' => $type_slug,
                    'value' => sanitize_text_field($_GET[$type_slug])
                ]
            ]);
        }
    }
}

// get the passed values and modify the query to use them
add_action('pre_get_posts', 'snoo_reviewstatus_posts_filter');         

Now you should be able to actually filter on the new field!

The only other thing that would be useful is to add a column to view the status on the posts listing.

/*
 * Add content of the custom column
 */
function snoo_custom_reviewstatus_column($column_name, $post_id)
{
    $type_slug = "review_status";

    // This does not really need to be an array, but allows for multiple fields later
    if (!in_array($column_name, array($type_slug))) {
        return;
    }

    $status = get_field($column_name, false);

    if (!empty($status)) {
	    $status_value = $status['value'];
        echo "<a href='edit.php?post_type={$post_type}&post_review_status={$status_value}'> " . esc_html($status['label']) . "</a>";
    } else {
        echo '<i>Not set</i>';
    }
}

add_action('manage_posts_custom_column', 'snoo_custom_reviewstatus_column', 10, 2);

The last thing is to add the column to the custom posts array. If you already have a function adding this, just tack it on. Or, if you insist:

function snoo_custom_columns($columns) {
    $columns['review_status'] = 'Review status';
    return $columns;
}
add_filter('manage_posts_columns', 'snoo_custom_columns');

That should be everything you need to get to using the fields as a usable tag for admin/authoring purposes. I hope it works for you!

Categories
Programming Server Web

REST Authentication

I did some research the other day to secure my REST API using The Slim framework.

I found a tidy little way to force HTTP authentication (basic, in this case) using this article as well as the PHP manual.

I get the client to provide the user name and password, then I can look up the (hashed) password in the database. It simply causes the call to authenticate each login with their corresponding password.

In combination with forcing the page through https (a .htaccess task) and this, I can protect API access pretty simply.

I used this in VB6 (and the great Chilkat components) to authenticate a PUT command. I can now more safely allow database inserts from over the Net. It was as simple as:

Dim oHttp As New ChilkatHttp
Dim resp As String
oHttp.Login = 'userid'
oHttp.Password = 'supersecret'

resp = oHttp.PutText('http://my.apiurl.com',sXMLText, "utf-16","text/xml",0,0)

Next task is to find a more secure way to access the (read-only) API from JavaScript, without just embedding a password in the source code.  This looks like a great start.