vBulletin 4 and PHP 7, making it work.

Note: vBulletin has now released versions that support PHP 7+, I recommend doing a vBulletin upgrade instead of following this guide. If you do not want to upgrade your forum then you can still use this guide.

PHP 7 has been officially released on the 3rd of December.
It introduces a lot of new features, deprecates a bunch of functions and most importantly, its performance received a huge boost.

I personally used https://webtatic.com/packages/php70/ to upgrade my PHP from 5.4.45 to 7 on CentOS 6.7.
Make sure that you install PHP OPCache as well, it gives you a huge performance boost.

Now vBulletin 4 (<= 4.2.3 at the time of writing this post) does not support PHP 7 out of the box. A couple of changes have to be made in order to make it work smoothly with PHP 7.
There are 3 things we have to fix:
– Methods with the same name as their class may not be constructors.
– Function name must be a string.
– Redefinition of arguments are not allowed.

Step 1: configuration file
First be sure you have a backup of your forum files, this is very important in case the changes do not end up working for you.
Now open the /includes/config.php file and make sure that your datastore class is set to vB_Datastore_Filecache, APC/xCache/Memcached will not work since they do not support PHP 7 (yet) and it’s not necessary to use either of those anyway since OPCache does the job.

Step 2: fixing “function name must be a string”
First we edit /includes/class_bbcode.php. Look for the following:

$pending_text = $this->$tag_info['callback']($open['data'], $open['option']);

and change it to:

$function = $tag_info['callback'];
$pending_text = $this->$function($open['data'], $open['option']);

This will fix the “function name must be a string” error. It seems that PHP doesn’t like it when the value of a key in an array is directly being used as a function name. My fix first assigns it to a variable and then uses it as the function name.

Step 3: fixing /includes/class_datastore.php
Open the file /includes/class_datastore.php and find each of the following:

function vB_Datastore_Memcached(&$registry, &$dbobject)
function vB_Datastore_Filecache(&$registry, &$dbobject)

change them both to:

function __construct(&$registry, &$dbobject)

Now also find the following:

parent::vB_Datastore($registry, $dbobject);

and replace all occurrences to:

parent::__construct($registry, $dbobject);

Step 4: fixing /includes/class_hook.php
Open the file /includes/class_hook.php and find the following:

	function vBulletinHook()
	{
	}

and change it to:

	function __construct()
	{
	}

Step 5: fixing /includes/class_core.php
Open the file /includes/class_core.php and find each of the following:

function vB_Database(&$registry)
function vB_Datastore(&$registry, &$dbobject)
function vB_Input_Cleaner(&$registry)
function vB_Registry()
function vB_Session(&$registry, $sessionhash = '', $userid = 0, $password = '', $styleid = 0, $languageid = 0)

and change all of the function names to __construct:

function __construct(&$registry)
function __construct(&$registry, &$dbobject)
function __construct(&$registry)
function __construct()
function __construct(&$registry, $sessionhash = '', $userid = 0, $password = '', $styleid = 0, $languageid = 0)

Step 6: fixing /vb/exception/parser.php
This is probably a mistake by vBulletin, but we have to fix it in order to make it work.
Open the file /vb/exception/parser.php and find the following:

	public function __construct($message, $line = false, $code = false, $file = false, $line = false)
	{
		$message = $message ? $message : 'Parser Error';
		
		if (!empty($line))
		{
			$message .= "::$line";
		}
		
		parent::__construct($message, $code, $file, $line);
	}

and replace it by:

	public function __construct($message, $line = false, $code = false, $file = false, $line2 = false)
	{
		$message = $message ? $message : 'Parser Error';
		
		if(empty($line)){
			$line = $line2;
		}

		if (!empty($line))
		{
			$message .= "::$line";
		}
		
		parent::__construct($message, $code, $file, $line);
	}

And that’s it. Your vBulletin installation should now work correctly with PHP 7.
In case it does not, check the error logs of your web-server/PHP and make sure that none of your plugins or custom scripts are making use of deprecated functions.

Below is a comparison of vBulletin 4 and PHP 5.4, PHP 7 and PHP 7 with OPCache.
As you can see, vBulletin loads almost 2.5(!) times faster.

Before the upgrade (PHP 5.4.45)
Homepage
409.31 – 429.84 – 389.37 – 401.51 – 389.75 – 426.69
~407.75ms average loading time

Forum Category
518.51 – 491.49 – 447.64 – 491.94 – 433.52 – 543.93
~487.84ms average loading time

Thread
326.34 – 399.72 – 387.09 – 383.79 – 370.03 – 375.21
~373.70ms average loading time

—–

After the upgrade (PHP 7 WITHOUT OPCache)
Homepage
289.79 – 291.41 – 289.11 – 253.45 – 285.06 – 259.98
~278.13ms average loading time

Forum Category
281.83 – 333.31 – 293.23 – 276.73 – 268.91 – 281.63
~289.27ms average loading time

Thread
221.73 – 344.83 – 257.84 – 250.72 – 250.04 – 220.07
~257.53ms average loading time

—–

After the upgrade (PHP 7 WITH OPCache)
Homepage
225.63 – 200.26 – 219.48 – 181.72 – 199.93 – 177.55
~200.76ms average loading time

Forum Category
194.95 – 184.47 – 237.45 – 182.62 – 185.93 – 178.06
~193.91ms average loading time

Thread
207.83 – 156.41 – 146.43 – 143.13 – 155.13 – 142.61
~158.59ms average loading time

How to create your own vBulletin 4 sidebar block.

Since I couldn’t really find any guide on how to create your own forum sidebar block with custom settings, I decided to write this little guide. It consists of a few things:
– The .php file which takes care of most of the functionality
– The configuration
– Phrases

The main .php file
All of the current blocks are present in the /includes/block/ folder. To make your own block we have to make a new file in this directory. Name it something logical since the filename is dependent of a few things. We are simply going to copy the threads.php file in the /includes/block/ folder. For this guide, I copied the threads.php file to technidev.php. The functionality I will implement is to show the latest x registered users.

Now open the file you just duplicated and change the classname, currently it’s “class vB_BlockType_Threads extends vB_BlockType” and we have to rename that to “class vB_BlockType_Technidev extends vB_BlockType”.
Say you duplicated the threads.php file to onlineusers.php, you name it to “class vB_BlockType_Onlineusers extends vB_BlockType”.

The file consists of a few important things:
– The protected $settings variable. This defines the settings of the block.
– getData function. You should use this to retrieve all of the necessary data.
– getHTML function. This will call the getData function and returns any output.

$settings variable
This will display all of the settings when you want to add a new block in the admincp. For our functionality I want the user to be able to select which usergroup is a “registered” usergroup and how many of the latest users we want to display. That means we have to add 2 settings as follows:

	protected $settings = array(
		'technidev_usergroupid' => array(
			'defaultvalue' => 2,
			'displayorder' => 1,
			'datatype'     => 'integer'
		),
		'technidev_amount' => array(
			'defaultvalue' => 5,
			'displayorder' => 2,
			'datatype'     => 'integer'
		)
	);

That’s how easy it is, the $settings variable is very flexible and you can add many things. All of the settings can be reached with the $this->config array, $this->config[‘technidev_usergroupid’] for example. But say you want to make a select box where the user will be able to select multiple usergroupid’s, you can add an option such as:

	protected $settings = array(
		'technidev_usergroupids' => array(
			'defaultvalue' => -1,
			'optioncode'   => 'selectmulti:eval
			$uid = $vbulletin->db->query_read("SELECT usergroupid FROM " . TABLE_PREFIX . "usergroup ORDER BY title ASC");
			$options = array();
			while($row = $vbulletin->db->fetch_array($uid)){
				$options[$row[\'id\']] = $row[\'title\'];
			}',
			'displayorder' => 4,
			'datatype'     => 'arrayinteger'
		)
	);

This will set $this->config[‘technidev_usergroupids’] as an array.
Note that if you are an advanced user, check out the includes/adminfunctions_options.php file and the function print_setting_row, you can see which options you can use in that function.

The getData and getHTML function
The getData function will be used to retrieve any data and return an array. In my case I want to retrieve the latest registered users based on the settings which are specified by the user. We can do something such as:

	public function getData()
	{
		$user = $this->registry->db->query_read("
			SELECT userid, username
			FROM " . TABLE_PREFIX . "user
			WHERE usergroupid = " . $this->config['technidev_usergroupid'] . "
			ORDER BY userid DESC
			LIMIT 0," . $this->config['technidev_amount']);

		$users = array();
		while($row = $this->registry->db->fetch_array($user)){
			$users[] = $row;
		}
		return $users;
	}

The getHTML function should return the HTML output, in this case we can simply create a template, register variables and return the rendered output. Since we retrieve an array, we can simply register it as a variable to our own block_technidev template and then register the output to a variable for the block_html template.

The getHTML function will look like this:

	public function getHTML($userarray = false)
	{
		if (!$userarray)
		{
			$userarray = $this->getData();
		}

		if ($userarray)
		{
			$templater = vB_Template::create('block_technidev');
				$templater->register('blockinfo', $this->blockinfo);
				$templater->register('stream', $userarray);
			$block = $templater->render();

			$templater = vB_Template::create('block_html');
				$templater->register('blockinfo', $this->blockinfo);
				$templater->register('content', $block);
			return $templater->render();
		}
	}

And the template block_technidev will look something like:

		

Phrases
This is a very important thing because if you don’t add the required phrases, the block will miss a lot of information such as the setting titles/descriptions and the name of the block itself.
We first add the block phrase:

1. Languages & Phrases -> Phrase Manager -> Add New Phrase
2. Select “Forum Blocks” as the Phrase Type and “vBulletin” as the product
3. Set the varname to blocktype_yourblock, blocktype_technidev in my case.
4. Set the text to your block name. “Technidev Registered Users” in my case and save.

Now we have to register the variables for the settings:

1. Languages & Phrases -> Phrase Manager -> Add New Phrase
2. Select “Forum Blocks Settings” as the Phrase Type and “vBulletin” as the product
3. Set the varname to “setting_technidev_usergroupid_title”
4. Set the text, this is the title of the setting and save.
5. Repeat this but use “setting_technidev_usergroupid_desc” as varname, this will be the description of the setting.

Testing
Now if you did it all right, you should go to Forums & Moderators -> Forum Blocks Manager -> Click at the bottom on “Reload Block Types” -> Now add a new block -> Find the name of your block -> Configure the settings and add it.

Add Sidebar Block

I added and configured my block, and I now see this in the sidebar:

Sidebar Block

It doesn’t look fancy, but you get the idea. You can fully customize it to your needs within the template and the .php file.

Changing vBulletin 4 its password hashing to use BCrypt

By default, vBulletin uses a very basic and easy to crack password hashing:

md5(md5($password) . $salt)

With the GPU’s of these days, it may be a matter of minutes to crack a password using a dictionary attack/rainbow tables.

Fortunately, it’s very easy to change the password hashing. I will change the password hashing of vBulletin to use BCrypt, a much better algorithm. The password hashing will look like this at the end of this guide:

password_hash(md5(md5($password) . $salt), PASSWORD_BCRYPT, array('salt' => $salt))

 
Requirements for this guide:
– Access to the FTP or permission to edit .php files on the server.
– PHP version of 5.5+ (we make use of the password_hash function)
– You must update all of the passwords in the current database.
– vBulletin 4.2.2 (this modification has been made on 4.2.2)
– Database access in the form of SSH/PHPMyAdmin or any other database manager tool

Create a backup of the files we are going to edit first so you can always restore it in case it goes wrong!

 
Step 1: modify password column type

First things first, we have to modify the type of the password column in the user table.
You can either do this in PHPMyAdmin by changing it to char(60) or by executing the following query:

ALTER TABLE user MODIFY password char(60);

 
Step 2: edit /includes/functions_login.php

Now let’s modify the verify_authentication function in the /includes/functions_login.php file.
Look for:

		if (
			$vbulletin->userinfo['password'] != iif($password AND !$md5password, md5(md5($password) . $vbulletin->userinfo['salt']), '') AND
			$vbulletin->userinfo['password'] != iif($md5password, md5($md5password . $vbulletin->userinfo['salt']), '') AND
			$vbulletin->userinfo['password'] != iif($md5password_utf, md5($md5password_utf . $vbulletin->userinfo['salt']), '')
		)

And replace it with:

		$hash1 = password_hash(md5(md5($password) . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));
		$hash2 = password_hash(md5($md5password . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));
		$hash3 = password_hash(md5($md5password_utf . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));

		if (
			$vbulletin->userinfo['password'] != iif($password AND !$md5password, $hash1, '') AND
			$vbulletin->userinfo['password'] != iif($md5password, $hash2, '') AND
			$vbulletin->userinfo['password'] != iif($md5password_utf, $hash3, '')
		)

This function will, as the function name describes, verify the authentication.

 
Step 3: edit /includes/class_dm_user.php

This file contains the verify_password and hash_password function which must be changed as well.
Look for the hash_password function:

	function hash_password($password, $salt)
	{
		// if the password is not already an md5, md5 it now
		if ($password == '')
		{
		}
		else if (!$this->verify_md5($password))
		{
			$password = md5($password);
		}

		// hash the md5'd password with the salt
		return md5($password . $salt);
	}

And replace it with:

	function hash_password($password, $salt)
	{
		// if the password is not already an md5, md5 it now
		if ($password == '')
		{
		}
		else if (!$this->verify_md5($password))
		{
			$password = md5($password);
		}

		// hash the md5'd password with the salt
		return password_hash(md5($password . $salt), PASSWORD_BCRYPT, array('salt' => $salt));
	}

Now look for the verify_password function:

	function verify_password(&$password)
	{
		//regenerate the salt when the password is changed.  No reason not to and its
		//an easy way to increase the size when the user changes their password (doing
		//it this way avoids having to reset all of the passwords)
		$this->user['salt'] = $salt = $this->fetch_user_salt();

		// generate the password
		$password = $this->hash_password($password, $salt);

		if (!defined('ALLOW_SAME_USERNAME_PASSWORD'))
		{
			// check if password is same as username; if so, set an error and return false
			if ($password == md5(md5($this->fetch_field('username')) . $salt))
			{
				$this->error('sameusernamepass');
				return false;
			}
		}

		$this->set('passworddate', 'FROM_UNIXTIME(' . TIMENOW . ')', false);

		return true;
	}

And replace it with:

	function verify_password(&$password)
	{
		//regenerate the salt when the password is changed.  No reason not to and its
		//an easy way to increase the size when the user changes their password (doing
		//it this way avoids having to reset all of the passwords)
		$this->user['salt'] = $salt = $this->fetch_user_salt();

		// generate the password
		$password = $this->hash_password($password, $salt);

		if (!defined('ALLOW_SAME_USERNAME_PASSWORD'))
		{
			// check if password is same as username; if so, set an error and return false
			if ($password == password_hash(md5(md5($this->fetch_field('username')) . $salt), PASSWORD_BCRYPT, array('salt' => $salt)))
			{
				$this->error('sameusernamepass');
				return false;
			}
		}

		$this->set('passworddate', 'FROM_UNIXTIME(' . TIMENOW . ')', false);

		return true;
	}

We have to modify this function since there’s a check to see if the password matches the current username, which is not allowed by default.

 
Step 4: update all passwords in the user table

The only downside of this is that we have to update all passwords in the database.
But no worries, no one will have to change or update their password to make this work since we use the old password hashing in BCrypt.

Please make a backup of your database or user table before doing this so you can restore it in case it does not work as intended!

Create a file in the root of your forum and name it something like update_passwords.php. Contents of the file:

require("./global.php");
$query = $db->query_read("SELECT userid, password, salt FROM user WHERE password NOT LIKE '$%' ORDER BY userid ASC");

echo 'We have ' . $db->num_rows($query) . ' password(s) to update..

'; while($row = $db->fetch_array($query)){ $db->query_write("UPDATE user SET password = '" . password_hash($row['password'], PASSWORD_BCRYPT, array('salt' => $row['salt'])) . "' WHERE userid = '" . $row['userid'] . "'"); } echo 'Updated all passwords, re-run this script to be sure!';

This will update all of the current password hashes to use BCrypt.
In case the script timed out, just run it again or change the script execution timeout.

 
Step 5: verifying functionality

Now try to login onto your forum and see if that works, in case you get an incorrect username/password error, it means you did something wrong in this guide.
In case it works, try to update your password and relog to see if that part works as well.

I tested the login, registration, remember me checkbox and changing password function on a local test forum which seemed to work fine.

Let me know if you have any questions and I’ll gladly answer them.