Category Archives: Tips

How To Use WordPress Filters and Actions To Extend Plugins Functionality

There are two main ways to extend or customize existing WordPress plugin without editing the code directly – filters and actions (as a whole called “hooks”). The WordPress Codex does a good work of documenting the functions for actions and filters. It does not do such a good job in giving easy to understand example how to actually use this to do something useful. We are going to fix the problem with this tutorial.

Before going further you must have one thing in mind – the plugin that you are going to customize must be customization-friendly and offer at least some hooks. Example of such plugin is our Namaste! LMS with its Developer’s API. If you are writing a plugin it’s good to think about adding do_action and apply_filters calls on the most important places.

In this tutorial I’m going to use a couple of examples from Namaste! LMS and our other developer-friendly plugin WatuPRO to illustrate the usage of only the 4 most important action / filter functions:

  • do_action
  • add_action
  • apply_filters
  • add_filter

Let’s go!

Filters

The below chart will give you basic idea how filters work. Then we’ll provide some examples.

filters

Filters are used mostly when you want some content to be modified before displayed or sent to user. You can apply filters on text but not only – there are a lot more creative ways to use filters. Here are a couple examples from Namaste! LMS:

1. All the places where courses are selected to be shown to the user in Namaste! LMS have this hook:
$courses = apply_filters('namaste-reorder-courses', $courses);

What does it do? It allows other plugins to apply changes directly to the $courses array. And Namaste! PRO uses exactly this filter to apply the custom order of courses to the array by catching the filter:

add_filter( 'namaste-reorder-courses', array('NamastePROCourses', 'reorder_filter'));

There are several things that you must understand here:

a) The “source” plugin (the one that you will be customizing) must provide the proper filter by calling apply_filters($tag, $value) on the variable that will be modified. The first argument is the tag of the filter. It’s just unique name that will then be used by other plugins in the add_filter($tag, $function) call. When the name matches, the function defined in add_filter will be executed over the content of $value from apply_filters in the other plugin. Thus you can modify the $value (in our cases $value is the variable $courses).

b) The extending plugin must catch the filter with add_filter() call as explained above. But in order anything to happen the extending plugin should also define the function or method mentioned in the add_filter call (the second argument called $function). Look at the code again:

add_filter( 'namaste-reorder-courses', array('NamastePROCourses', 'reorder_filter'));

This means your customization plugin should have a class called NamastePROCourses with method called reorder_filter.

c) The apply_filters() call returns the modified $value which should also be returned by the function defined by add_filter(). It’s not important how exactly the method reorder_filter works but it must return the modified variable:

static function reorder_filter($courses) {
   // do something with $courses inside the method
   // not shown here

   return $courses; // return the modified variable
}

Only this way the $courses variable will be modified in the original plugin.

d) Multiple plugins can apply changes to the content using the same filter tag. So you can write your plugin that will for example add more properties to the $courses array, slice it, erase it, change it to something completely different and so on.

e) Do not rely on order of filters execution. When writing a custom plugin you should never assume that add_filter() calls from other plugins will be executed before or after your call.

f) If no filters are defined with add_filter for a given tag, then the apply_filters call will return the original variable.

2. As you saw from the above example, you can filter not just text but any kind of variable – in our case it was an array. The below example will demonstrate a smart trick using text. We need to allow external plugin to apply user-based restrictions to the SQL query that selects courses. instead of working with the courses array, we just use SQL variable string that will be applied to the query:

$filter_sql = '';
$filter_sql = apply_filters('namaste-course-select-sql', $filter_sql, $user_ID);

In the above example the variable $filter_sql (which is after that used inside the SQL query string) is first initialized as empty string. This way if no plugin defines add_filter on the namaste-course-select-sql tag, the string will remain empty and the query will remain as is. However if another plugin catches this filter and adds some SQL to it, the query which contains the $filter_sql variable will also be changed!

The above example also passes additional argument so the add_filter call needs two extra arguments:

add_filter( 'namaste-course-select-sql', array('NamastePROClass', 'course_select_sql'), 10, 2);

Check the add_filter documentation to understand why they are required.

Actions

Actions don’t return value and don’t let you modify variables but are probably even more powerful and easier to understand than filters. Actions let you notify other plugins that something in your plugin has happened. Then the other plugins can act based on it. You can also pass arguments to actions. The “source” plugin must call do_action($tag) and the receiving (customization) plugins must call add_action($tag, $function) with the same $tag to catch the action and call the $function. Here are a couple of examples:

1. Probably the most used action call in WatuPRO is the “waturpo_completed_exam” action called when a quiz is completed:

do_action('watupro_completed_exam', $taking_id);

This call sends the ID of the just taken quiz record so other plugins can use it. And they do. For example the Play Plugin catches this action to update user’s level, badges, points balance etc:

add_action('watupro_completed_exam', array($_user, 'update_meta'));

The variable $_user in this case is instance of the WatuPROPlayUser user object which has method update_meta. The method update_meta takes one argument – the taking ID – and does things with it:

function update_meta($taking_id){ /* do something here */}

Of course, just like with filters, the call to add_action must use the the same tag as the call to do_action to catch the action – in this case the tag is watupro_completed_exam.

2. Here’s a bit different example, this time from Namaste! LMS. At the end of creating the menu of the plugin we add the following simple do_action call, no arguments needed:

do_action('namaste_lms_admin_menu');

This lets other plugins hook their links under the Namaste! LMS menu so their extra options nicely align in it instead of creating more top-level menu links. This action is caught by Namaste! Reports, Namaste! Connect, Namaste! PRO, the InstaMojo integration plugin and many more. If you write your own plugin for Namaste! LMS I recommend using the same action (in case you need to add pages to the menu). Example call:

add_action('namaste_lms_admin_menu', array('NamastePRO', 'menu'));

Then the menu() function in NamastePRO class simply adds its own add_submenu_page() calls to add more links (note that the add_submenu_page() parent slug must also be properly defined).

Actions and filters give you huge options for customization and the best of it: without touching the code of the original plugin. Beautiful! Of course this is only possible if the original plugin defines do_action and add_filter calls in the proper places. If it doesn’t you can try to request the original developer to add some hoks. Most will not refuse because adding a hook here and there is not hard and makes the plugins easier to extend.

If there’s anything unclear in the above explanations, please ask in the comments.

InstaMojo Integration for Namaste! LMS

This plugin will let users from India who have the Intelligence module for Namaste! LMS to sell access to courses and classes via the Instamojo payment service.

The plugin requires Namaste! LMS min. version 1.6.2. If you use PRO, the min. compatible version is 1.1.8.

Download namaste-instamojo v. 1.1 here (18 KB)

From version 1.0 the bridge also works with the Namaste! PRO shopping cart.

How To Use It

Install the plugin and activate it. A new menu item will appear under your Namaste! LMS menu. The item is called “Instamojo integration”:

Before you can proceed you need the following:

  • Your API key and Auth token. Once you have an Instamojo account you can obtain these from the Developers section.
  • A dynamic payment link. The link must have a fixed price and “Pay what you want” options selected.
  • The link must also have two custom fields added in the Advanced Settings section in your Instamojo link management page. One of the fields should be called item_id and the other field should be item_type.
  • It needs to have “Custom redirection URL” attribute also set up. You can see the URL in your “Instamojo Integration” page in Namaste! LMS.

Once all this is done, ensure a couple more things:

1. You must enter the field identifiers of the custom fields in the Instamojo Integration page. The field indentifiers are visible in the “Custom fields” section of the Instamojo link management page for the given link. You need to mouse over the field names to see the identifiers.

2. You have to enter the following code inside your “Other payment instructions” box in Namaste! LMS Settings page (exactly as you see, just copy and paste)

[namastemojo-button amount="{{amount}}" item_id="{{course-id}}" item_type="{{item-type}}"]

This will generate a payment link on your paid quizzes. It can work along with all other payment methods. The payment link has class “namastemojo-button” so feel free to style it.

Create a Text-Based / Question-Based “Captcha”

Google’s recaptcha is really good but not always the most optimal tool for stopping spambots. It takes a bit too much space, requires entering app IDs, and sometimes is hard for the visitors to solve.

Question based captcha’s are often easier solution. A simple question that requires textual answer is usually enough to keep most bots away, and is easy to code, requires no graphic libraries or API keys.

Two of our WordPress plugins – WatuPRO and Arigato PRO already implement such captcha features with great success. Here’s how we did it:

The steps

There are two main steps: to generate the captcha, and to verify it. In the case of our plugins we let our users to create their own questions in the admin panel. Here for simplicity we’ll assume the questions are hardcoded in a PHP array.

Generating the captcha

So, let’s assume we have the questions in array called $captcha_questions in such format:

 

$captcha_questions = array("What is the color of the snow? = white",
                          "Is fire hot or cold? = hot"
                          "In which continent is France? = Europe");

 

This code uses one array element for question and answer separated by = sign. You can of course use array of arrays or key => value pairs. But the above example is closer to to the format we let our plugin users input for questions in their admin panel.

So, now we need one random question of these and need to display it on the front-end:

 

shuffle($captcha_questions);
$question = $captcha_questions[0];
list($q, $a) = explode("=", $question);
$q = stripslashes($q);

 

Now let’s echo the question on the screen and display a field to enter the answer. You need also one hidden field. Depending on how you keep your questions in the database you may want to pass question ID. If you use hardcoded array of questions you can pass the question key. Or if you use something else, just pass the question itself – it’s a good idea to use base64_encode to ensure easier matching on the back end.
Do not, of course, include the answer anywhere on the page:

echo trim($q)." <input name="text_captcha_answer" type="text" />\n<input name="text_captcha_question" type="hidden" value="\"".base64_encode(trim($q))."\"" />";

Verify the captcha

I’ll just to ahead with the whole function and then explain:

// this function will actually do the verification
function verify($question, $answer) {
     $answer = stripslashes($answer); // don't forget this or you may get unexpected results
		
     // the $captcha_questions variable needs to be recognized here as well. Use global, constant,  DB entry etc

     $question = base64_decode($question); // decode the hidden field with the question
	
     // go through all questions and compare	
     foreach($captcha_questions as $captcha_question) {
	list($q, $a) = explode("=", $captcha_question);
	$q = trim($q);		
	$q = stripslashes($q);
	$a = stripslashes($a);
				
	if(strcmp($q, $question) == 0 and strcasecmp(trim($a), trim($answer)) == 0) return true;
}		

That’s it. The above function goes through all the questions in the array (note that the code does not show where $captcha_questions variable comes from – don’t use the function as is, you need to declare the variable), explodes question and answer and then compares each of the combinations to the values given to the function. In case of match, returns true.

Pay special attention to using stripslashes() and trim() or you’ll have a lot of headaches to figure out why captcha answers are not correct.

Note also that we use strcasecmp to compare the answer to the question because we don’t want the check to be case sensitive.

And here is how to use it roughly:

// the user has submitted the form. Use different variable name if you wish
if(!empty($_POST['submit'])) {
   // verify the captcha
   if(!verify($_POST['text_captcha_question'], $_POST['text_captcha_answer'])) die("The answer to the verification question was not correct.");
   
   // now process the form
}

The above code isn’t super user-friendly because it will simply die in case of error. I recommend you to implement the check with ajax and stay on the same page until correct answer to the question is given.