Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I'm adapting a script I found online to add custom bulk actions to the screen with the list of posts. It has this line:

add_action('load-edit.php', 'custom_bulk_action');

I'm trying to adapt it for the media library. I see that in place of edit.php I should use upload.php, which leads me to believe I need to find the media analog for load-edit.php. Sounds easy, but I can't even find load-edit.php in my WP install to see if by chance it might be what I'm looking for itself. I have found a few references online to load-*.php (e.g., Custom bulk_action), but nothing that tells me what values * can take.

(I've tried load-upload.php but it's not working--though it could always be something else in my code that's gumming up the works.)

So my questions are two:

  1. What is the media analog of load-edit.php?
  2. Where is load-edit.php (and the other load-*.php files), or what code handles these file requests?

The first is my real question, but the second has gotten under my skin.

Can any of you experts out there give me some guidance? I would very much appreciate it.


By "not working" I meant not that it crashes, but that it wasn't doing as it was supposed (changing a media attribute).

The code I'm adapting can be downloaded at the bottom of the post "Add a WordPress Custom Bulk Action" by Justin Stern of Fox Run Software. Going back to verify each step of the code, I got the adapted version to work, but only if I comment out the conditional and the security check (both asterisked below). What are the media analogs I should use to replace these?

 add_action('load-upload.php', array(&$this, 'custom_bulk_action'));

function custom_bulk_action() {

//  ***if($post_type == 'attachment') {  REPLACE WITH:
    if ( !isset( $_REQUEST['detached'] ) ) {

    // get the action
    $wp_list_table = _get_list_table('WP_Media_List_Table');  
    $action = $wp_list_table->current_action();

    echo "\naction = $action\n</pre>";

    $allowed_actions = array("export");
    if(!in_array($action, $allowed_actions)) return;

    // security check
//  ***check_admin_referer('bulk-posts'); REPLACE WITH:

    // make sure ids are submitted.  depending on the resource type, this may be 'media' or 'ids'
    if(isset($_REQUEST['media'])) {
      $post_ids = array_map('intval', $_REQUEST['media']);

    if(empty($post_ids)) return;

    // this is based on wp-admin/edit.php
    $sendback = remove_query_arg( array('exported', 'untrashed', 'deleted', 'ids'), wp_get_referer() );
    if ( ! $sendback )
      $sendback = admin_url( "upload.php?post_type=$post_type" );

    $pagenum = $wp_list_table->get_pagenum();
    $sendback = add_query_arg( 'paged', $pagenum, $sendback );

    switch($action) {
      case 'export':

        // if we set up user permissions/capabilities, the code might look like:
        //if ( !current_user_can($post_type_object->cap->export_post, $post_id) )
        //  wp_die( __('You are not allowed to export this post.') );

        $exported = 0;
        foreach( $post_ids as $post_id ) {

          if ( !$this->perform_export($post_id) )
            wp_die( __('Error exporting post.') );

        $sendback = add_query_arg( array('exported' => $exported, 'ids' => join(',', $post_ids) ), $sendback );

      default: return;

    $sendback = remove_query_arg( array('action', 'action2', 'tags_input', 'post_author', 'comment_status', 'ping_status', '_status',  'post', 'bulk_edit', 'post_view'), $sendback );


I appreciate your help.


I modified the code above to reflect information from the accepted answer. Many thanks to Ralf912!

share|improve this question
Please, post a full working sample of your code and specify what doesn't work in it. For me, add_action('load-upload.php', 'callback'); works ok. –  brasofilo Mar 22 '13 at 1:30
Thanks for your reply. It is a BIG chunk of code. Not the neatest. I'll see about pulling out what's relevant, but it's going to take some time. –  JohnK Mar 22 '13 at 2:03
You really need to translate this sentence "it's not working" into words and code. Test only this: add_action('load-upload.php', 'my_callback'); function my_callback() { wp_die('hook firing ok'); } –  brasofilo Mar 22 '13 at 2:10
Thanks--great suggestion. I modified the code as suggested and it died when I loaded the media (upload.php) page--this seems appropriate. So we know the hook is working. I'm going to paste my code above and perhaps you can tell me why the global variable isn't coming through correctly. –  JohnK Mar 22 '13 at 19:28
I worked a bit on your current code (the one above) and made some comments. Maybe this help you gist.github.com/RalfAlbert/5231286 –  Ralf912 Mar 24 '13 at 10:12
add comment

2 Answers

up vote 2 down vote accepted

If you want to use your code, try this:

If you want to check if the medias are attachments, you can try to use $_REQUEST['detached']

add_action( 'load-upload.php', 'export_media_test' );

function export_media_test() {

    if ( ! isset( $_REQUEST['action'] ) )

    echo 'Export Media';

    if ( isset( $_REQUEST['detached'] ) ) {
        die( 'No attachments' );
    } else {
        die( 'Attachments' );


You can not check an nonce that wasn't set. The nonce bulk-posts is set in edit.php, and this is the the posts list. In upload.php is the bulk-media nonce set. So use check_admin_referer('bulk-media');

share|improve this answer
Thanks! This looks like the solution. I was wondering, though, what is the advantage of if ( ! isset( $_REQUEST['action'] ) ) return; over the lines I have in the code? Is it just length? I ask because $_REQUEST['action'] (for the top pull-down) and $_REQUEST['action2'] (for the bottom pull-down) are separate. And it seems I've got to get the action from whichever has it anyway. Thanks again! –  JohnK Mar 23 '13 at 22:43
This is just a test code. If you leave out the if(!isset($_REQUEST['action']), then the code will be executed everytime the media page is called. If you want to execute the code only if an action is triggered, then you have to check if the action was triggered. Extend the code to `if(!isset($_REQUEST['action']) || 'whatever_action' !== $_REQUEST['action']) return; –  Ralf912 Mar 24 '13 at 9:08
add comment

WordPress is missing some apply_filters and do_action in upload.php. So you have to do some nasty tricks.

At first, we have to add an export action to the bulk actions. WP_Media_List_Table is an object and this job is very easy. We can simply extend the class and override/extend the needed methods:

require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
require_once( ABSPATH . 'wp-admin/includes/class-wp-media-list-table.php' );

class Extended_Media_List_Table extends WP_Media_List_Table

     * Add the export bulk action
     * @return array
    public function get_bulk_actions() {

        // get the original bulk actions    
        $actions = parent::get_bulk_actions();

        // add our own action(s)
        $actions['export'] = __( 'Export' );

        // return the actions    
        return $actions;

     * Returns the current action
     * @return string
    public function current_action() {

        // check if our action(s) are set and handle them
        if ( isset( $_REQUEST['action'] ) && 'export' === $_REQUEST['action'] ) {
            return 'export_media';

        // let the parent class handle all other actions



The first method simply add the export action. The second method returns export_media if the export action is choosen.

Now it become nasty. There is no apply_filter in upload.php and we can not change the class upload.php use to display the medias. The second point is, there is no add_action to hook into to add another action if a bulk action is selected.

Copy upload.php and rename it (e.g. extended_upload.php). Edit your new file and remove require_once( './admin.php' );. Later we hook into load-upload.php, this hook is called in admin.php. Leaving this line, will end in an endless lopp.

The next step is to insert a handler for your export action. As you can see in the extended class above, ::current_action() returns a string that will be copied into $doaction. In extended_upload.php you will find a switch statement which handles the action. Add a case to handle the export action:

case 'export_media':
    // handle only attachments
    // $wp_list_table->detached = false ==> attachments
    // $wp_list_table->detached = true ==> not attached files
    if ( true == $wp_list_table->detached )

    // $_REQUEST['media'] contains an array with the post_ids
    if ( ! empty( $_REQUEST['media'] ) ) {
        foreach ( $_REQUEST['media'] as $postID ) {
            export_image( $postID );

Remember! All actions will be handled inside an ajax-request. So no output (echo, print, printf, ...) will be displayed!!

The last step to do is, to hook into load-upload.php and say WordPress to use our new extended_upload.php instead of the original upload.php:

add_action( 'load-upload.php', 'add_bulk_action_export_to_list_media' );
function add_bulk_action_export_to_list_media() {
    require_once 'extended_upload.php';

This is a very nasty and tricky solution. Every WordPress update can destroy it and you have to track every changes to upload.php. But this is the only way to retrieve some values (e.g. $wp_list_table->detached which will tell you if it is an attachment or not).

Maybe it is not a bad idea to write a ticket to filter $wp_list_table and and add an action to the switch statement.

$wp_list_table = apply_filters( 'upload_list_table', _get_list_table('WP_Media_List_Table') );

switch ( $doaction ) {

  global $doaction;
  do_action( 'upload_list_table_actions' );

This two changes would make it very easy to add bulk actions and handle them.

share|improve this answer
Wow. Thanks for this extensive reply, Ralf! You're right: it is quite nasty. And unfortunately I won't have the time to update this code with every WP update. But you've really demonstrated virtuosity with WP! Amazing. –  JohnK Mar 23 '13 at 22:36
add comment

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.