TalkPHP
 
 
Account Login
Latest Articles
» The basic usage of PHPTAL, a XML/XHTML template library for PHP
» Vulnerable methods and the areas they are commonly trusted in.
» Simple way to protect a form from bot
» The Basics On: How Session Stealing Works
» How to keep your forms from double posting data
IRC Channel
IRC Speech Bubble Join the friendly bunch on IRC...
(#TalkPHP on Freenode)

...Also available via a web interface.

See this thread for information on the TalkPHP Free Hugs Initiative™. Subject to availability.
Associates
Associates
CSS Tutorials
Reply
 
LinkBack (18) Thread Tools Search this Thread Display Modes
Old 11-20-2007, 03:09 PM   18 links from elsewhere to this Post. Click to view. #1 (permalink)
La Vida es Sueño
Advanced Programmer Top Contributor 
 
Wildhoney's Avatar
 
Join Date: Sep 2007
Location: Oldham
Posts: 2,210
Thanks: 90
Wildhoney is on a distinguished road
Part 2: Giving our Currency Conversion Script some Responsibility

Since the first article I submitted to Digg was voted onto the front page a couple of days ago, we're going to add onto the script and make it update the conversion rates from a website that keeps track of them all. This is quite a jump from the first tutorial and so buckle your seatbelt, Dorothy, 'cause Kansas... OK, 'nuff of the Matrix quotes. Let's begin!

If you haven't read part 1 then you'll be well advised to give it a read through. I'm not going to re-explain all the code from the first part because that would be out-right silly.

First we are going to turn our script into a class, we also have to think about memory here - something I'm sure many PHP programmers don't give a damn about. I was considering using singletons but I haven't for 2 obvious reason:
  1. What if we wanted to modify the source and target currencies?
  2. It's a bit advanced for this tutorial. Read guide on singletons.

As the first tutorial was aimed for the neophytes of the community, I'll also be keeping this tutorial simple. If I lose you at any point, please don't hesitate to ask for clarification.

First we are going to define our class:

php Code:
class TalkPHP_CurrencyConverter
{

}

Simple enough, but she's looking rather empty. Let's fill her up to the brink with various members - variables and constants. First the variables we are going to be using:

php Code:
private $m_fSourceRate;
private $m_fSourceCurSymbol;
private $m_fSourceIntlSymbol;

private $m_fTargetRate;
private $m_fTargetCurSymbol;
private $m_fTargetIntlSymbol;

...And then we'll want a couple of class constants to be used and shared. The constant OPT_FILE will be the location where our file will be kept, whereas OPT_URL is the XML resource containing all the conversion rates based against the Euro.

php Code:
const OPT_FILE = 'talkphp_conversion_rates.json';
const OPT_URL = 'http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml';

If you are a little unsure about class constants then please refer yourself to this guide on PHP constants. I hope I'm not going too fast, too quickly.

Next we are going to create our construct. A construct function is the function which is called when the object class is instantiated. In PHP4 this function was simply called the same as the class, and thus if we had our class called TalkPHP_CurrencyConverter then our construct function name would be TalkPHP_CurrencyConverter(). However, because we're lucky enough to have the immense power of PHP5, our function is called __construct():

php Code:
public function __construct($szCurrencyFrom = '', $szCurrencyTo = '')
{

}

We're giving the 2 arguments, $szCurrencyFrom and $szCurrencyTo so that we can call another function, updateRates(), which we will get to later on, if we do not wish to convert anything. Our next code will check for the variables being empty, and if they are, call the update function which we will write later on:

php Code:
if(empty($szCurrencyFrom) || empty($szCurrencyTo))
{
    $this->updateRates();
    return;
}

We can then procure the details of the 2 currencies - the source and the target. From those we need the international currency symbol and the locale currency symbol. Although we don't technically require the currency symbol for our source currency, it's good practice to return it nevertheless just in case you wish to extend onto the class at a later date.

php Code:
setlocale(LC_MONETARY, $szCurrencyFrom);
$aSource = localeconv();

$this->m_fSourceCurSymbol = $aSource['currency_symbol'];
$this->m_fSourceIntlSymbol = $aSource['int_curr_symbol'];

setlocale(LC_MONETARY, $szCurrencyTo);
$aTarget = localeconv();

$this->m_fTargetCurSymbol = $aTarget['currency_symbol'];
$this->m_fTargetIntlSymbol = $aTarget['int_curr_symbol'];

OK, long list, long list! It may seem a little bit harsh to throw so much code out at you, but it's really just code repetition, we set the locale and return the 2 symbols for our source currency, and then we repeat the process for the second currency. I won't go into how this works because I covered that in the first part.

Last but not least we'll want to obtain the rates from our cache file. We're using PHP's list construct. This essentially works by taking the first offset of the array and assigning it to the left most variable in our list construct.

php Code:
list($this->m_fSourceRate, $this->m_fTargetRate) = $this->getRates();

It may be wise now to crack open the getRates function to have a peak. We'll be using exceptions extensively from now on, this will allow us to return various messages from our class to the front-end, thus we can avoid simply returning an equivocal boolean.

php Code:
private function getRates()
{

}

We'll be setting a scope of private for this function because we are calling it from inside our class construct. We will first check to see if the cache file currency exists, if it doesn't then we'll call the update function to create it for us:

php Code:
if(!file_exists(self::OPT_FILE))
{
    $this->updateRates();
}

Nothing overly complicated about that code, so we'll move swiftly onto the next snippet. Enjoying yourself yet?

php Code:
$szContent = file_get_contents(self::OPT_FILE);
$aContent = json_decode($szContent);

"Woah!" you say? Yes, we'll be using JSON because I'm quite fond of its storage technique. If you're a little unfamiliar with JSON then by all means please read our article on JSON to enlighten yourself.

Fundamentally speaking, however, what we are doing is reading the contents of our JSON cache file and then decoding it using PHP5's native json_decode function. Next we need to ensure we've got back the correct data to continue on with the script, if not then we're throwing an exception to really put our foot down!

php Code:
if(!isset($aContent->{$this->m_fSourceIntlSymbol}))
{
    throw new Exception('Invalid source currency supplied.');
}

if(!isset($aContent->{$this->m_fTargetIntlSymbol}))
{
    throw new Exception('Invalid target currency supplied.');
}

If you've read meticulously over the JSON article then you'll realise, unless you explicitly specify otherwise, the json_decode function will return the data back as an object - I suppose you could draw some parallels between storage objects and arrays, though if were to be pedantic, they are quite dissimilar. We use the arrow (->) to access members of the object which relate to the items in our file which were JSON encoded. Thus:

php Code:
return array($aContent->{$this->m_fSourceIntlSymbol}, $aContent->{$this->m_fTargetIntlSymbol});

We're using the curly braces as we're saying we want to resolve the value of the variable and use that, instead of using the variable name literally. However, PHP would cause an error anyway because we've left the dollar sign in there which results in an incorrect syntax.

Now you can see why we're using PHP's list construct to crack open the array and assign it to 2 different variables - because we're returning an array with the 2 conversion rates in it that we need.

Before we jump into the toCurrency function, which if you've used Zend Framework before which comes highly recommended from me, then you'll realise I've temporarily borrowed the function's name, we'll create our update function which uses the SimpleXML class - now native to PHP since PHP5, to parse the XML document and return the conversion rates. We're getting perhaps a little advanced here but I've got faith in your abilities!

Again we start with an empty function with a private scope:

php Code:
private function updateRates()
{

}

...And again we're going to fill it with juicy code. As we're obtaining data from an unreliable source - a source which we have no control over, we'll be using exceptions to ensure it has returned from the server:

php Code:
$szContent = file_get_contents(self::OPT_URL);

if(!$szContent)
{
    throw new Exception('XML resource unavailable.');
}

Not a lot new here. We'll then want to create a new instance of SimpleXML and create an empty array which we'll use to store the items from the XML document:

php Code:
$pXML = new SimpleXMLElement($szContent);
$aRates = array();

Now, as you may be aware, XML has come on in leaps and bounds over the past couple of years. The predominant reason for XML's rise to power is due to the requirement of needing some standardisation in data that is passed from one place to another. XML, like HTML, but more closely related to XHTML, has nodes that may have attributes, even namespaces, and inevitably, data inside the nodes. If our XML document looked something like so:

xml Code:
<cars>
    <car maker="Ferrari">Enzo</car>
    <car maker="Lamborghini">LP640</car>
</cars>

We would be able to access the data by traversing 2 nodes, namely, cars and car. In PHP object style this would be:

php Code:
echo $pXML->cars->car[0];

Which would in turn echo out our first car, the Enzo. And so, once we have initiated a new SimpleXML instance, we can begin to parse the XML document to return the items we require for our script to work perfectly.

php Code:
foreach($pXML->Cube->Cube->Cube as $pChildren)
{
    $aRates[(string) $pChildren['currency']] = (float) $pChildren['rate'];
}

This will return every item as: Currency Symbol => Conversion Rate against Euro. All that's left to do now for our update function is to encode its contents using json_encode and then save it into its designated file. I'm sure the fwrite process is of no mystery to yourself, but for argument's sake I present you:

php Code:
$pFile = fopen(self::OPT_FILE, 'w+');
fwrite($pFile, json_encode($aRates));
fclose($pFile);

Let us track back to where we were. If you remember, we'd just about finished our class to convert to a currency, but we just needed the function to perform the last calculations and to return it to use with the currency symbol automatically prepended. I may as well throw this function at you in 1 go because in all honesty, everything declared and on the table, there's not a lot to it.

php Code:
public function toCurrency($fAmount)
{
    $fTotal = ($fAmount / $this->m_fSourceRate) * $this->m_fTargetRate;
           
    return $this->m_fTargetCurSymbol . number_format($fTotal, 2, '.', ',');
}

So, was ist das? Pardon me, German never was my most favoured second language. Furthermore, mathematics was never my most favoured subject and so excuse me while I get Karl to type up an explanation. This calculation converts a given amount (in any currency), divides that amount by the base rate for that currency (for example, the EUR to USD base rate) and then multiplies it by the target rate (for example EUR to GBP base rate).

Once we've done all the final calculations we return the total back to the front-end with the currency's symbol prepended. And because exceptions are prevalent in our script, we use the try and catch block to try the script and catch any exceptions:

php Code:
try
{
    $pCurrency = new TalkPHP_CurrencyConverter(CURRENCY_GBP, CURRENCY_USD);
    echo $pCurrency->toCurrency(50);
}
catch(Exception $pEx)
{
    echo $pEx->getMessage();
}

Based on today's conversion rates, 50 GBP into USD comes to $103.48. And not one defined constant which requires us to change everyday! Naturally I would have done this in the first part of the guide, but I didn't want you to get too far ahead of yourself. People learn a step at a time!

If we want to update the conversion rate cache file then we merely call the class, like so:

php Code:
new TalkPHP_CurrencyConverter();

To sum this guide up, because conversion rates are fluctuating constantly throughout the day as billion dollar deals are sealed, and as managing directors pay for their McWhoppas (or whatever they're called), McDonalds accumulate more money. So what better way than to give our script the awe-inducing ability to monitor and update its own conversion rates? That way you can code it and then let it fly, fly, fly, all on its own. No hands, no hands!
Attached Files
File Type: php TalkPHP_CurrencyConverter.php (2.7 KB, 1204 views)
__________________
The man who comes back through the Door in the Wall will never be quite the same as the man who went out.
Send a message via AIM to Wildhoney Send a message via MSN to Wildhoney Send a message via Yahoo to Wildhoney
Wildhoney is offline  
Reply With Quote
The Following 4 Users Say Thank You to Wildhoney For This Useful Post:
Alan @ CIT (01-15-2008), deflated (12-29-2007), rojer_31 (03-15-2009), sarmenhb (10-10-2008)
Old 11-21-2007, 12:34 PM   #2 (permalink)
The Acquainted
 
Join Date: Oct 2007
Posts: 172
Thanks: 18
maZtah is an unknown quantity at this point
Default

Great read, again. Great tutorial, again!

When you read through this tutorial everything seems to be that simple, but you know it is not. Though, I've learned alot again. Thank you for sharing, sir!
maZtah is offline  
Reply With Quote
Old 11-21-2007, 12:51 PM   #3 (permalink)
Super Moderator
Advanced Programmer 
 
bluesaga's Avatar
 
Join Date: Sep 2007
Posts: 165
Thanks: 0
bluesaga is on a distinguished road
Default

Very nice loosely coupled class and awesome tutorial! Nice one
__________________
Halo 3 Cheats
bluesaga is offline  
Reply With Quote
Old 12-29-2007, 06:08 AM   #4 (permalink)
The Wanderer
 
deflated's Avatar
 
Join Date: Dec 2007
Location: 127.0.0.1
Posts: 18
Thanks: 7
deflated is on a distinguished road
Default

Thank you very much! That class is quite useful!
deflated is offline  
Reply With Quote
Old 01-11-2008, 01:12 AM   #5 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default

!!WARNING!!

The "locale" always depends on the server configuration.
and if not installed this script will just fail !
worth defining the all of the CurSymbols and IntlSymbols in your definitions at the top and then if the locale is not set it catches the problem - and with just four currencies is easy enough to do
soup is offline  
Reply With Quote
Old 01-11-2008, 01:33 AM   #6 (permalink)
La Vida es Sueño
Advanced Programmer Top Contributor 
 
Wildhoney's Avatar
 
Join Date: Sep 2007
Location: Oldham
Posts: 2,210
Thanks: 90
Wildhoney is on a distinguished road
Default

Thanks for pointing this out, soup. You seem pretty intent on getting your message across on this ! Welcome to the community nonetheless.
__________________
The man who comes back through the Door in the Wall will never be quite the same as the man who went out.
Send a message via AIM to Wildhoney Send a message via MSN to Wildhoney Send a message via Yahoo to Wildhoney
Wildhoney is offline  
Reply With Quote
Old 01-11-2008, 01:37 AM   #7 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default

lol sorry about that - i'd just spent 10 minutes banging my head against the wall trying to figure out why it didnt work !
soup is offline  
Reply With Quote
Old 01-11-2008, 01:48 AM   #8 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default

er......there is another thing to

the rates you get from the ECB do not include the euro rate as this is always counted as one so you really need if converting from / to euros to define that EUR will always be 1
soup is offline  
Reply With Quote
Old 01-11-2008, 02:15 AM   #9 (permalink)
La Vida es Sueño
Advanced Programmer Top Contributor 
 
Wildhoney's Avatar
 
Join Date: Sep 2007
Location: Oldham
Posts: 2,210
Thanks: 90
Wildhoney is on a distinguished road
Default

All the currencies on ECB are against the Euro and therefore the Euro is assumed to be 1. That's how currency converters work. You set a currency to the default of 1 and calculate the differences for the mulitude of currencies. In which case on ECB, the Euro is the base currency.

Rant all you like about it ! It's a while since I did this so you have me thinking!
__________________
The man who comes back through the Door in the Wall will never be quite the same as the man who went out.
Send a message via AIM to Wildhoney Send a message via MSN to Wildhoney Send a message via Yahoo to Wildhoney
Wildhoney is offline  
Reply With Quote
Old 01-11-2008, 09:38 AM   #10 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default

wasn't meant as a rant

And i just needed a converter now so this was perfect and many thanks

just i tried to convert euro to canadian and promptly got a Division by Zero error thats all.

I have just changed getRates to this

php Code:
private function getRates()
    {
    if(!file_exists(self::OPT_FILE))
    {
    $this->updateRates();
    }

    $szContent = file_get_contents(self::OPT_FILE);
    $aContent = json_decode($szContent);

    if(!isset($aContent->{$this->m_fSourceIntlSymbol}))
            {
    if($this->m_fSourceIntlSymbol == 'EUR') {
        $source = 1;
                } else {
     throw new Exception('Invalid source currency supplied.');
        }
            } else {
    $source =   $aContent->{$this->m_fSourceIntlSymbol};
            }

    if(!isset($aContent->{$this->m_fTargetIntlSymbol}))
            {
    if($this->m_fTargetIntlSymbol == 'EUR') {
            $target = 1;
                } else {
     throw new Exception('Invalid target currency supplied.');
                }
            } else {
        $target =   $aContent->{$this->m_fTargetIntlSymbol};
            }

            return array($source, $target);
   
        }

Last edited by Wildhoney : 01-11-2008 at 01:13 PM.
soup is offline  
Reply With Quote
Old 01-11-2008, 01:14 PM   #11 (permalink)
La Vida es Sueño
Advanced Programmer Top Contributor 
 
Wildhoney's Avatar
 
Join Date: Sep 2007
Location: Oldham
Posts: 2,210
Thanks: 90
Wildhoney is on a distinguished road
Default

Excellent It looks good. Thanks for the update!
__________________
The man who comes back through the Door in the Wall will never be quite the same as the man who went out.
Send a message via AIM to Wildhoney Send a message via MSN to Wildhoney Send a message via Yahoo to Wildhoney
Wildhoney is offline  
Reply With Quote
Old 01-15-2008, 12:31 PM   #12 (permalink)
Alan @ CIT
Member of the Month
The Frequenter
Member of the Month Top Contributor 
 
Alan @ CIT's Avatar
 
Join Date: Apr 2005
Location: South UK
Posts: 483
Thanks: 51
Alan @ CIT is on a distinguished road
Default

Thanks Wildhoney, very handy class - stolen for future use

Alan
Send a message via MSN to Alan @ CIT
Alan @ CIT is offline  
Reply With Quote
Old 01-16-2008, 09:24 AM   #13 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default A couple more small changes

Cos I'm actually using this
I have changed another couple of things that may be of interest.

In my construct is now
# make sure we have the latest rates
$this->checkRatesActual();

which looks like this

private function checkRatesActual() {
$filename= 'conversion_rates.json';
if (file_exists($filename)) {
date_default_timezone_set("America/Los_Angeles");
setlocale(LC_TIME, "de_DE");
$nTimestamp = filectime($filename);
$dtime = strftime("%d %b %Y", $nTimestamp);
$str = "now";
$timestamp = strtotime($str);
$now = strftime("%d %b %Y", $timestamp);
if($now > $dtime) {
$this->updateRates();
}
}
}

all this does is check that the date of the file is todays date, otherwise update our rates - yes i know i must add something for the weekends !

I have also added this function

public function str2int($string, $concat = true) {
$value = split('[/.-]', $string);
$string = $value[0].$value[1];
$length = strlen($string);
for ($i = 0, $int = '', $concat_flag = true; $i < $length; $i++) {
if (is_numeric($string[$i]) && $concat_flag) {
$int .= $string[$i];
} elseif(!$concat && $concat_flag && strlen($int) > 0) { $concat_flag = false;
}
}
return (int) $int;
}

which i now call in toCurrency - just to make sure that whatever is passed is usuable

public function toCurrency($fAmount)
{
if(!is_numeric($fAmount)) {
$fAmount = $this->str2int($fAmount);
}

Oh and at the bottem of update rates i have included a touch because on the server i am on the date of the file was not being updated.
eg
$pFile = fopen(self::OPT_FILE, 'w+');
fwrite($pFile, json_encode($aRates));
fclose($pFile);
touch($pFile);

hope that helps someone
soup is offline  
Reply With Quote
Old 10-09-2008, 02:58 PM   #14 (permalink)
The Visitor
 
Join Date: Oct 2008
Posts: 2
Thanks: 0
nativebreed is on a distinguished road
Default

Getting cross-browser AJAX request working for our currency convertion service was the biggest challenge I had. After Googling everything under the sun, I ended up adopting the Yahoo webservices way of doing things - use JSON.

Have a look and let me know what you think (Convert, buy, transfer and compare currency (money) exchange rates.). We wanted to create a simple converter for webmasters to use on their website, and I think we might have cracked it.
nativebreed is offline  
Reply With Quote
Old 03-15-2009, 12:09 PM   #15 (permalink)
The Visitor
 
Join Date: Mar 2009
Posts: 1
Thanks: 1
rojer_31 is on a distinguished road
Default One question

Quote:
Originally Posted by soup View Post
!!WARNING!!

The "locale" always depends on the server configuration.
and if not installed this script will just fail !
worth defining the all of the CurSymbols and IntlSymbols in your definitions at the top and then if the locale is not set it catches the problem - and with just four currencies is easy enough to do
Can you give an example of the code required incase the locale isn't setup properly on the server? It seems to be incorrectly setup in our server.
Can anyone give the change of code needed to make this class work without depending on the 'locale' setting? Atleast for the sample set of currencies listed here..

Great tutorial btw :)
rojer_31 is offline  
Reply With Quote
Old 03-17-2009, 02:53 PM   #16 (permalink)
The Wanderer
 
Join Date: Jan 2008
Posts: 7
Thanks: 0
soup is on a distinguished road
Default

put this lot outside of the class for whatever currencies your using

global $G_cur;
$G_cur['CURRENCY'] = array(
'CURRENCY_CAD' => array(
'SourceCurSymbol' => 'C$',
'SourceIntlSymbol' => 'CAD'
),
'CURRENCY_EUR' => array(
'SourceCurSymbol' => '€',
'SourceIntlSymbol' => 'EUR'
),
'CURRENCY_USD' => array(
'SourceCurSymbol' => '$',
'SourceIntlSymbol' => 'USD'
),
'CURRENCY_GBP' => array(
'SourceCurSymbol' => 'PD',
'SourceIntlSymbol' => 'GBP'
),
);


and then later on in your class

in the construct declare global $G_cur;

and then can be called whereever in the class

$this->m_fSourceCurSymbol = $G_cur['CURRENCY'][$szCurrencyFrom]['SourceCurSymbol'];
$this->m_fSourceIntlSymbol = $G_cur['CURRENCY'][$szCurrencyFrom]['SourceIntlSymbol'];
$this->m_fTargetCurSymbol = $G_cur['CURRENCY'][$szCurrencyTo]['SourceCurSymbol'];
$this->m_fTargetIntlSymbol = $G_cur['CURRENCY'][$szCurrencyTo]['SourceIntlSymbol'];

hope that sorts it out

regards
soup is offline  
Reply With Quote
Reply


LinkBacks (?)
LinkBack to this Thread: http://www.talkphp.com/general/1503-part-2-giving-our-currency-conversion-script-some-responsibility.html
Posted By For Type Date
Cowburn - Online storage depot for Peter Cowburn This thread Refback 01-13-2008 05:43 PM
Digg / Programming This thread Refback 01-13-2008 10:36 AM
Linux Knowledge Base and Tutorial - PHP: Part 2 - Currency Converter with Automatic Conversion Rates via XML This thread Refback 01-11-2008 11:13 PM
I am stuck: Error on a line that begins with the word This thread Refback 01-07-2008 07:44 AM
PHP: Creating a Currency Converter with Live Exchange Rates at 9rules Programming Community This thread Refback 01-06-2008 01:38 AM
Digg / All News, Videos, & Images This thread Refback 12-29-2007 10:52 PM
Digg / Technology This thread Refback 12-29-2007 08:39 PM
Digg / All News, Videos, & Images This thread Refback 12-29-2007 08:30 PM
Digg / All News, Videos, & Images This thread Refback 12-29-2007 03:17 PM
Digg / Technology This thread Refback 12-29-2007 08:17 AM
Digg / All News, Videos, & Images This thread Refback 12-29-2007 07:57 AM
Digg / Programming This thread Refback 12-29-2007 06:06 AM
Digg / Programming This thread Refback 12-28-2007 01:28 PM
Digg / Programming This thread Refback 12-27-2007 08:30 PM
Digg / Technology This thread Refback 12-26-2007 04:47 PM
Digg - PHP: Part 2 - Currency Converter with Automatic Conversion Rates via XML This thread Refback 12-23-2007 01:31 AM
Digg / Programming This thread Refback 12-22-2007 01:59 AM
Digg / Programming This thread Refback 12-21-2007 11:05 PM

Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools Search this Thread
Search this Thread:

Advanced Search
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Trackbacks are On
Pingbacks are On
Refbacks are On


All times are GMT. The time now is 01:08 PM.

 
     

Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2010, Jelsoft Enterprises Ltd.
Search Engine Optimization by vBSEO 3.1.0
Inactive Reminders By Icora Web Design