Save Expand icon

Ron Valstar
front-end developer

Using WordPress media library in a plugin

I just spent a couple of hours trying to figure this one out. Here’s how to use the WordPress media library in a plugin or custom post type… the right way.

It took me a while to to get this right. So much for Google because everybody with a similar problem posted the same solution, mostly with the same cumbersome code (why bother writing a new post if you’re just gonna copy-paste the code from someone elses blog).

But before I proceed: carefull not to turn this post into a rant I’ll say it now just to get it over with: “Wow, what a bunch of poorly documented, badly written spaghetti code WordPress is”. That’s it.

If you look a bit closer to the admin source you’ll find a JavaScript click-implementation for all ‘a.thickbox’ (media-upload.dev.js ln 65-71).
A bit further you’ll see that the upload uri for that ‘a.thickbox’ can be retrieved with the PHP function ‘get_upload_iframe_src($type)’. Now it doesn’t say anywhere what that $type is supposed to be but digging through the code (media.php) and doing some trial and error it seems a string of the possible values: image, video, audio, file or media (where the latter is any of the previous but file). If you want extra types checkout add_filter with ‘upload_mimes‘.

All we need now is to add a JavaScript callback function.
The JavaScript callback function is a global. This is bloody ridiculous but not surprising if you’ve ever inspected window or $_GLOBAL in WordPress.
The callback function returns a different HTML string depending on the type of file you’ve selected (type image has an image tag inserted).
What all these identical solutions on Google do wrong is that they set this global JavaScript callback function (and add placeholders) the moment you enter the page. It is neater to set it the moment you press the ‘a.thickbox’ button, this way you can adjust your callback when you have multiple uploads (ie an image and a pdf).

Once you’ve added the thickbox and setup the JavaScript callback you should be able to use the WordPress media library.

But what if you do not want that HTML that is returned but, say, a post_id (more experienced WordPress users will know that posts, pages, attachements, images and everything are all stored in the table [wp]_posts). With the post_id (or attachement_id) you’d be able to use more than just an image- or attachement uri, you can also use the title and/or description of a file.

As said, the callback function returns a different types of HTML. The image HTML snippet contains the attachement_id but the others don’t. When you trace back where the callback function is invoked you’ll find an add_filter hook called ‘media_send_to_editor’. It’s nowhere to be found on codex.wordpress.org but it’s easy enough to implement. The call itself is done in media.php ln 488 (apply_filters(‘media_send_to_editor’, $html, $send_id, $attachment)). And if you look through the surrounding function you’ll see the $send_id is exactly what we need. Not only that; the $attachement parameter is an associative array filled with other usefull stuff (title,description etc…).
We overwrite the $html parameter (the filter callback expects $html returned) and set the priority of the filter to something really high so we know for sure it’s the last function applied… and we’re there…
…or are we?
Hooking up to ‘media_send_to_editor’ will also cause whatever you do in that function to happen in the normal text editor (after all: that’s what we’re hacking into).
So we need to try to determine were we come from. There is the $_POST[‘_wp_http_referer’] but if you examine it you’ll see that it does not contain the $GET vars we added to the upload button href. It took me a while to notice: if you check the upload iframe uri you’ll see that somebody did a very sloppy job off chopping of the last $GET variables after ‘TB_iframe’ (WTF WordPress, seriously?!). So don’t append or use add_query_arg but insert your extra variables.

So since we now know where we come from we can use this in our hook function. We don’t even have to parse an HTML string. We just want some data so we’re going to parse JSON.

Here is an example custom post type: foobarbaz.zip (simply add it to your theme dir and include the php file in your functions.php).

There are two important lines in the PHP (the rest is mainly for building a custom post type):

$sUri = esc_url( str_replace('&type=','&target=foobarbaz&input='.$sInputName.'&preview='.$sPreview.'&tab=library&type=',get_upload_iframe_src($sSubType)) );

This is the code for creating the uri used in the upload button. Notice the insertion of the $GET vars.
And there is the entire mediaSendToEditor function on line 81.

function mediaSendToEditor($html, $attachment_id, $attachment) {
    // check for the GET vars added in boxView
    parse_str($_POST['_wp_http_referer'], $aPostVars);
    if (isset($aPostVars['target'])&&$aPostVars['target']=='foobarbaz') {
        // add extra data to the $attachement array prior to returning the json_encoded string
        $attachment['id'] = $attachment_id;
        $attachment['edit'] = get_edit_post_link($attachment_id);
        $attachment['input'] = $aPostVars['input'];
        $attachment['preview'] = $aPostVars['preview'];
        if ($aPostVars['type']=='image') {
            foreach (array('thumb','medium','large') as $size) {
                $aImg = wp_get_attachment_image_src( $attachment_id, $size);
                if ($aImg) $attachment['img_'.$size] = $aImg[0];
            }
        }
        $html = json_encode($attachment);
    }
    return $html;
}

There’s also a tiny caveat conserning the ‘Insert into post’ button in the upload iframe. If you create a custom post type that does not support ‘editor’ you’ll have to add this button yourself.

Here’s the js responsible for the callback:

(function($){
    var fnOldSendToEditor;
    $(function(){
        // store original send_to_editor function
        fnOldSendToEditor = window.send_to_editor;
        // add a different send_to_editor function to each thickbox anchor
        var $Box = $('#foobarbaz-box');
        if ($Box.length) {
            $Box.find('a.thickbox').each(function(i,el){
                var $A = $(el).click(function(){
                    window.send_to_editor = getFnSendToEditor($A.data('type'));
                });
            });
        }
        // hack tb_remove to reset window.send_to_editor
        var fnTmp = window.tb_remove;
        window.tb_remove = function(){
            if (fnOldSendToEditor) window.send_to_editor = fnOldSendToEditor;
            fnTmp();
        };
    });
    function getFnSendToEditor(type){
        return function(fileHTML){
            var oData = JSON.parse(fileHTML);
            //console.log(oData);
            $('#'+oData.input).val(oData.url);
            $('#'+oData.preview).text(type+': '+oData.url.split('/').pop());
            tb_remove();
        }
    }
})(jQuery);

First the original send_to_editor function is stored. Then each a.thickbox is assigned an onclick to replace the send_to_editor. This function creates and returns an anonymous function in order to parse the ‘type’ parameter through it’s scope (not mandatory but a nice alternative to the insertion of the $GET vars).
Then we hack into tb_remove to restore the original send_to_editor function (since we also want this to happen when you simply close the upload iframe).
You’ll also notice that the original global ‘send-to-editor’ function is stored and restored on callback so as to maintain it’s original texteditor functionality. And that’s it.

So there you have it…

(once again: foobarbaz.zip)