PDA

View Full Version : [How-To] Extend and use the Session Table Effectively


mfyvie
15 Jul 2007, 20:50
[How-To] Extend and use the Session Table Effectively

Have you ever wondered what would be the most efficient way to pass information between pages as a user moves through the forum? What about if you wanted to check for a certain condition in a few different places, but the check itself involved a few different operations and you were worried that this might add too much overhead on a busy forum?

You can make use of vBulletin's session table to store this information. This will also have the added benefit of other people being easily able to make use of your session variables in their own modifications.

The basics

Let's make up a fictitious example. Imagine we want to create a session variable called "mozillian". This variable will be set according to whether the user has the text string "Mozilla" in their user agent. Sure, it would be simple to test the user agent variable ourselves, but we are trying out a simple example.

Now because we don't want to have to do our test every time, we'll test it just once when the session is first created and then store the result in a variable.

Let's go ahead and create an extra column in our session table. We'd use a query (or installation code) something like this:
$vbulletin->db->query_write("ALTER TABLE " . TABLE_PREFIX . "session ADD mozillian TINYINT(4) DEFAULT 0 NOT NULL");You might be wondering why we didn't create that column as BOOLEAN and why we made it "NOT NULL". This will be explained later.

Now that our column is ready we'll be able to access the result in our plugins as $vbulletin->session->vars['mozillian'], or $session[mozillian] in templates. First we'll take a look at how to get it there and some other issues.

Using vBulletin's built-in session class and session functions

At this point you may also want to check out the vBulletin 3.6 code documentation (http://members.vbulletin.com/api/), specifically in the "Class(es)" section under "vB_Session". Under "Method Summary" you'll see methods that concern us - build_query_array(), save() and set(string $key, string $value). Of these three methods, we'll only be using one of them directly.

Before we go on, let's take a quick look at how vBulletin handles sessions from the beginning of a page load to the end.

Near the beginning of each page load vBulletin checks to see if a session exists for the current user, and if not it creates one. If an existing session is found, all of the columns in the session table are loaded automatically into the $vbulletin->session-vars array. This makes life very easy for us, since we don't have to do anything to read these values once they are written to the session table.

As the page loads other details may need to be changed in the session table, for example the user's location within the forum or the number of unread messages. When these changes occur the set(string $key, string $value) method is used to change the variables, but they are not saved immediately to the database. As each key is updated with a new value vBulletin tracks which variables need to be changed. Just before the page finishes loading (and output is flushed to the browser) the save() method is called and any changes (and only those changes) are written to the session table, ready to be read again when the next page loads.

How should we apply these functions?

Our logic is very simple - first we need to check to see if our variable has been set - if not, this is the beginning of a session and we need to execute whatever code we need to determine how our session variable should be set. For our above example it means we would be starting a plugin at the global_start hook which would begin something like this:
if ($vbulletin->session->vars['mozillian'] == 0)
{
// Set to 1 if user agent contains "Mozilla", or -1 if it doesn't
$mozilla = strpos(USER_AGENT, 'Mozilla') ? 1 : -1;If you were wondering why we didn't use a boolean earlier when we added a new column to the session table, you are probably wondering why we are using -1, 0 and 1 for our values, rather than a simple true/false value. We'll come to this in a minute.

So our code is only ever going to execute once, and that's when the session is first created. Once our session variable "mozillan" is set, we don't need to run our code again (important point to save some resources on a busy system). Now we need to write our variable to the session table. However, because our column is "non-standard" the methods within the session class will not accept our changes until we push the column name into a special array like this:
$vbulletin->session->db_fields = array_merge($vbulletin->session->db_fields, array('mozillian' => TYPE_INT));(note that another method within the session class called build_query_array() cleans variables according to whether they are TYPE_INT or TYPE_STR - everything else it treats like TYPE_STR, just something to bear in mind if you pass something different into this array or subsequent set() (coming up).

Now that we've made sure our new variable will be included in the update, it's time to set it:
$vbulletin->session->set('mozillian', $mozilla); This writes the value (-1 or 1) that we tested earlier. That's pretty much all we need - our entire plugin now looks like this:
if ($vbulletin->session->vars['mozillian'] == 0)
{
// Set to 1 if user agent contains "Mozilla", or -1 if it doesn't
$mozilla = strpos(USER_AGENT, 'Mozilla') ? 1 : -1;
$vbulletin->session->db_fields = array_merge($vbulletin->session->db_fields, array('mozillian' => TYPE_INT));
$vbulletin->session->set('mozillian', $mozilla);
}So just to summarise what's happened so far: The first time our user visits the forum our variable returns 0 (the default value) and our plugin runs. We set it either to -1 or 1 and the value is written to the session table. When the next page loads, our value will be retrieved again from the session table (along with everything else). Because the value is no longer 0, our plugin will not execute again. Other pages are now free to make use of our variable without incurring any additional overhead.

Caveat one - not using save()

Note that we didn't use the $vbulletin->session->save() method. Why not? My own tests have shown that if you do, you sometimes get the results overwritten with the other save() operation that vBulletin performs. There's nothing in the documentation to warn about this, I discovered it only after a lot of digging around and head-scratching. See this thread (http://www.vbulletin.org/forum/showthread.php?t=152221) for an example of a problem I had. Therefore we simply use the set() method to put our variables into the list of changes to be written later to the session table. When the page ends, all the changed variables will be written to the session table automatically and we didn't need to use any additional queries! Remember - if you do use save() manually, you risk not having your data written correctly to the session table - just don't do it.

Caveat two - writing zero or a null value with set()

You'll see from our example that we basically use three states - the user is a mozilian, they are not a mozillian, or we don't yet know. Now you've probably been wondering why we haven't simply used a boolean true/false here, and rely on the value being NULL or simply not set (testing with the isset() function for example) when we don't know. This seems like a reasonable idea, except for one small problem - during the set() method we encounter this code:
if ($this->vars["$key"] != $value)This basically tries to say that if our value is identical to what we already had, then there is nothing to change and our change gets rejected and not added to the update list. This seems logical until we consider a case where our default value is NULL and we try to write 0 (as an integer or boolean) using save(). Essentially this equates to:
if (NULL != 0)Of course we all know that NULL and 0 are two very different concepts and are not equal, therefore this statement should read true. Unfortunately it is false. This means if you start with a null value and try to write 0, it simply won't work and you'll be wondering why your value doesn't get written to the session table.

For this reason I've chosen to make the default value 0, and the "true / false" states 1 and -1 respectively. You may want to verify your results by looking into your session table and/or writing out log entries to make sure that everything is working the way you expect it to.

Caveat three - using string values and null with set()

Imagine that instead of a numerical value we were dealing with a string. Let's say that we record some other detail in our "mozillian" column like the operating system the browser is running on. If we can't find it, we'll write a string like '' which will cause the variable to be set, then we can use isset() to test whether we need to run it. Imagine the previous example, but something like this instead:
if (!isset($vbulletin->session->vars['mozillian'])
{
if ($some_condtion)
{
$mozilla = 'PC';
}
else
{
// Didn't find what we were looking for, but we don't want to do this again.
$mozilla = '';
}
$vbulletin->session->db_fields = array_merge($vbulletin->session->db_fields, array('mozillian' => TYPE_INT));
$vbulletin->session->set('mozillian', $mozilla);
}Of course, this seems reasonable - if we write the value as '', the variable itself will still be set, and therefore next time the page is loaded the variable will be set, so our code won't execute twice. Wrong. Our variable will remain set while this page loads, but once NULL is written to the database (which is what '' really is) then it won't be loaded when the next page loads - your variable will then not be set, and the example listed above will not work. I've tried writing the null byte - chr(0) which didn't work either. I worked around this by writing ' ' (space character) and then using trim() on any variables that I read back before using them. In other words, after the above routine I would place this code after the conditional (so that it always ran on each page):
$vbulletin->session->vars['mozillian'] = trim($vbulletin->session->vars['mozillian']);This would mean that the variable would effectively be set back to null (if it were a space) and you could test it as such in any program code in another plugin.

Examples

There are two mods which use these techniques and it was while I was writing and testing these mods that I discovered many of these issues. I thought that by writing this tutorial I might be able to help others save a lot of the time that I wasted learning from my mistakes. The GLA (http://www.vbulletin.org/forum/showthread.php?t=151601) (Geographic Location Awareness) mod demonstrates setting a variable (or three variables to be precise) as strings, and the Remove Spiders from Who's Online (http://www.vbulletin.org/forum/showthread.php?t=152321) mod demonstrates setting an integer (which acts like a boolean). If you study these mods it may help you to understand the practical application of this tutorial.

amcd
03 Aug 2007, 08:33
very good article. :claps:

why should we use the 'global_start' hook? it would be better if vbulletin provided us with a 'session_create_complete' hook.

I think it would be better to use empty() instead of isset() because in class_core.php:

$session = $db->query_first_slave("
SELECT *
FROM " . TABLE_PREFIX . "session
WHERE sessionhash = '" . $db->escape_string($sessionhash) . "'
AND lastactivity > " . (TIMENOW - $registry->options['cookietimeout']) . "
AND idhash = '" . $this->registry->db->escape_string(SESSION_IDHASH) . "'
"
.
.
$this->vars =& $session;

This only affects existing sessions, so it will not matter after some time. It will only make a difference immediately after plugin installation or edit, but that may be significant in some situations.

dwh
28 Sep 2007, 22:52
Great post!
I don't know how I'm going to use it yet, but I've wondered about how to do this in the past.

David LeBow
12 Nov 2008, 11:53
I have custom ad rotator logic implemented in an asp page. In the asp page (VBScript), I have two custom lines of code to store and retrieve a value from a session variable:

nNext = CInt(Session("nNext"))
...
Session ("nNext") = CStr(nNext)

Using the ASP page outside of my VBulletin forum, I can read/write to the session variable with no problem. When I use the page from within VB, I don't seem to be able to store/retrieve the value.

Do I understand your post correctly: only session variables which are in the VBulletin Session Table will be retained within a session? Would I need to add "nNext" to the session table before my non-VB page could access it?

Thanks

orion808
23 Jan 2009, 13:09
FYI: The author isn't being rude by not responding. Follow his Remove Spiders from Who's Online (http://www.vbulletin.org/forum/showthread.php?t=152321) link.

zylstra
21 Sep 2013, 08:32
Of course we all know that NULL and 0 are two very different concepts and are not equal, therefore this statement should read true. Unfortunately it is false. This means if you start with a null value and try to write 0, it simply won't work and you'll be wondering why your value doesn't get written to the session table.

For this reason I've chosen to make the default value 0, and the "true / false" states 1 and -1 respectively. You may want to verify your results by looking into your session table and/or writing out log entries to make sure that everything is working the way you expect it to.
No need to use 1 and -1. Simply use
(NULL!==0)for your conditional.

schan
08 Nov 2013, 20:19
Nice article, but I have a quick question. I might overlooked it, but I didn't see anything about escaping "$mozilla" before inserting it into the database. For security reasons, I thought you always escape something before inserting it into the database. Is this not needed for some reasons? I don't think "set" automatically escapes it, right?

zylstra
08 Nov 2013, 21:50
The database insertion code is not shown here. We assume vB escapes it before inserting it to the database.

quitsmoking
16 Mar 2014, 18:31
Thanks a million :)
You made my day.