Creating my first Habari plugin

In this post I detailed my reason for creating this plugin, and what problem it solves.

In a nutshell, I want to add buttons to the admin interface for a new Entry that will add a particular tag or set of tags and publish the post when clicked.

Where to start?

I figured that the Creating A Plugin page on the Habari wiki would be a good choice. It shows you what files you need to create in order to have your plugin recognized, so I won't repeat that here. I also learned that plugins are based on Actions and Filters. An Action occurs when an event in the software takes place. A Filter is similar, but expects you to change the data passed in and return it. Both Actions and Filters have a "hook name" to represent the event, and you can capture these by creating a "sink" function in your plugin, like so:

function action_plugins_loaded()
    {
        Utils::debug('Hello!');
    }

or

function filter_spam_filter($rating, $comment, $handler_vars)
    {
        if ( strpos( $comment->content, 'viagra' ) ) {
            $rating++;
        }
        return $rating;
    }



But how to find these hooks?

The snag, at least for a beginner, comes in trying to find what hooks you need to create sinks for. I can think of 4 ways to figure this out:

  • Find a plugin that is operating in a similar context. e.g., if you want to add options to a section of the admin interface, find a plugin that adds something to the same location. Download the plugin and you can view the code that makes it work. Learning from other people's code is an invaluable way to gain insight into a new system.
  • Look at the: list of available hooks on the wiki. This can be daunting because if you're like me the filenames don't mean anything yet and there are so many, but some are obvious. Like 'user_insert_before', or 'user_update_before'
  • Poke through the code yourself. This is what I did, mainly. You can learn a lot by searching through the code using grep (or even better, ack). If you can find where the actual data is assigned to objects or written to the database you'll find lots of calls to Plugins::act() sprinkled around.
  • Ask someone on IRC (#habari on irc.freenode.net) or the Habari mailing list. Remember it's good to have done some of your own research in advance.

The first action I found was 'form_publish' for adding the buttons to the form. I poked around in the FormUI documentation as well as the code around where the action is called and found the syntax for adding buttons to an existing part of the form and created my first sink.

My (slightly simplified) action sink:

public function action_form_publish ( $form, $post, $context )
    {
        $buttons = $this->get_configuration();
        foreach($buttons as $btn_name => $tags) {
            $id = $this->_make_id($btn_name);
            $form->append('submit', $id, $btn_name, 'admincontrol_submit');
            $form->$id->move_into($form->buttons);
        }
    }

I was amazed how simple this was, and the results were very good.
button_row.png

I tried using the FormUI method move_after() so I could move the buttons to the right of the existing Publish button, but I kept getting strange errors. More on that later... Next I looked for a way to intercept the post prior to publishing and add the appropriate tags.

My initial thought was that in the Action 'post_publish_before' I could add the tags, and I began looking for a way to save and publish rather than just Save so that I could make my buttons act like the Publish button. However, in the course of trying to find the Publish button I found out that it is actually generated by Javascript! It is dynamically added to the form when the admin page is shown, and an onclick event is added to it that submits the form. I realized that I had been thinking about publishing incorrectly. I was assuming it was stored differently from a normal saved post, but it appears that the post status is just set to indicate that it is published.

Armed with this knowledge, I found what I thought was the correct Action 'post_insert_before' so that I could add my tags to the post and mark it as published before it is saved. However, I got hung up on major detail - I didn't know how to figure out which submit button was pressed. 'post_insert_before' only had a Post object, which had already been given the parameters that the admin handler deemed important. Back to the Action hunt. I paged through the AdminHandler class and found an Action that looked promising: 'publish_post'. It had both a Post object and a FormUI object, and the post gets updated right after the action sink is called. After much trial and error I managed to get the status and tags saving correctly.

Unfortunately it still wasn't working properly. It turns out that the FormUI object passed to the Action is the original form in its entirety, and has no knowledge of what was actually submitted. So even though I could save tags properly, I still didn't know which button had been clicked to submit the form. Back to AdminHandler. I found a page-specific Action that fires when a $_POST array is present in the admin interface, which looked promising. Had I managed to spell my sink function name correctly, I would have been done much sooner...

Eventually, however, I figured out my error and with a few more Utils::debug() calls and a little more browsing the source code for examples I was able to figure out how to get the value of the submitted button and I used that to store the tags that I needed to add to the post. This let me switch my other Action back to 'post_insert_before' as I no longer needed the FormUI parameter, and that Action is closer to where I want the post updated. And it works! I could still mess with formatting the buttons better, and providing a configuration for the plugin rather than having the buttons hard-coded, but I'll save that for Part 2 (and if anyone actually wants this plugin more widely available).

/*
        See what button was clicked and save the tags
    */
    public function action_admin_theme_post_publish ( $admin_handler, 
                                              $theme )
    {
        // Seems like there has to be a better way to do this
        $buttons = $this->get_configuration();
        foreach($admin_handler->handler_vars as $name=>$value) {
            if( isset($buttons[$value]) ) {
                $this->submitted_tags = $buttons[$value];
            }
        }
    }

    /*
        Add the tags to the post
    */
    public function action_post_insert_before ( $post )
    {
        // check if we had any submitted tags
        if(count($this->submitted_tags)) {
            // set post as published
            $statuses = Post::list_post_statuses( false );
            $post->status = $statuses['published'];

            $current_tags = $post->tags;
            $updated_tags = array_merge(array_values($current_tags), 
                                             $this->submitted_tags);

            $post->tags = array_unique($updated_tags);
        }
    }

Update: Habari .7 replaces tags with taxonomies, so I had to change how the tags work. This is for the better, as the above method felt like I was taking advantage of the array internals of the tags object rather than understanding how things work. The following works in the later versions of Habari, although the tag needs to exist first. Replace the tags bit of the above code with this:

foreach($this->submitted_tags as $tag) {
    $tag_obj = Tags::get_one($tag);
    $post->tags->append($tag_obj);
}