Understanding AJAX requests

Have you ever written a post, started to type in a tag, and saw this sort of interface reaction:

As you start to type in a tag, you’ll see an auto-type show up. How does this work? One way is to pre-load the list within the DOM (either within the HTML or the JavaScript) and reference that list, but a cooler way, and a much more extensible way, is to use AJAX calls. Thankfully, WordPress supports them without much trouble!

Recently I needed to add functionality to take a given URL to an Amazon product, and retrieve the ASIN (Amazon unique ID) to store as information related to the post. Todays post shows one method to do this, using the AJAX callback.

You can download the complete code here.

The example is relatively simple, and my preferred method is to put things inside a PHP class, so let’s have a look at the first part:

Hook actions:

$TL_AmazonURL = new TL_AmazonURL;
class TL_AmazonURL
{
	var $meta_key = 'amazonkey';
	function __construct()
	{
		add_action( 'add_meta_boxes', array( $this, 'MetaBoxSetup' ) );
		add_action( 'save_post', array( $this, 'SavePostMeta' ) );
		add_action( 'admin_head', array( $this, 'AdminHead' ) );
		add_action('wp_ajax_tl_amazonurl_ajax', array( $this, 'AjaxCallback' ) );
	}

If you aren’t familiar with PHP classes within WordPress, just think of them this way: hook all the filters and actions inside the __construct() function, then you need to reference the functions for those hooks with an array referencing the class, $this, and the function FunctionName.

For this example, we’ll store the ASIN as a post meta field, and put the interface in a metabox on the post view (more later), so we configure a metabox with add_action( 'add_meta_boxes', array( $this, 'MetaBoxSetup' ) );

When we add the ASIN, we’ll put it in the post meta, therefore we want to verify the ASIN when the post is saved, so we register a function to run whenever a post is saved with add_action( 'save_post', array( $this, 'SavePostMeta' ) );

The AJAX requests require some custom JavaScript, so we’ll need to enqueue some script (and CSS!), but only in the admin page loads, so we use add_action( 'admin_head', array( $this, 'AdminHead' ) );

Finally, AJAX requests are handled with specially registered AJAX callbacks that require the action wp_ajax_ followed by your custom AJAX callback name, which needs to be distinct. In this example, our callback name is tl_amazonurl_ajax so we use add_action('wp_ajax_tl_amazonurl_ajax', array( $this, 'AjaxCallback' ) );

Add Meta box:

Registering the metabox is as simple as calling add_meta_box:

	function MetaBoxSetup()
	{
		add_meta_box(
			'tl_amazonurl',
			__( 'Amazon ASIN', 'tl_amazonurl_metaboxtitle' ),
			array( $this, 'MetaBox' ),
			'post',
			'side',
			'high'
		);
	}

Pick a unique label, it’s part of the HTML for the meta box: tl_amazonurl

The title of the meta box, but thinking internationally we’ll wrap it in the handy translational function __(); which we won’t take advantage of here, but it lets me point out lesser-known Best Practice: __( 'Amazon ASIN', 'tl_amazonurl_metaboxtitle' )

The function name, and metabox position are the remaining elements of the array. The final metabox, with no content, looks something like this:

Meta box contents:

There are two parts to the meta box contents, the HTML, and the CSS/JavaScript which we’ll include in the admin-head function. The HTML looks like:

function MetaBox()
	{
		global $post;
		$asin = get_post_meta( $post->ID, $this->meta_key );
		$asin = ( $asin ? $asin[0] : "" );
		$nonce = wp_create_nonce( 'tl_amazonurl_nonce' );
		?>
        <div id="tl_amazonurl_div">
            <p>Paste in the URL from Amazon:</p>
            <input type="hidden" name="tl_amazonurl_checked" value="" />
            <input type="hidden" name="tl_amazonurl_nonce" value="<?php echo $nonce; ?>" />
            <input type="text" id="tl_amazonurl_link" name="tl_amazonurl_link" value="<?php echo $asin; ?>" /><br />
            <span id="tl_amazonurl_return"></span>
            <input type="button" id="tl_amazonurl_check" name="tl_amazonurl_check" value="Check URL" />
        </div>
        <?php
    }

The first few lines are the most important. We first look for the post meta by the defined key, using $this->meta_key, and for security we generate a nonce (read up on them!) with the name tl_amazonurl_nonce

The other important thing to notice is that there isn’t any actual HTML form here. Instead, we’ll call some JavaScript onClick to watch the tl_amazonurl_check button, and grab what’s inside the input with the ID tl_amazonurl_link

The CSS and JavaScript is inside the AdminHead function, like this:

	function AdminHead()
	{
		?>

		input#tl_amazonurl_link {
			width: 100%;
		}
		div#tl_amazonurl_div {
			text-align: right;
		}
		div#tl_amazonurl_div p {
			text-align: left;
		}
		input#tl_amazonurl_check {
			margin-top: 8px;
		}
		span#tl_amazonurl_return {
			float: left;
			height: 32px;
			margin-top: 6px;
			margin-left: 4px;
			padding-top: 6px;
			padding-left: 32px;
		}
		span#tl_amazonurl_return.success {
			background: url('') top left no-repeat;
		}
		span#tl_amazonurl_return.failure {
			background: url('') top left no-repeat;
		}

		jQuery(document).ready( function($) {
			$('#tl_amazonurl_check').click( function () {
				$('#tl_amazonurl_return').text('Checking...');
				$("#tl_amazonurl_return").removeClass("failure");
				$("#tl_amazonurl_return").removeClass("success");
				var data = {
					action: 'tl_amazonurl_ajax',
					amazonurl: $("#tl_amazonurl_link").val()
				};
				$.post(ajaxurl, data, function(response) {
					if ( response == 'fail' ) {
						$('#tl_amazonurl_link').val('');
						$('#tl_amazonurl_return').text('Invalid URL!');
						$("#tl_amazonurl_return").removeClass("success").addClass("failure");
					} else {
						$('#tl_amazonurl_link').val(response);
						$('#tl_amazonurl_return').text('Success!');
						$("#tl_amazonurl_return").removeClass("failure").addClass("success");
					}
				});
			});
		});

		<?php
	}

The CSS is, I hope, mostly straightforward. If the AJAX call returns success we will use a green check-mark to give the user visual feedback, and a red x-mark if it fails. The rest of the CSS is small styling bits.

The real magic is in the JavaScript. Any WordPress page in the wp-admin area already queues up jQuery, so we can use that to do what we want, which is:

When you click the button to verify a URL in the text input box, we let the user know the computer is thinking by adding text to the span $('#tl_amazonurl_return').text('Checking...'); and we remove the CSS class that defines the green or red visual response using the jQuery removeClass.

The stuff we pass into the AJAX call is JSON data, which contains the action name, tl_amazonurl_ajax (Note! This is what decides which AJAX callback function to run, from the add_action('wp_ajax_tl_amazonurl_ajax'... very early on!) and the data, which is what was in the input box.

If the response is fail we’ll add the red x-mark and tell the user “Invalid URL!”. Otherwise, we’ll print the ASIN and give the green check mark to indicate success. (Check and x marks are done via CSS.)

The AJAX callback function:

There’s not much here:

	function AjaxCallback()
	{
		$key = $this->ValidateASIN( $_POST['amazonurl'] );
		if ( $key ) echo $key;
		else echo 'fail';
		die();
	}

We pass the Amazon URL into another function (that way we can use the same validation inside the save_post action) which will either give us the ASIN or fail, then we either echo out the ASIN or a failure message.

Note! You must use die(); at the end of the callback function! At this point, the headers have been sent, the text you made (the ASIN or “fail”) are output, and you need to end the rest of any WordPress code from running!

So, given an Amazon URL that looks like this, you’d get back the ASIN, which would look like this: B004HZYA6E

Saving the ASIN to the post

When we save the post meta field, it’ll look something like this:

	function SavePostMeta()
	{
		if ( current_user_can( 'edit_posts' ) && isset( $_POST['tl_amazonurl_nonce'] ) && wp_verify_nonce( $_POST['tl_amazonurl_nonce'], 'tl_amazonurl_nonce' ) )
		{
			global $post;
			// if the amazon url field is set, we'll check it
			if ( isset( $_POST['tl_amazonurl_link'] ) && $_POST['tl_amazonurl_link'] != '' )
			{
				// we'll double check the link, based on it's size
				if ( strlen( $_POST['tl_amazonurl_link'] ) >= 22 )
				{
					$key = $this->ValidateASIN( $_POST['tl_amazonurl_link'] );
					if ( $key )
					{
						update_post_meta( $post->ID, $this->meta_key, $key );
					}
					else $error = "The ASIN failed to validate with Amazon, please try again.";
				}
				else
				{
					$url = "http://www.amazon.com/dp/" . $_POST['tl_amazonurl_link'];
					$key = $this->ValidateASIN( $url );
					if ( $key )
					{
						update_post_meta( $post->ID, $this->meta_key, $key );
					}
					else $error = "The link failed to validate with Amazon. Please try copying and pasting again.";
				}
			}
			// if no asin was included, we'll delete the post meta (delete returns false if meta key not found, in this case it's okay)
			else
			{
				delete_post_meta( $post->ID, $this->meta_key );
			}
		}
		// if security checks fail, generate an error message
		else
		{
			$error = "You don't have permission to edit this field.";
		}

	}

The first line is a security check, using the WordPress nonce feature. If there is text inside the ASIN input box, we really should re-check it, in case the user modified it but didn’t click the button.

The Amazon API specifies that the ASIN will be 22 or less characters, so we’ll skip any potentially complicated logic and assume that strings shorter than 23 characters are in the final ASIN form already, and if they are longer we’ll assume they are proper URLs.

If there’s no text, we’ll clean things up by deleting the post meta field entirely. (As opposed to just removing the text, we want to remove the text and key entirely.)

(Note also that the error variables aren’t actually used anywhere, I just put them there to make it more clear to you what the error was.)

Validating the Amazon URL/ASIN

When I was trying to solve this problem, I read many solutions that used regex to try disassembling the URL, either by counting characters between /s, or some other method. All of these will fail, due to Amazon changing their URL scheme, so I wanted a better solution. This one is not as fast as a regex, but is stronger.

First, after a little exploration, I found that Amazon pages all contain (in the HTML) the following line: <link rel=”canonical” href=”http://www.amazon.com/{product name}/dp/{ASIN} />

So, what we want to do is load the Amazon page given in the link, then extract the {ASIN} field:

	function ValidateASIN( $url )
	{

		// to easily retrieve the HTML element, we'll use the parser here: http://simplehtmldom.sourceforge.net/
		include( 'simple_html_dom.php' );

		// for some reason some servers won't work without an explicit "http://"
		$link = strpos( $url, "http://" );
		if ( $link === false ) $link = "http://".$url;
		else $link = $url;

		// you can go ahead and grab the HTML from the given URL
		$html = @file_get_html( $link );

		// if there was a failure to get the HTML, return false immediately
		if ( !$html ) return false;

		// now you need to grab this element from the HTML:
		//
		$element = $html->find('link[rel=canonical]');

		// if $html->find returns an array you have to go in and get it
		if ( $element )
		{
			// grab the url
			$element = $element[0];
			$element = $element->attr['href'];

			// split the url to get the code
			$pieces = explode( "/", $element );
			$key = $pieces[ count( $pieces ) - 1 ];

			// for the above example, the function returns this string: 0596102356
			return $key;
		}
		// the parser didn't find the element
		else
		{
			return false;
		}

	}

The way I am going to parse the HTML is using the very excellent Simple HTML DOM class. Note that you don’t need to use this class, but if you write your own HTML parser I will probably shun you forever.

Anyway, the rest is implementation of grabbing the ASIN from the HTML document. The only notable part is that it returns the ASIN key by itself, as text, which is important for the AjaxCallback function.

The end!

That’s really all there is to it! Here’s what the meta box should look like without anything in it:

Here’s what it looks like when I put in a URL and click the button, but before the server can respond (note the user feedback, so they can tell that it’s actually doing something):

Here’s what it looks like when the server responds with a success:

And here’s what a failure looks like:

Final thoughts:

The above code is incomplete, there are ways certain technically-correct URLs could fail, and there are better ways to give the user feedback, so be sure to think that stuff through before using this publicly.

However, the main takeaway is this: When you want an AJAX call (which can do anything!) you need to add an action for your custom named callback, so it looks of the form add_action('wp_ajax_CALLNAME' where your CALLNAME is a unique and custom name, referenced also in the AJAX call, which uses JSON data of the form { action: 'CALLNAME', KEY: VALUE } where the KEY and VALUE are any number of key/value paired, JSON-valid information.

Let me know in the comments if you have questions!

Plugin template

In anticipation of the official WordPress release of the Sermon Posts plugin (not until after school, which is early May), I decided to start a blog post series aimed at helping the junior WordPress plugin developer.

Most open source type projects have some sort of bug tracker system, but what I thought could be done is essentially walk through the project source code, examining the major chunks to see how they are built, and why they were built the way they were. I hope that by doing this I will both help the newcomers to WordPress, and also make my own plugin code much more transparent, which should be good for review/security.

Download this example’s entire source code here.

The first thing I should explain (the only thing this post discusses) is my preferred folder hierarchy for plugins. The folder structure is given below, but I’ll explain it in more detail in later posts:

Core Folders:

plugin_name/
plugin_name/include/
plugin_name/include/media
plugin_name/include/other
plugin_name/include/plugin

Core Files:

plugin_name/plugin_name.php
plugin_name/include/plugin/admin-core.php

The general idea is that the plugin should load as little as possible on each access, so for the typical client request all the code will be inside plugin_name/plugin_name.php, and for all administrative tasks the code will be located in plugin_name/include/plugin/admin-core.php. While this doesn’t actually make the PHP script run faster, it does separate the two code sources, and makes code maintenance easier later on.

Inside the core code, plugin_name/plugin_name.php, the following is written:

<?php
/*
Plugin Name: PluginTemplate
Plugin URI: http://tobiaslabs.com
Description: AsimpleTemplateForMakingPlugins
Author: Tobias Davis
Version: 0.1
Author URI: http://davistobias.com
*/

// Initialization loads as little as possible.
if ( is_admin() )
{
    require_once( 'include/plugin/admin-core.php' );
    $TL_PlugTemp = new TL_PluginTemplate_Admin;
}
else
{
    $TL_PlugTemp = new TL_PluginTemplate_Core;
}

// The core visitor-side functionality is held held here
class TL_PluginTemplate_Core
{

    // plugin information, for the admin side
    var $plugin_info = array();

    /**
     * @since 0.1
     * @author Tobias Davis
    */
    function __construct()
    {
        // set the plugin information, for activation and other options
        $this->plugin_info['plugin_location'] = __FILE__;

        // Initialization
        add_action( 'init', array( $this, 'Init' ) );
    }

    /**
     * @since 0.1
     * @author Tobias Davis
    */
    function Init()
    {
    }

}
?>

Let’s explain this line by line:

/*
Plugin Name: PluginTemplate
Plugin URI: http://tobiaslabs.com
Description: AsimpleTemplateForMakingPlugins
Author: Tobias Davis
Version: 0.1
Author URI: http://davistobias.com
*/

This is the minimal reasonable information to describe a plugin. If you don’t know this already, you should probably read this excellent Codex entry. Obviously, change your plugin name, URI, etcetera. I’ve been starting any sandbox plugin at version 0.1, to indicate that it is definitely not ready for prime-time.

Next you’ve got a bit of if->then logic, to load the admin code only if the viewer is on an admin page:

if ( is_admin() )
{
	require_once( 'include/plugin/admin-core.php' );
	$TL_PluginTemplate = new TL_PluginTemplate_Admin;
}
else
{
	$TL_PluginTemplate = new TL_PluginTemplate_Core;
}

You should change the name of PluginTemplate to reflect your plugin name. In my most recent plugin template file, I have consistent naming, so I can do a simple search+replace for TL_PluginTemplate to rename it to whatever I want. Other than that, it should be pretty straightforward: The function is_admin() tests whether the loaded page is one of the wp-admin pages. If it is, it loads the admin class (more on that later), otherwise it loads the smallest amount of code possible.

Next we have the core plugin class:

class TL_PluginTemplate_Core
{
	// plugin information, for the admin side
	var $plugin_info = array();

The variable $plugin_info is used to hold information about the plugin, mostly just the __FILE__ seen inside the __construct() function, which is the file name of the core plugin code. I’ve found that without this information, it becomes difficult to include other code within the admin code section described later in this post.

	/**
	 * @since 0.1
	 * @author Tobias Davis
	*/

One thing I picked up from fellow WordPress engineer, James Lafferty, and from reading through the core WordPress code, is the idea of noting when a function was made, along with other useful bits. This example is pretty bare, but you’ll see in later posts that properly constructed comments make documentation basically automatic. This reduces work load later, by making sure your code is correctly documented right away.

	function __construct()
	{
		// set the plugin information, for activation and other options
		$this->plugin_info['plugin_location'] = __FILE__;

If you aren’t familiar with constructors and destructors, check out the Wikipedia page on constructors, and the PHP specific page on them. Essentially, when you call $var = new ClassName; the __constructor() function is called first, so we can put critical items in the function, like:

		// Initialization
		add_action( 'init', array( $this, 'Init' ) );
	}

WordPress uses so-called “action“s to get script functions to trigger at the right time. The Codex is helpful, but Adams list of hooks is also very helpful. One thing that I eventually started doing, as I got more comfortable, was looking at the actual WordPress code. WordPress uses the same functions in the core that you will use in your plugin!

Since the plugin code is inside a PHP class, we need to pass the function name in as an array, array( $this, 'Init' ). The variable $this is essentially a PHP reserved variable which refers to the current class. You could probably also use array( $TL_PlugTemp, 'Init' ), or whatever you name your variable (it needs to be unique!), but typically you’ll be working within the plugin class, so you might as well use $this.

Finally, of course, we have the function called by add_action, and the end of the core class:

	/**
	 * @since 0.1
	 * @author Tobias Davis
	*/
	function Init()
	{
	}
}

Inside the Init() function you would put things like register_post_type and register_taxonomy, which I’ll describe in later posts.

Let’s move to the other core file, plugin_name/include/plugin/admin-core.php which looks like this:

<?php
// security
if ( !function_exists( 'is_admin' ) && !is_admin() ) die();

class TL_PluginTemplate_Admin extends TL_PluginTemplate_Core
{

	function __construct()
	{
		// additional security
		if ( get_parent_class($this) != 'TL_PluginTemplate_Core' ) die();
		// functions named the same need to call the parent class function
		parent::__construct();

		// activation/deactivation
		register_activation_hook( $this->plugin_info['plugin_location'], array( $this, 'Activation' ) );
		register_deactivation_hook( $this->plugin_info['plugin_location'], array( $this, 'Deactivation' ) );

		// admin actions
		add_action( 'admin_init', array( $this, 'AdminInit' ) );

	}

	function AdminInit()
	{
	}

	function Activation()
	{
	}

	function Deactivation()
	{
	}

}
?>

And let’s take this a chunk at a time, starting with the single line of security if ( !function_exists( 'is_admin' ) && !is_admin() ) die();. This first line makes sure none of the admin code can ever load from an errant PHP call, such as direct calls like http://www.site.com/wp-content/plugins/my_plugin/include/admin-core.php that might execute code if written poorly. Note that if I catch you not using other security measures, I’ll tan yer hide.

Anyway, moving on to the next line class TL_PluginTemplate_Admin extends TL_PluginTemplate_Core, note that this is similar to a normal PHP class declaration, but it uses “extends”. This is because we load the core class in the core file as well, and this one adds to its functionality. This is especially evident in the following:

	function __construct()
	{
		// additional security
		if ( get_parent_class($this) != 'TL_BibBlue_Core' ) die();
		// functions named the same need to call the parent class function
		parent::__construct();

		// activation/deactivation
		register_activation_hook( $this->plugin_info['plugin_location'], array( $this, 'Activation' ) );
		register_deactivation_hook( $this->plugin_info['plugin_location'], array( $this, 'Deactivation' ) );

		// admin actions
		add_action( 'admin_init', array( $this, 'AdminInit' ) );

	}

Note that the __construct() function appears in both the core and the admin class, but the admin class has the line parent::__construct(); which essentially runs the core __construct() function before running the admin __construct() function.

Additionally, the register_activation_hook function uses the $plugin_info variable instead of it’s own file name, which is the typical method. This is a technical issue, the plugin activation records the core file location, but the admin functionality is in a deeper file, so we reconcile them in this way. The same can be said for the register_deactivation_hook function. If this isn’t very clear, just use it for now, and hopefully later posts will clarify why this is necessary.

Finally, there is another add_action, which references a function only available for admin-side views, such as add_menu_page and add_options_page.

	function AdminInit()
	{
	}

	function Activation()
	{
	}

	function Deactivation()
	{
	}

}

That’s it for now!

Hopefully this sets the stage well for later posts, which I’ll use to describe things like registering custom posts, storing peculiar meta-data, and even adding database tables! All my plugins follow this same folder hierarchy, and later posts will add to it.

Next I’ll describe how to register a custom post.

All is not lost!

Wow, these last few days have been incredibly frustrating!

I’m wrapping up my final semester at the University, taking 21 credit hours, so I’m swamped. I came home late Thursday night, and went on one of my websites to look for a reference I had linked to. Suddenly my browser had 3 pop-up windows, all of them were grossly pornographic. Well, I don’t typically even include ads on my sites (not enough people to be worth it, even if I wanted to), and I definitely don’t use porn ads!

To shorten the already long story, my server had been compromised, and every PHP file was bad, so the most efficient way I could repair it was basically reinstalling everything. Ugh.

So, I hope I installed this site back the way it was, I hope especially that the RSS feed carried over correctly. Leave me a comment if you followed through an RSS feed to get here, it would give me comfort to know it still works.

In other news: obviously the Sermon Posts plugin has been on hold for some time, and that won’t change for a while, although (like I said) I did not abandon the project. This is my last semester at the University, Lord willing, and then I will have more time on my hands.

Thanks to the few of you who follow my progress :)

Status Update, Finally!

Hey all, I just watned to let you know that I am finally back at they keyboard, and I’ve already started cranking out the next big update. I anticipate one more update, and then a release through the WordPress site.

I started using Trello to track bugs and whatnot as a Project Tracker, I’ve found it to increase productivity in my code work here and all my other projects as well. Check it out at the top under “Sermon Posts > Project Tracker”.

Stay tuned, I’m currently updating the import process, you’ll find it much easier to work with, and much more accurate!

Everything is a mess and I apologize.

Hello everyone, I know I said I would be back shortly, working on projects that you care about, and I apologize that I have been unable to fulfill those promises.

As past readers know, I got a co-op (like an intern, but longer) at a really great company. The bad news is that there wasn’t any internet nearby (problem solved now) and that it happened very quickly during finals week, so I was bogged down really badly.

Now more bad news keeps piling up: Perhaps you have heard of the Missouri river flooding? Yeah, the river is high. It’s pretty intense. The place I am employed is so badly flooded that they had to shut down their big machinery and go on standby (a very expensive task at this company!) and all the co-ops were requisitioned to flood detail. This won’t change until the river goes back down about two more feet, so…

Anyway, I am not going to make any promises here except one: I have not abandoned this project, and I remain dedicated to producing a plugin to not only replace the old Sermon Browser plugin, but to replace it with something far better.

It’s just that life keeps getting in the way of progress.

Anyway, I really have no idea when I’ll get back to working on this, but if I had to guess it will probably be another several weeks before I can dig into it again. It’s on my to-do list, and it’s even on the high priority to-do list, so…

Apologies for delays

Hello all, I wanted to apologize for the delays in promised updates.

As you may be aware I am still a university student, and I had to stop working on this during final exams. Immediately after final exams I was offered a great job in another city, so I had to pack up and move. The place I moved to is out into the countryside a ways, so it doesn’t have internet yet.

I talked to the phone company, and they do offer moderate (it’s slow-ish, but reasonable) internet access via a typical DSL phone-modem, so I’ll be setting that up within the next few weeks. Until then, I’ll be working from home and toting from my laptop so I can drag that into town to a coffee shop and commit updates. Progress will be slow until early June, when I get settled into the new job and have internet access.

My next update will be a few repairs and code standardizations, and then I’ll be releasing it via the official WordPress website. Hooray!

Until then, keep on serving the Lord!

Site Overhaul

I’ve been putting in many long hours and making several updates on my latest project, the Sermon Posts WordPress plugin, so the updates have been all that’s been coming through.

Although the old layout suited my needs at the time, the focus of this site is not only on the totally cool plugin but on my general research and projects. Because of this, and in anticipation of several new hot projects I’ll be working on over the summer, I gave the site a major overhaul.

You’ll notice a friendlier new theme, a big change in layout, and soon you’ll be able to customize your own RSS feed! No log-in or registration required!

Want to hear every single thing I say? Haha!You’re making me blush!

But seriously, if you aren’t interested in a particular project but want to stay informed of other things, you’ll have that option! It’s actually in place now, but I need to make some sort of form for it.

Also, do people want emails instead? You’ll be able to do that as well!

[Update: So apparently designing on a small screen makes some aspects difficult to check. Like centering and repeating header images. Yeah...]

Sermon Posts 0.11

Added a couple more options to adjust the size of the Thickbox “Add/View Files” pop-up, hopefully I’ll make it dynamic at some point.

Speaking of which: The “Add/View Files” button now works! It’s even pretty close to what I wanted! Check it out, try and break it, then let me know what happened because I’m pretty sure there are still bugs. It took me forever, wow.

Which reminds me: This plugin is still BETA! I just want to make it clear, since it still has the possibility to be insecure, or trash your data. I try hard to do the Right Thing, but it’s getting to be pretty big.

Other SVN-esque notes: Piles of new functions added to sermonposts.php, and pretty much the whole fileupload.php was rewritten. The metabox.php really doesn’t have much different, just the link to the thickbox pop-up changed.

I removed a bit of code that was supposed to allow for plugin updating for unofficial plugins. It would have been nice to have during beta, but I couldn’t get it working and was planning on taking it out anyway, so now it’s gone. Wheeee!