Getting Right with Image Rights: Workflow and Major Minor Upgrade

After announcing the WP-RUBI Beta (0.91) a bit more than a week ago, I installed and began to work with it at this very blog, and I immediately began to notice that “workflow” improvements I had thought to save for a later day – or even reserve for a “premium” version – had to be considered “basic” to using the plug-in effectively. So, I began another week of fairly intensive work now represented in the Beta numbered 0.93. I could “re-up” or “re-announce,” but I’ll think I’ll save doing so for submission of “1.0” to the WordPress Plug-In Repo.

The enhancements include the following:

  • Set and View Image Removal/Replacement Status from Post Edit and All Posts/Pages (Quick and Bulk Edit) Screens
  • Category and Author Inclusion/Exclusion by Display Name instead of ID #
  • Category Inclusion/Exclusion Includes “Child” Categories
  • Option to Replace Images without Standard Image File Extensions (Mainly Served Images)
  • Admin Convenience Improvements (Expandable Text Areas instead of Text Boxes, Additional Editing Instructions)
  • Expanded Reset Options: Reset Main Settings and Post Settings Separately if Desired

In terms of actual workflow as I worked and flowed it, I think that the first and third above were the most significant (code samples at end):

Set and View Image Removal/Replacement Status from Post Edit and All Posts/Pages (Quick and Bulk Edit) Screens

In 0.91 and every version going back to Replace Old Images, individual post inclusions or exclusions were handled strictly via the main settings page, where included/excluded posts would be listed by ID. I realized that it would be much simpler if I could include or exclude (“clear”) a post from its own edit screen or from All Posts listings. The answer was to use “post meta” instead of global settings, and to add new columns to All Posts (and All Pages). A related convenience feature is the display of discrepancies, if any, between all images found and all images matched. Users may wish to examine “discrepant” posts for images served or sourced elsewhere than in the usual uploads directories, and adjust the “rights clearing” process accordingly.

Category Inclusion/Exclusion Includes “Child” Categories

In 0.91, category-based exclusions/inclusions did not include “child” categories (sub-categories). I quickly realized that at different points, depending on the number and character of posts in a given category or its child categories, it would be easier to be able to work with a “parent” as such instead of having to deal one by one with its children. Upon successful clearing of an entire parent category, it would also be more practical on the settings page to exclude the parent from general removal without having to list each individual child as well.

Workflow Notes

I found myself proceeding as follows:

PHASE I – Initial implementation: Prior To Date

  1. Set current date as “Prior To Date.”
  2. Clear “front page” posts of questionable/dangerous images – move “Prior To Date” back.

At that point, I felt I had a safe and initially presentable site.

PHASE II – Priority Categories

  1. Identify, exclude, and view high priority category.
  2. Open all posts in tabs, page by page: In most cases I could tell by featured image whether they next had to be opened for editing.
  3. Clear priority category – post by post.
  4. Exclude category from image removal and replacement.
  5. View effects on later pages: If appropriate, move “Prior To Date” back again.
  6. Move to new priority category and repeat.

After the 0.93 changes, I could easily move through All Posts, and bulk clear large numbers of posts at a glance. If I had an archive of 10s of 1000s of posts, however, I might prefer to work by author or category. Even then, however, a WordPresser can filter for Author or Category fairly easily via All Posts, so in many instances working through All Posts rather than category by category from Front End archives might still end up being the most efficient way to proceed.

General Observations: On Getting Honest

If fear of a lawsuit or threat of one – as a low probability, high impact danger – is not enough to motivate a blogger to get right with image rights, and the moral question also doesn’t move him or her, there’s something to be said for going gray anyway – as writers mostly have had to do for thousands of years up until the present (oh so fallen) era.

Along my image-curing way, I discovered many non-rights-cleared images that I now think may have detracted from the posts in some ways, even if they initially seemed to make them more attractive or striking.

So, for example, my first “priority” category was “Featured” – posts from my archives that I think of or thought of as best representing my work. If I ever put together a Selected Works by CK MacLeod anthology, I would begin with these. Among them was a series of posts I wrote in 2013 on the political, moral, and international-legal ramifications and possible historical significance of the chemical attack in East Ghouta, Syria, and the reaction of the Obama Administration. I had used several news photos depicting either the physical destruction of Syrian cities or the direct casualties of the attack, but I wonder now whether they oversold my argument, putting me in the position of someone scolding my moral inferiors (everyone) or seeming to blame them (you) for an indifference or callousness that exploitative use of, say, a photograph of assembled children’s corpses, may actually reinforce and hypocritically demonstrate.

In any event, I feel comfortable letting the words stand for themselves. I think the argument and perhaps the descriptions stand forth more strongly without exploitation in the common sensationalistic and therefore (quite observably) de-sensitizing, way. In other posts I found use of images from popular culture mainly adding a comedic effect that, again, in retrospect, seemed to detract from the seriousness of the presentation, even when they were not, as in a few cases, simply clichés or destined to become cliché (like using a “Terminator” image in a discussion of drone warfare).

If and when I construct and publish an anthology or anthologies, leaving the images out ought to simplify the project: I’ll have less to worry about as far as adaptation to “e-pub” or print formats is concerned, and I’ll also not have to worry about getting permissions.

Code Example 1: Include Child Categories

This function detects whether the given Post is in any of the designated categories (from $options) and returns “true” if so. I had some difficulty getting it right (see commented portions).

/* 
 * CATEGORIES EXCLUDED FROM IMAGE REMOVAL AND REPLACEMENT
 * @param object $post
 * @param array $options
 * @return boolean
 */
function cks_rui_exclude_cats( $post, $options) {
    
    $excl_cat_str = $options['exclude_cats'];
    
    $excl_cat_arr = explode( ',', $excl_cat_str );
    
    //since array is going to be used in array_merge(), twice, must be empty, not NULL
    $litters = array();
    
    foreach ( $excl_cat_arr as $excl_cat ) {
        
        $kittens = get_term_children( $excl_cat, 'category' );
            
        $litters = array_merge( $litters, $kittens );
        
    }
    
    //since get_term_children does NOT also return parent category:
    $whole_ward = array_merge( $litters, $excl_cat_arr );
    
    $postid = $post->ID;
    
    if ( in_category( $whole_ward, $postid ) ) {
        
        return true;
        
    } 
    
}

Code Example 2: Adding columns and options to Quick and Bulk Editor

screenshot-6This process was much more complicated: worth a post in itself. Or, rather, it might be if  WordPresser Rachel Carden (@bamadesigner) hadn’t already done most of the groundwork at her blog and at Github.

Getting the output I wanted for the two new columns did involve more “original” work: Nothing terribly fancy, and I’ve no doubt it could be improved, but here it is, with one unsolved mystery noted at the end.

First, the code for the Featured Image column, which, as you can see from the annotated screenshot above, is among other things designed to tell you via fallback image and color-coding both whether the Post or Page has a “Featured Image” (aka “Thumbnail,” but not always displayed at “thumbnail” size), and whether the Post or Page is included in Image Removal and Replacement.

/**
 * Get Featured Image
 * @param int $post_ID
 */
function cks_rui_get_featured_image( $post_ID ) {
    
    $post_thumbnail_id = ( get_post_thumbnail_id( $post_ID) ) ?  get_post_thumbnail_id( $post_ID ) : '';
    
    if ( $post_thumbnail_id ) {
        
        $post_thumbnail_img = wp_get_attachment_image_src( $post_thumbnail_id, 'featured_preview' )  ;
        
        return $post_thumbnail_img[0];   
        
    } else {
        
        return false;
    
    }
    
}

A second helper function counts occurrences of images found in posts, returning one number for all images actually matched, another for all actually found. [29 June: Had to change this file to address relatively rare Regex results producing “catastrophic backtracking” in some cases – also refined search pattern – still working on it, but for latest results check plugin file admin.php, currently ca. lines 682 – 708]

/**
 * UTILITY FUNCTION FOR EDIT.PHP (POSTS AND PAGES) IMAGES COLUMN
 * @param object $post
 * @param array $options
 * @return array of counts
 */
function cks_rui_count_matched_images( $post, $options ) {
    
    $content = $post->post_content;
    
    $static_image_url = $options['image_url'];
    
    $url_match_1 = ( $options['match_1'] ) ? $options['match_1'] . '\S*' : '' ;
        
    $url_match_2 = ( $options['match_2'] ) ? $options['match_2'] . '\S*' : '' ;
        
    $image_match_list = ( $options['image_matches'] ) ? $options['image_matches'] : '' ;
        
    $image_matches_trimmed = str_replace( ' ', '|\.', trim( $image_match_list ) );
        
    $image_matches = ( $image_matches_trimmed ) ? '\.' . $image_matches_trimmed : '' ;
        
    $new_content = preg_replace( '/https?:\S*' . $url_match_1 . $url_match_2 . '[^ ]+?(?:' . $image_matches . ')/', $static_image_url, $content );
  
    //counts number of replacements
    $new_new_content = str_replace( ' src="' . $static_image_url, ' src=#', $new_content, $count);
    
    //counts "src=" tags in posts
    $new_new_new_content = str_replace( ' <img ', '', $new_new_content, $count2); 
        
    return array( $count, $count2 );

}

And here are the both-columns content functions:

/* HEADINGS */
function cks_rui_columns_head( $defaults ) {
   $defaults['featured_image'] = 'Featured Image' ;
   $defaults['images_cleared'] = 'Image Remove/Replace' ;
   return $defaults;
}

/**
 * COLUMNS FOR ALL POSTS
 * Creates two new columns in edit.php for Posts
 * @param string $column_name
 * @param int $post_id
 */
function cks_rui_columns_content( $column_name, $post_id ) {
    
    global $post;
    
    $options = get_option( 'cks_rui_options' );
    $fallback_img = plugin_dir_url( __FILE__ ) . 'no_featured_image.png';
    
    $num_images_arr = cks_rui_count_matched_images( $post, $options );
    
    $global_img_del = ( ! empty($options['global_image_del']) ) ? $options['global_image_del'] : '' ;
    
    //if all images regardless of extension are being removed, set matches = found
    if ( $global_img_del) {
        
        $num_images_arr[0] = $num_images_arr[1] ;
    }
    
    //add classes based on results
    if ($num_images_arr[1] == 0) {
        
        $class = 'no-images';
        
    } else {
        
        if ( $num_images_arr[1] !== $num_images_arr[0] ) {
            
            $class = 'images-found mismatch';
            
        } else {
        
        $class = 'images-found';
        
        }
    }
    
    if (cks_rui_remove_images_from_post( $post, $options ) ) {
        
        $class .= ' cks_rui-images-removed';
        $images_cleared = false;
        
    } else {
        
        $class .= ' cks_rui-images-displayed';
        $images_cleared = true;
    }
    
    //fill first, "Featured Image" column
    if ( $column_name === 'featured_image' ) {
             
        if ( cks_rui_get_featured_image( $post_id ) ) {
            
            $post_featured_image = cks_rui_get_featured_image( $post_id );
            
            echo '<img class="cks_rui-edit-posts-img ' . $class . '" src="' . $post_featured_image . '" />';
            
        } else {
            
            echo '<img class="cks_rui-edit-posts-img ' . $class . '" src="' . $fallback_img . '" />';
        }
        
    }
    
    //fill second, "Image Removal/Replacment Column
    if ( $column_name === 'images_cleared') {
       
        if ( ! get_post_meta( $post->ID, '_is_image_safe', true) ) { 
        
            $image_safe = 'unset' ; 
        
        } else {
        
            $image_safe = get_post_meta( $post->ID, '_is_image_safe', true );
    
        }
        
        $checked_1 = '';
        $checked_2 = '';
        $checked_3 = '';
        
        if ( $image_safe == 'safe' ) { $checked_1 = 'checked' ; }
        if ( $image_safe == 'unsafe' ) { $checked_2 = 'checked' ; } 
        if ( $image_safe == 'unset' ) { $checked_3 = 'checked' ; }
            
        //output hidden, but "stolen" by Quick/Bulk edit jQuery functions    
        echo "<input id='checked_1' style='display: none' type='radio' readonly $checked_1/>";
        echo "<input id='checked_2' style='display: none' type='radio' readonly $checked_2/>";
        echo "<input id='checked_3' style='display: none' type='radio' readonly $checked_3/>";
        echo '<div class="' . $class . '-text">'; 

        if ($num_images_arr[1] == 0 ) { 
            
            echo 'No Images Found in Post';
            
        } 

        if ( $num_images_arr[1] == 1 )  {

            echo '1 Image Found in Post';
            
        } 

        if ( $num_images_arr[1] > 1 ) {

            echo $num_images_arr[1] . ' Images Found in Post';

        }
        
        if ( ! $global_img_del ) {

            if ($num_images_arr[0] == 0 ) { 
                
                echo '<br>No Images Matched in Post';
                
            } 

            if ( $num_images_arr[0] == 1 )  {

                echo '<br>1 Image Matched in Post';
                
            } 

            if ( $num_images_arr[0] > 1 ) {

                echo '<br>' . $num_images_arr[0] . ' Images Matched in Post';

            }
        
        }

        echo ( $images_cleared ) ? ',<br>Cleared for Display' : ', <br>Set for Removal & Replacement'; 
        echo '</div>';

    }
     
}

The above columns need to be enabled by familiar add_filter and add_action functions…

add_filter( 'manage_posts_columns', 'cks_rui_columns_head' );
add_action( 'manage_posts_custom_column', 'cks_rui_columns_content', 10, 2 );

…which need to be copied and run a second time under different names to produce a display in All Pages. That’s the mystery.

IOW, the first time out, I tried

add_filter( 'manage_pages_columns', 'cks_rui_columns_head' );
add_action( 'manage_pages_custom_column', 'cks_rui_columns_content', 10, 2 );

But it didn’t work until I wrote the new virtually identical functions (I ended up changing “Post” to “Page” in the messages) under new names, producing the following:

/**
 * ADD TWO NEW COLUMNS TO POSTS AND PAGES EDIT (edit.php)
 */

/* POSTS */
add_filter( 'manage_posts_columns', 'cks_rui_columns_head' );
add_action( 'manage_posts_custom_column', 'cks_rui_columns_content', 10, 2 );

/* PAGES */
add_filter( 'manage_pages_columns', 'cks_rui_page_columns_head' );
add_action( 'manage_pages_custom_column', 'cks_rui_page_column_content', 10, 2 );

…and all of this is a small part of how I went from a plug-in whose code required all of around 200 lines in one PHP file to more than 2000 lines in a set of PHP, CSS, and jQuery files.

Commenter Ignore Button by CK's Plug-Ins

Leave a Reply

Your email address will not be published. Required fields are marked *

*