View Full Version : [How-To] Plugins for Template Edits (Adv. Version) - What your mother didn't tell you

04 Jul 2007, 09:44
Plugins for Template Edits (Advanced Version) - What your mother didn't tell you about templates

In this tutorial we are going to tackle some of the more mysterious and tricky aspects of using the plugin system to perform automatic edits of templates in vBulletin. I've added the "what your mother didn't tell you about templates" to the title because I figured all this out after a lot of banging my head against the wall. I didn't find any information about this anywhere, so I figured the best thing to do was share what I've learned.

Before we start

This tutorial is going to build on material I covered in my earlier template tutorial - Using Plugins for Automatic Template Edits (http://www.vbulletin.org/forum/showthread.php?t=151254). Please read it and understand it before reading this tutorial. Even if you think you know everything, please at least go back and skim-read it.

During this tutorial we are also going to use some of the tools and techniques I covered in the tutorial called Debugging Your Plugin - how to save time and frustration (http://www.vbulletin.org/forum/showthread.php?t=151288). There is an include file on that thread containing some functions that we'll be using. You should also go and read and understand the debugging tutorial before continuing with the current tutorial.

Why do we need to know this stuff?

Because you want to do automatic template edits, but you learned in the previous tutorial that you have to stay away from any conditional statements in a template, tab characters, new lines. That's not much fun! Maybe you've come to this tutorial because you've been scratching your head trying to wonder why you can't seem to get a conditional statement to work when you change a template with a plugin. You've come to the right place, because we are going to get our hands dirty in this tutorial!

What's the main issue here?

When you look at a template in the template editor you are not seeing what is really stored in the template, but rather a sanitised version which is easy for you to understand and edit. In reality, the template is converted into PHP code when it is stored or used. Therefore we need to look at the "wiring under the board" if we really want to be successful at using the plugin system to edit templates automatically. You may never look at a template the same way again...

So what does a template really look like?

OK, you already understand how to use str_replace and you know how to make a hex dump of a template (because you've read and understood the previous two tutorials), so you understand that unless we can correctly identify the exact string to replace and insert code that will actually work, there's little point in attempting this.

The first thing we need to do is actually look at a template. We are going to use the dump_hex() function to do this. Since we want to make our life easy, we are probably also going to use the print_log() function to write the data to a log file for us to check. Remember that these functions are not part of PHP or vBulletin - but you already know that from the debugging tutorial and you downloaded the file attached to the debugging tutorial with those functions included. I know you want to look at the template, but first we have to set up our plugin and decide what to look for.

Before we start - is your template cached?

Remember to consider at which hook your str_replace is going to execute. If you run it at a hook like global_start, was it placed into the $globaltemplates array so that the template in question is put into the template cache? If you are performing a str_replace on built-in vBulletin templates this isn't an issue as they are always cached in the relevant places anyway. However, if you are changing other templates from other mods (or your own) with this method, then it will not work unless your template it is in the cache first!

Create your plugin

Create a plugin, and execute it at a sensible hook for the template we are going to work on. We will work with two templates, WHOSONLINE and whosonlinebit. Create your plugin at online_start. Our plugin is going to add a new column to our who's online display to display the country where a user is logging on from. The underlying variables used for this won't function (unless you have one of my forthcoming mods installed already), but we should be able to at least create an empty column.

You'll remember that we started all our plugins with something like this:
if ($vbulletin->userinfo['userid'] == 1)
// include some special functions we might want
include_once(DIR . '/includes/devtools.php');
// Our code goes here.
}Now we don't have to worry about other users seeing our changes and we have our extra debugging functions available. If your userid is not 1, change it now.

Examine the template in the editor

Now go and load the the WHOSONLINE template via the style manager. We are going to insert the code highlighted in bold. Locate the area around this insertion point and try to figure out which string we should try to match to make the replacement.
<if condition="$show['ip']"><td class="thead">$vbphrase[ip_address]</td></if>
<if condition="$show['country']"><td class="thead"> $vbphrase[show_country]</td></if>
<if condition="$vboptions['showimicons']"><td class="thead" align="center">$vbphrase[instant_messaging]</td></if>
</table>You'll remember from the first template tutorial that we were supposed to stay away from conditionals? We are about to find out why that is.

Dumping the template with dump_hex()

Insert the following code in the middle of the plugin you just created:
printlog(dump_hex($vbulletin->templatecache['WHOSONLINE']));Now login to your forum and load the who's online display. Nothing unusual will display, but when you go to your log file you should find that it now has something in it. Unless you've changed the path to the logfile in ./includes/devtools.php, you can find your logfile in ./stats/devlog.log. You'll see that you now have something that looks like this:
0000 24 73 74 79 6c 65 76 61 72 5b 68 74 6d 6c 64 6f $styleva r[htmldo
0010 63 74 79 70 65 5d 0d 0a 3c 68 74 6d 6c 20 64 69 ctype].. <html di
0020 72 3d 5c 22 24 73 74 79 6c 65 76 61 72 5b 74 65 r=\"$sty levar[te
0030 78 74 64 69 72 65 63 74 69 6f 6e 5d 5c 22 20 6c xtdirect ion]\" l
0040 61 6e 67 3d 5c 22 24 73 74 79 6c 65 76 61 72 5b ang=\"$s tylevar[
0050 6c 61 6e 67 75 61 67 65 63 6f 64 65 5d 5c 22 3e language code]\">
0060 0d 0a 3c 68 65 61 64 3e 0d 0a 24 68 65 61 64 69 ..<head> ..$headi
0070 6e 63 6c 75 64 65 0d 0a 24 6d 65 74 61 72 65 66 nclude.. $metaref
0080 72 65 73 68 0d 0a 3c 74 69 74 6c 65 3e 22 20 2e resh..<t itle>" .
0090 20 24 47 4c 4f 42 41 4c 53 5b 27 76 62 75 6c 6c $GLOBAL S['vbullThis is the start of the template, if you keep looking you'll see the entire template is there. Personally I don't find it easy to locate the part I'm looking for when the display is listed in two columns of eight characters, so let's update our code to look like this:
printlog(dump_hex($vbulletin->templatecache['WHOSONLINE'], true));Adding the first parameter as "true" makes this function skip the hex part and show us just the ASCII. This is easier to read. Save your plugin, reload your who's online page and go back to your log. Locate the section of template code we examined earlier.
1600 $vbphrase[location_temp]</a> $sortarrow[location]</td>...".(($show['ip']) ? ("<t
1680 d class=\"thead\">$vbphrase[ip_address]</td>") : (""))."...".(($GLOBALS['vbullet
1760 in']->options['showimicons']) ? ("<td class=\"thead\" align=\"center\">$vbphrase
1840 [instant_messaging]</td>") : (""))."..</tr>..$onlinebits..</table>....".(($show, [B]false, 2, 16));Rather than the tiny view of two columns of eight characters we had earlier, I'd prefer to have two columns of sixteen. You'll notice that we now inserted "false" and two numbers into this function call. We have to insert false (hex display mode) because we want to specify the second and third parameters, which are the number of columns and column width. Let's look at our output and specifically check just the section between offsets 679 and 756 (in hexadecimal this time):
0660 72 74 61 72 72 6f 77 5b 6c 6f 63 61 74 69 6f 6e 5d 3c 2f 74 64 3e 0d 0a 09 22 2e 28 28 24 73 68 rtarrow[location ]</td>...".(($sh
0680 6f 77 5b 27 69 70 27 5d 29 20 3f 20 28 22 3c 74 64 20 63 6c 61 73 73 3d 5c 22 74 68 65 61 64 5c ow['ip']) ? ("<t d class=\"thead\
06a0 22 3e 24 76 62 70 68 72 61 73 65 5b 69 70 5f 61 64 64 72 65 73 73 5d 3c 2f 74 64 3e 22 29 20 3a ">$vbphrase[ip_a ddress]</td>") :
06c0 20 28 22 22 29 29 2e 22 0d 0a 09 22 2e 28 28 24 47 4c 4f 42 41 4c 53 5b 27 76 62 75 6c 6c 65 74 (""))."...".(($ GLOBALS['vbullet
06e0 69 6e 27 5d 2d 3e 6f 70 74 69 6f 6e 73 5b 27 73 68 6f 77 69 6d 69 63 6f 6e 73 27 5d 29 20 3f 20 in']->options['s howimicons']) ?
0700 28 22 3c 74 64 20 63 6c 61 73 73 3d 5c 22 74 68 65 61 64 5c 22 20 61 6c 69 67 6e 3d 5c 22 63 65 ("<td class=\"th ead\" align=\"ce
0720 6e 74 65 72 5c 22 3e 24 76 62 70 68 72 61 73 65 5b 69 6e 73 74 61 6e 74 5f 6d 65 73 73 61 67 69 nter\">$vbphrase [instant_messagi
0740 6e 67 5d 3c 2f 74 64 3e 22 29 20 3a 20 28 22 22 29 29 2e 22 0d 0a 3c 2f 74 72 3e 0d 0a 24 6f 6e ng]</td>") : ("" ))."..</tr>..$onI've highlighted the start and end of our text for you again, both in the hex and ASCII views.

What can you see? You can see that we have a few changes. I'll list some statements that we had in the template, followed by what they actually become when they are stored in the template in green:
<if condition="$show['ip']"> ".(($show['ip']) ? ("
<if condition="$vboptions['showimicons']"> ".(($GLOBALS['vbulletin']->options['showimicons']) ? ("
</if> ) ? ("Notice the dots here really are dots. We confirmed this by checking the corresponding hex value which was 2e. Remember that non-printable characters are displayed in the hex dump as dots, so when we see a dot on the ASCII side, we need to check what it really is in an ASCII table by checking the hex value on the hex side. Did you also notice that our single quote characters have not become escaped with backslashes? You'll remember we went over this in the first tutorial, but we learned that every double quote in the template is escaped. However, single quotes are not escaped, at least not when they are stored in the template. Ever noticed how we never actually use single quotes in the templates we see in the template editor? Now you can see the reason - they are only ever used in the parts we can never see in the template editor. Later on it will look as though we are escaping single quotes when we put certain strings into variables. Remember that in these cases we are only escaping them to get single quotes into a variable, they won't actually go to the template in their escaped form as the double quotes do. If you are totally confused at this point, don't worry - just follow the examples.

The only other thing to note in this output is where you see 0d 0a - this is a new line. Where you see 09, this a tab. Since these are non-printable characters they will appear in the ASCII side as dots. Remember these are hex values, we'll be working with their decimal equivalents in a minute.

Constructing the required strings

Now we need to build some code to find and replace strings within our template. You'll remember we did the same thing in the first tutorial, but now we are going to get a little more complicated. Let start by entering the following code:
$ifstart = '".(('; // equates to first half of if - <if condition="
$ifend = ') ? ("'; // equates to second half of if - ">
$endif = '") : (""))."'; // equates to an endif - </if>
$newline = chr(13) . chr(10); // equates to a new lineRemember the list with the translations in green a bit earlier? We are basically making short cuts for ourselves for the strings we are about to construct. This is because all those bracket and double quote characters can get confusing. Notice that we wrapped the strings in single quotes to get them into the variable, the single quotes don't actually get stored. Don't get confused with the names of the variables - $endif and $ifend, I've added some comments to the code so you can understand better. Notice $newline? We don't actually need it in this example, but I just wanted to show you how we might use it. The decimal values you see there equate to the hex values 0d 0a that we saw earlier in the hex dump. You could also generate a tab with chr(9).

Now that we've made a few variables to help us construct the string we are going to find, let's add another line:
$find = $ifstart . '$GLOBALS[\'vbulletin\']->options[\'showimicons\']' . $ifend;Looks quite straight-forward? Remember that we couldn't use the original $vboptions['showimicons'] because it's not stored in the template like that. You'll notice we escaped the single quotes inside that variable name. Otherwise PHP would have been confused because we used single quotes to enclose the string itself. Can you see that the variable above will equate to:
".(($GLOBALS['vbulletin']->options['showimicons']) ? ("This is the exact string we need to match, as it is stored in the database or template cache.

Now we can go ahead and built the string we would like to replace:
$replace = $ifstart . '$show[\'country\']' . $ifend . '<td class=\"thead\">$vbphrase[show_country]</td>' . $endif;Now all we need to do is actually run the str_replace() function, exactly like we did in the previous tutorial:
$vbulletin->templatecache['WHOSONLINE'] = str_replace($find, $replace . $find, $vbulletin->templatecache['WHOSONLINE']);Was our insertion of the new text into the template successful? We could either load the who's online page to check, or maybe it would be better to take another hex dump of the template after we've performed the str_replace()? This way we can check to see if our new text was inserted correctly. Hint: if you do this, use the ASCII mode of dump_hex, it's much easier. We aren't going to show you the replaced template because we've been showing quite enough hex dumps so far.

The final code

Let's just review the final code all in one block, without the hex dumps or the additional conditional:
$endif = '") : (""))."';
$ifstart = '".((';
$ifend = ') ? ("';
$newline = chr(13) . chr(10);
$find = $ifstart . '$GLOBALS[\'vbulletin\']->options[\'showimicons\']' . $ifend;

$replace = $ifstart . '$show[\'country\']' . $ifend . '<td class=\"thead\">$vbphrase[show_country]</td>' . $endif;
$vbulletin->templatecache['WHOSONLINE'] = str_replace($find, $replace . $find, $vbulletin->templatecache['WHOSONLINE']);

$replace = $ifstart . '$show[\'country\']' . $ifend . addslashes('<td class="alt1">$userinfo[country]</td>') . $endif;
$vbulletin->templatecache['whosonlinebit'] = str_replace("$find", "$replace$find", $vbulletin->templatecache['whosonlinebit']);You'll notice that we also did some work on a second template there (to fill the column itself with our imaginary data). If you look carefully you'll see that we did things slightly differently. I'm not going to go into a lengthy detailed example, but you should be able to figure the second one out for yourself.

Using these ideas in your own plugins

What we have seen so far has not been an exhaustive list of techniques, but now you can use tools like dump_hex to discover the true variable names and conditional syntax in templates when developing your own plugins. There are many other things inside a template that we didn't list here. What about the <else /> tag?

Remember that now you know how to include newlines and tabs in your searches - don't get carried away. If anybody has changed their template by removing or adding tabs or newlines, your search string may not match. Try to find the best possible insertion point by looking for a place where there is less chance of a change being made by someone else. The smallest possible strings, which are still unique, are best. For example, there would be little point in searching and replacing based on <td class= because this appears many times within most templates.

The bottom line - use these techniques to employ a more structured and methodical approach to your automatic template edits. By actually checking the results before and afterwards and determining in advance exactly what you need to do, you can save yourself a lot of time by avoiding "trial and error".

Alternative methods to see the template

Of course there are other methods to look inside the template, I've just listed the ones I use the most. Rather than going to the template cache, you could display it in your log or to screen simply by accessing fetch_template('your_template_name'). You should be aware that this is only good for reading the template, if you want to change it, you'll still have to go directly to the template cache. Remember also that if you try to display the output in a browser it might not work so well since the template contains HTML formatting. If you are outputting to the command line this should be no problem.

You could also go and look at the template directly in the database if you like. You'll need the appropriate tools, such as phpmyadmin, but if you look in the table called "template" you'll find two columns - "template" and "template_un". The first is the template as it appears to PHP (and the template cache), the second is the same template, but stored in the "human-readable" form that you see when using the style manager. Be careful though - depending on your tools, you will see newlines and tabs as they appear, rather than they actually are - you'll still need a hex dump to see exactly which non-printable characters have been stored.

What's next?

The good news is that there is an easier way. However, it is important for you to understand how this process works, which is why this was such a long and boring tutorial. There will be a third and final tutorial in this series which will show you a much easier way to do these automatic edits.

12 Jul 2007, 19:52
Helpful. :)

07 Nov 2007, 16:44
Ah, this explains a problem I'm having trying to insert some code right before a conditional. Thanks for the writeup! :cool:
The good news is that there is an easier way. However, it is important for you to understand how this process works, which is why this was such a long and boring tutorial. There will be a third and final tutorial in this series which will show you a much easier way to do these automatic edits.I was looking around but didn't spot any subsequent parts. Has the 3rd or 4th part, saying how to do this easier, been posted anywhere? :o

23 Feb 2008, 14:28
hardcore tuitorial mfyvie, very helpful - thanks for explaining how to do this, and preparing the useful devtools.php file

16 Jul 2008, 18:26
There are quite a few "Standard" template hooks in the standard templates

For example, I use this to auto insert the itrader template edit into the postbit and postbit_legacy template. It is fired at the postbit_display_complete hook.

eval('$template_hook[\'postbit_userinfo_left\'] .= "' . fetch_template('cg_itrader_postbit_link') . '";');

You can find these and other template hooks by viewing the template in an editor. I would do a search for them.

I went through and pulled the postbit and postbit_legacy and the navbar template hooks along with when they should be fired.

template name="navbar"
hook = "global_setup_complete"

template name="postbit_legacy" and template name="postbit"
hook = "postbit_display_complete"


24 Oct 2008, 21:22
Personally I find this works a bit easier, the catch is any special tags like conditionals need to be properly started and ended:

//just an example
require_once(DIR . '/includes/adminfunctions_template.php');
$findstring = compile_template('<if condition="$show[\'pagenav\']"><td align="$stylevar[right]">$pagenav</td></if>');
$vbulletin->templatecache['SHOWTHREAD'] = str_replace($findstring, $vbulletin->templatecache['strike_center_edit'] . $findstring, $vbulletin->templatecache['SHOWTHREAD']);

Sticks one template's contents into a specific place of another one :p

31 Oct 2008, 18:24
I went through and pulled the postbit and postbit_legacy and the navbar template hooks along with when they should be fired.Mark, I'm working on some changes and "re-found" this thread. The list you compiled was very helpful, thank you. :cool: