Page 1 of 1

Client Environment data fetcher

Posted: Wed Apr 11, 2007 7:14 pm
by RobertGonzalez
I have seen requests recently about getting a clients OS or browsers. I posted some code a few days ago in response to someone's request for this type of code and decided to put together a little bit cleaner of class to use for this. I borrowed some regular expression code and browser/os detection code from phpSniff to make this, so this is not all mine.

Please critique away.

Code: Select all

<?php
/**
 * Padlock - The PHP Application Developers Library and Opensource Code Kit
 *
 * @author Robert Gonzalez <robert@everah.com>
 * @package Padlock
 * @copyright None
 * @license Creative Commons Attribution License 3.0 {@link http://creativecommons.org/licenses/by/3.0/}
 */

/**
 * Padlock_Client_Profile is used to gather information about the client.
 *
 * This class gathers information about the client, such as the users browser,
 * operating system and IP. Much of this information is broken down cleanly
 * into smaller bits that can be used to build larger bits. A lot of it can be
 * used for analysis as well.
 *
 * The two most significant parts of this class code were derived from phpSniff
 * developed by Roger Raymond <epsilon7@users.sourceforge.net>. Those parts are
 * the regular expression usage for snagging the users browser and operating
 * system. The browser array was also borrowed from phpSniff.
 *
 * ----------------------------------------------------------------------------
 *
 * Usage:
 * <code>
 * The easiest way to use the class is to instantiate it:
 *      $client = new Padlock_Client_Profile();
 *      $client_details = $client->get_details();
 * This will give you all of the details set by the class that were
 * fetched from the client.
 *
 * To get a brief output of the clients browser and OS you can use the display() method:
 *      $client->display('<p>', '</p>', ' ');
 * This will echo out a single line of text along the lines of:
 *      Browser: Mozilla Firefox 2.0.0.3 Operating System: Windows XP
 *
 * The display() method takes three params: opentag, closetag, separator
 * </code>
 * @version 0.1
 * @package Padlock_Client_Profile
 * @copyright None
 * @license Creative Commons Attribution License 3.0 {@link http://creativecommons.org/licenses/by/3.0/}
 * @author Robert Gonzalez <robert@everah.com>
 */ 
class Padlock_Client_Profile
{
    /**
     * Array to hold all user profile details
     *
     * @var array
     * @access private
     */
    private $details = array();
    
    /**
     * Array of browsers 
     * 
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @var array
     */
    private $browser_list = array(
        'microsoft internet explorer'   => 'IE',
        'msie'                          => 'IE',
        'netscape6'                     => 'NS',
        'netscape'                      => 'NS',
        'galeon'                        => 'GA',
        'phoenix'                       => 'PX',
        'mozilla firebird'              => 'FB',
        'firebird'                      => 'FB',
        'firefox'                       => 'FX',
        'chimera'                       => 'CH',
        'camino'                        => 'CA',
        'epiphany'                      => 'EP',
        'safari'                        => 'SF',
        'k-meleon'                      => 'KM',
        'mozilla'                       => 'MZ',
        'opera'                         => 'OP',
        'konqueror'                     => 'KQ',
        'icab'                          => 'IC',
        'lynx'                          => 'LX',
        'links'                         => 'LI',
        'ncsa mosaic'                   => 'MO',
        'amaya'                         => 'AM',
        'omniweb'                       => 'OW',
        'hotjava'                       => 'HJ',
        'browsex'                       => 'BX',
        'amigavoyager'                  => 'AV',
        'amiga-aweb'                    => 'AW',
        'ibrowse'                       => 'IB' 
    );

    /**
     * Class constructor
     *
     * @access public
     * @param boolean $echo True displays on construction
     */
    public function __construct($echo = false)
    {
        // Fetch boy, fetch. Good dog. Sit Ubu.
        $this->initialize();
        
        // Should we display immediately?
        if ($echo)
        {
            $this->display();
        }
    }
    
    /**
     * Initialze class data
     *
     * @access private
     */
    private function initialize()
    {
        $this->set_useragent();
        $this->set_os();
        $this->set_browser();
        $this->set_ip();
    }
    
    /**
     * Output what we know in a clean little outputter
     *
     * @access public
     * @param string $opentag The HTML tag to use to open the string
     * @param string $closetag The HTML tag to use to close the string
     * @param string $separator The HTML tag to use to separate the browser from OS in the string
     * @return void Outputs a report of the browser stats or an error message if there is a problem
     */
    public function display($opentag = '', $closetag = '', $separator = ' ')
    {
        if (isset($this->details['useragent']))
        {
            echo $opentag . 'Browser: ' . $this->get_browser() . $separator . 'Operating System: ' . $this->get_platform() . $closetag;
        }
        else
        {
            // This is literally the only place there would be an error to report
            echo 'We were unable to detect your browser and operating system';
        }
    }
    
    /**
     * Adds an entry into the details array
     * 
     * @access private
     * @param string $token The name of the item we are adding
     * @param string $value The value of the item we are adding
     */
    private function add_detail($token, $value)
    {
        if (!isset($this->details[$token]))
        {
            $this->details[$token] = $value;
        }
    }
    
    /**
     * Sets the users complete user agent string
     *
     * @access private
     * @return string The users user agent on success, boolean false on failure
     */
    private function set_useragent() 
    {
        $useragent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : getenv('HTTP_USER_AGENT');
        
        if ($useragent === false)
        {
            throw new Exception('The user agent could not be fetched.');
        }
        
        $this->add_detail('useragent', $useragent);
    }
    
    /**
     * Sets the IP property
     * 
     * @author Robert Gonzalez <robert@everah.com>
     * @access private
     */
    private function set_ip()
    {
        $this->add_detail('client_ip', $this->get_ip_address());
    }
    
    /**
     * Sets browser information
     * 
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @author Robert Gonzalez <robert@everah.com>
     * @access private
     * @return string browser name and version or false if unrecognized
     */
    private function set_browser()
    {
        $browser_regex = $this->get_browser_regex_string();
        $browser_name = 'Unknown';
        $browser_abbreviation = 'Unknown';
        $browser_major_version = 'Unknown';
        $browser_minor_version = 'Unknown';
        $browser_letter_version = null;
        $browser_full_version = 'Unknown';
        $browser_complete_name = 'Unknown';
        
        if (preg_match_all($browser_regex, $this->details['useragent'], $results))
        {
            // get the position of the last browser found
            $count = count($results[0])-1;
            
            // Browser name
            $browser_name = $results[1][$count];
            
            // Major version
            $browser_major_version = $results[2][$count];
            
            // insert findings into the container
            $browser_abbreviation = strtolower($results[1][$count]);
            
            // parse the minor version string and look for alpha chars
            preg_match('/([.\0-9]+)?([\.a-z0-9]+)?/i', $results[3][$count], $match);
            
            // Minor version handling
            $minor_version = isset($match[1]) ? $match[1] : '.0';
            
            // Letter version handling
            if (isset($match[2])) 
            {
                $browser_letter_version = $match[2];
            }
            
            // Create the necessary detail values
            $this->add_detail('browser_name', $browser_name);
            $this->add_detail('browser_major_version', $browser_major_version);
            $this->add_detail('browser_minor_version', $minor_version);
            $this->add_detail('browser_full_version', $this->details['browser_major_version'].$this->details['browser_minor_version']);
            $this->add_detail('browser_full_name', $this->details['browser_name']. ' ' . $this->details['browser_full_version']);
            $this->add_detail('browser_abbreviation', $this->browser_list[$browser_abbreviation]);
            
            // Handle the registration of the letter version
            if (!is_null($browser_letter_version))
            {
                $this->add_detail('browser_letter_version', $browser_letter_version);
            }
        }
    }
    
    /**
     * Sets operating system and platform details
     *
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @return string os name and version or false in unrecognized os
     */
    private function set_os()
    {
        // regexes to use
        $regex_windows  = '/([^dar]win[dows]*)[\s]?([0-9a-z]*)[\w\s]?([a-z0-9.]*)/i';
        $regex_mac      = '/(68[k0]{1,3})|(ppc mac os x)|([p\S]{1,5}pc)|(darwin)/i';
        $regex_os2      = '/os\/2|ibm-webexplorer/i';
        $regex_sunos    = '/(sun|i86)[os\s]*([0-9]*)/i';
        $regex_irix     = '/(irix)[\s]*([0-9]*)/i';
        $regex_hpux     = '/(hp-ux)[\s]*([0-9]*)/i';
        $regex_aix      = '/aix([0-9]*)/i';
        $regex_dec      = '/dec|osfl|alphaserver|ultrix|alphastation/i';
        $regex_vms      = '/vax|openvms/i';
        $regex_sco      = '/sco|unix_sv/i';
        $regex_linux    = '/x11|inux/i';
        $regex_bsd      = '/(free)?(bsd)/i';
        $regex_amiga    = '/amiga[os]?/i';
        
        $client_platform = 'Unknown';
        $client_os = 'Unknown';
        
        // look for Windows Box
        if (preg_match_all($regex_windows, $this->details['useragent'], $match))
        {
            /** Windows has some of the most ridiculous HTTP_USER_AGENT strings */
            //$match[1][count($match[0])-1];
            $v  = $match[2][count($match[0])-1];
            $v2 = $match[3][count($match[0])-1];
            // Establish NT 5.1 as Windows XP
            if (stristr($v,'NT') && $v2 == 5.1) 
            {
                $v = 'XP';
            }
            // Establish NT 5.0 and Windows 2000 as win2k
            elseif ($v == '2000') 
            {
                $v = '2000';
            }
            elseif (stristr($v, 'NT') && $v2 == 5.0) 
            {
                $v = '2000';
            }
            // Establish 9x 4.90 as Windows 98
            elseif (stristr($v, '9x') && $v2 == 4.9)
            {
                $v = '98';
            }
            // See if we're running windows 3.1
            elseif ($v.$v2 == '16bit') 
            {
                $v = '3.1';
            }
            // otherwise display as is (31,95,98,NT,ME,XP)
            else 
            {
                $v .= $v2;
            }
            
            // update browser info container array
            if (empty($v)) 
            {
                $v = 'Windows';
            }
            
            $client_os = $v;
            $client_platform = 'Windows';
        }
        //  look for amiga OS
        elseif (preg_match($regex_amiga, $this->details['useragent'], $match))
        {
            $client_platform = 'Amiga';
            
            if (stristr($this->details['useragent'], 'morphos')) 
            {
                // checking for MorphOS
                $client_os = 'MorphOS';
            } 
            elseif (stristr($this->details['useragent'], 'mc680x0')) 
            {
                // checking for MC680x0
                $client_os = 'MC680x0';
            } 
            elseif (stristr($this->details['useragent'], 'ppc')) 
            {
                // checking for PPC
                $client_os = 'PPC';
            } 
            elseif (preg_match('/(AmigaOS [\.1-9]?)/i', $this->details['useragent'], $match)) 
            {
                // checking for AmigaOS version string
                $client_os = $match[1];
            }
        }
        // look for OS2
        elseif (preg_match($regex_os2, $this->details['useragent'])) 
        {
            $client_os = 'OS2';
            $client_platform = 'OS2';
        }
        // look for mac
        // sets: platform = mac ; os = 68k or ppc
        elseif (preg_match($regex_mac, $this->details['useragent'], $match))
        {
            $client_platform = 'Mac';
            $os = !empty($match[1]) ? '68K' : '';
            $os = !empty($match[2]) ? 'OSX' : $os;
            $os = !empty($match[3]) ? 'PPC' : $os;
            $os = !empty($match[4]) ? 'OSX' : $os;
            $client_os = $os;
        }
        //  look for *nix boxes
        //  sunos sets: platform = *nix ; os = sun|sun4|sun5|suni86
        elseif (preg_match($regex_sunos, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            if (!stristr('sun', $match[1]))
            {
                $match[1] = 'Sun'.$match[1];
            }
            $client_os = $match[1].$match[2];
        }
        //  irix sets: platform = *nix ; os = irix|irix5|irix6|...
        elseif (preg_match($regex_irix, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = $match[1].$match[2];
        }
        //  hp-ux sets: platform = *nix ; os = hpux9|hpux10|...
        elseif (preg_match($regex_hpux, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $match[1] = str_replace('-', '', $match[1]);
            $match[2] = (int) $match[2];
            $client_os = $match[1].$match[2];
        }
        //  aix sets: platform = *nix ; os = aix|aix1|aix2|aix3|...
        elseif (preg_match($regex_aix, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'aix'.$match[1];
        }
        //  dec sets: platform = *nix ; os = dec
        elseif (preg_match($regex_dec, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'dec';
        }
        //  vms sets: platform = *nix ; os = vms
        elseif (preg_match($regex_vms, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'vms';
        }
        //  sco sets: platform = *nix ; os = sco
        elseif (preg_match($regex_sco, $this->details['useragent'], $match)) 
        {
            $client_platform = '*nix';
            $client_os = 'SCO';
        }
        //  unixware sets: platform = *nix ; os = unixware
        elseif (stristr($this->details['useragent'], 'unix_system_v'))
        {
            $client_platform = '*nix';
            $client_os = 'Unixware';
        }
        //  mpras sets: platform = *nix ; os = mpras
        elseif (stristr($this->details['useragent'], 'ncr'))
        {
            $client_platform = '*nix';
            $client_os = 'Mpras';
        }
        //  reliant sets: platform = *nix ; os = reliant
        elseif (stristr($this->details['useragent'], 'reliantunix'))
        {
            $client_platform  = '*nix';
            $client_os = 'Reliant';
        }
        //  sinix sets: platform = *nix ; os = sinix
        elseif (stristr($this->details['useragent'], 'sinix')) 
        {
            $client_platform = '*nix';
            $client_os = 'Sinix';
        }
        //  bsd sets: platform = *nix ; os = bsd|freebsd
        elseif (preg_match($regex_bsd, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = $match[1].$match[2];
        }
        //  last one to look for
        //  linux sets: platform = *nix ; os = linux
        elseif (preg_match($regex_linux, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'Linux';
        }
        
        $this->add_detail('client_platform', $client_platform);
        $this->add_detail('client_os', $client_os);
    }
    
    /**
     * This simply sets a regular expression string to use for browser comparison
     * 
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @return string A regular expression string
     */
    private function get_browser_regex_string()
    {
        $browsers = '';
        
        while (list($k,) = each($this->browser_list))
        {
            if (!empty($browsers)) 
            {
                $browsers .= '|';
            }
            
            $browsers .= $k;
        }
        
        $version_string = '[\/\sa-z(]*([0-9]+)([\.0-9a-z]+)?';
        return "/($browsers)$version_string/i";
    }
    
    /**
     * Get the users IP address
     * 
     * @access private
     * @return string IP Address of the user
     */
    private function get_ip_address()
    {
        return getenv('HTTP_CLIENT_IP') ? getenv('HTTP_CLIENT_IP') : getenv('REMOTE_ADDR');
    }
    
    /**
     * Gets the users browser information
     *
     * @access public
     * @return string The users browser
     */
    public function get_browser()
    {
        return $this->get_detail('browser_full_name');
    }
    
    /**
     * Gets the users complete user agent information
     *
     * @access public
     * @return string The users browser
     */
    public function get_useragent()
    {
        return $this->get_detail('useragent');
    }
    
    /**
     * Gets the users operating system
     *
     * @access public
     * @return string The users operating system
     */
    public function get_os()
    {
        return $this->get_detail('client_os');
    }
    
    /**
     * Gets the users operating system environment
     *
     * @access public
     * @return string The users platform and operating system
     */
    public function get_platform()
    {
        return $this->get_detail('client_platform') . ' ' . $this->get_detail('client_os');
    }
    
    /**
     * Gets the users IP Address
     *
     * @access public
     * @return string The users IP Address
     */
    public function get_ip()
    {
        return $this->get_detail('client_ip');
    }
    
    /**
     * Gets the detail requested
     *
     * @access public
     * @return string A detail entry if it exists, false otherwise
     */
    public function get_detail($detail)
    {
        if ($detail == 'all')
        {
            return $this->details;
        }
        else
        {
            return $this->has_detail($detail) ? $this->details[$detail] : 'Detail not found';
        }
    }

    /**
     * Gets all client details
     *
     * @access public
     * @return array An array of all information fetched from the client
     */
    public function get_details()
    {
        return $this->details;
    }
    
    /**
     * Checks whether a detail is available or not
     *
     * @access private
     * @return boolean True if the detail exists, false otherwise
     */
    private function has_detail($detail)
    {
        return isset($this->details[$detail]);
    }
}
?>
Example usage:

Code: Select all

<?php
// Include the class
require_once 'utility.clientprofile.php';

// Instantiate it
$client = new Padlock_Client_Profile();

var_dump($client->get_details());
/**
 * Will output something similar to:
 * array(10) {
 *   ["useragent"]=>
 *   string(90) "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3"
 *   ["client_platform"]=>
 *   string(7) "Windows"
 *   ["client_os"]=>
 *   string(2) "XP"
 *   ["browser_name"]=>
 *   string(7) "Firefox"
 *   ["browser_major_version"]=>
 *   string(1) "2"
 *   ["browser_minor_version"]=>
 *   string(6) ".0.0.3"
 *   ["browser_full_version"]=>
 *   string(7) "2.0.0.3"
 *   ["browser_full_name"]=>
 *   string(15) "Firefox 2.0.0.3"
 *   ["browser_abbreviation"]=>
 *   string(2) "FX"
 *   ["client_ip"]=>
 *   string(9) "127.0.0.1"
 * }
 */

echo 'The users IP is ' . $client->get_detail('client_ip');
// Or you could use $client->get_ip();
/**
 * Outputs something like:
 * The users IP is 127.0.0.1
 */

echo 'The client is on ' . $client->get_platform();
/**
 * Outputs something like:
 * The client is on Windows XP
 */

$client->display('<p>', '</p>', ' ');
/**
 * Outputs something like:
 * <p>Browser: Firefox 2.0.0.3 Operating System: Windows XP</p>
 */
?>

Posted: Sun Apr 15, 2007 4:26 am
by Ollie Saunders
Hi Everah,

Lets have a look at this then.

Code: Select all

// This is literally the only place there would be an error to report
echo 'We were unable to detect your browser and operating system';
Have you considered throwing an exception or trigger_error() instead here? Just echoing seems really inflexible because it can't be supressed or controlled in any way.

Also I'm wondering if you are overstepping the role of the object with the display() method. Single responibility principle would probably state that should be handled by another object. Consider where this might do harm: someone might want to extend the object to change the display, but equally they might have extended it is elsewhere to augment the detection functionality and run into issues if they then wanted to combine the two. I think this is setting a bad example for how the class might be extended or modified for different needs.

Where you have set_useragent(void) consider set_useragent(string $userAgent) and useRequesterUserAgent(void). You are limiting the detection to the value that come from $_SERVER or getenv() when someone could possibly be storing user agents in a database and wanting to extract information about them later - for statistics analysis for example.

In fact I find your use of 'set' a little confusing in general because these are not public setter functions at all.

Last but not least: no unit test?

Posted: Sun Apr 15, 2007 9:43 am
by RobertGonzalez
Finally, some feedback!

Thanks ole. Yes, I had actually put an exception in place of the echo'd error, but thought that someone that has no experience in developing, but wanted to use the snippet would actually benefit from not having to deal with exception. Though I think your point is stronger than mine.
ole wrote:Also I'm wondering if you are overstepping the role of the object with the display() method.... I think this is setting a bad example for how the class might be extended or modified for different needs.
I agree now that I look at it. I believe that is the only method that outputs from the class, so I would tend to agree with you that display could probably be better used outside of this "fetch" class.
ole wrote:Where you have set_useragent(void) consider set_useragent(string $userAgent) and useRequesterUserAgent(void). You are limiting the detection to the value that come from $_SERVER or getenv() when someone could possibly be storing user agents in a database and wanting to extract information about them later - for statistics analysis for example.

In fact I find your use of 'set' a little confusing in general because these are not public setter functions at all.
Do all setter type methods have to be public? I can see your first argument however. I like it and will more than likely add that methodology to the class. My thought when naming the methods was that, upon instantiation, the class fetches and sets internal values without any more interaction from the calling code. In essence, when the object is instantiated, all the work is already done. The calling code then access the data by get_*. Hence the public visibility of the get_* methods and private visibility of the set_* methods. But I do like your first point about taking a parameter, and I think it would be nice to have that functionality.
ole wrote:Last but not least: no unit test?
I am working on them. Believe it or not, I threw this thing together from some PHP4 code that I was using. I spent all of 30 minutes on it (mostly commenting) then tweaked it a little later on.

Thanks for the feedback. I will look at getting the suggestions implemented tomorrow.

Posted: Sun Apr 15, 2007 10:38 am
by Ollie Saunders
Do all setter type methods have to be public?
I suppose not.
My thought when naming the methods was that, upon instantiation, the class fetches and sets internal values without any more interaction from the calling code. In essence, when the object is instantiated, all the work is already done.
Well that method is a little limited. You could always have a static factory method. So..

Code: Select all

$prof = new Padlock_Client_Profile(); // empty state, pretty useless until...
$prof->initFromRequestHeader(); // or
$prof->initFromUserAgentString($dataFromWhereEver);
// at which point data can be retrieved
echo $prof->get_detail(...);

// And a shorthand for the most common usage
$prof = Padlock_Client_Profile::getCommonState();
// which does the instantiation and initFromRequestHeader() for you in one hit
I think initFromRequestHeader() is a bit clearer than my earlier useRequesterUserAgent().

Posted: Sun Apr 15, 2007 3:31 pm
by RobertGonzalez
I like it. Let me play around with this tomorrow. I have a tri-tip that needs BBQ'ing at the moment (mmmm, meat).

Posted: Mon Apr 16, 2007 6:27 pm
by RobertGonzalez
Ok, I rewrote some of the pieces per our discussion. I am not really fond of the idea of a class this small having a null state constructor, so I put the logic of assembling the details array into the constructor. However, whatever is used to create the original schema can be overridden with the parse_from_string() and parse_from_header() methods now. I also ditched the display() method and included a static method create_profile_schema() in the event the user does not want to actually instantiate the object.

Here it is now....

Code: Select all

<?php
/**
 * Padlock - The PHP Application Developers Library and Opensource Code Kit
 *
 * @author Robert Gonzalez <robert@everah.com>
 * @package Padlock
 * @copyright None
 * @license Creative Commons Attribution License 3.0 {@link http://creativecommons.org/licenses/by/3.0/}
 */

/**
 * Padlock_Client_Profile is used to gather information about the client.
 *
 * This class gathers information about the client, such as the users browser,
 * operating system and IP. Much of this information is broken down cleanly
 * into smaller bits that can be used to build larger bits. A lot of it can be
 * used for analysis as well.
 *
 * The two most significant parts of this class code were derived from phpSniff
 * developed by Roger Raymond <epsilon7@users.sourceforge.net>. Those parts are
 * the regular expression usage for snagging the users browser and operating
 * system. The browser array was also borrowed from phpSniff.
 *
 * ----------------------------------------------------------------------------
 *
 * Usage:
 * <code>
 * The easiest way to use the class is to instantiate it:
 *      $client = new Padlock_Client_Profile();
 *      $client_details = $client->get_details();
 * This will give you all of the details set by the class that were
 * fetched from the client.
 *
 * To get a brief output of the clients browser and OS you can use the display() method:
 *      $client->display('<p>', '</p>', ' ');
 * This will echo out a single line of text along the lines of:
 *      Browser: Mozilla Firefox 2.0.0.3 Operating System: Windows XP
 *
 * The display() method takes three params: opentag, closetag, separator
 * </code>
 * @version 0.1
 * @package Padlock_Client_Profile
 * @copyright None
 * @license Creative Commons Attribution License 3.0 {@link http://creativecommons.org/licenses/by/3.0/}
 * @author Robert Gonzalez <robert@everah.com>
 */
class Padlock_Client_Profile
{
    /**
     * Array to hold all user profile details
     *
     * @var array
     * @access private
     */
    private $details = array();
   
    /**
     * Array of browsers
     *
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @var array
     */
    private $browser_list = array(
        'microsoft internet explorer'   => 'IE',
        'msie'                          => 'IE',
        'netscape6'                     => 'NS',
        'netscape'                      => 'NS',
        'galeon'                        => 'GA',
        'phoenix'                       => 'PX',
        'mozilla firebird'              => 'FB',
        'firebird'                      => 'FB',
        'firefox'                       => 'FX',
        'chimera'                       => 'CH',
        'camino'                        => 'CA',
        'epiphany'                      => 'EP',
        'safari'                        => 'SF',
        'k-meleon'                      => 'KM',
        'mozilla'                       => 'MZ',
        'opera'                         => 'OP',
        'konqueror'                     => 'KQ',
        'icab'                          => 'IC',
        'lynx'                          => 'LX',
        'links'                         => 'LI',
        'ncsa mosaic'                   => 'MO',
        'amaya'                         => 'AM',
        'omniweb'                       => 'OW',
        'hotjava'                       => 'HJ',
        'browsex'                       => 'BX',
        'amigavoyager'                  => 'AV',
        'amiga-aweb'                    => 'AW',
        'ibrowse'                       => 'IB'
    );

    /**
     * Class constructor
     *
     * @access public
     * @param string $useragent A string representation of a useragent
     */
    public function __construct($useragent = '')
    {
        // Fetch boy, fetch. Good dog. Sit Ubu.
        $this->initialize($useragent);
    }
    
    /**
     * Static method to initialize the object without creating an object instance
     *
     * @access private
     * @param string $useragent A useragent string to parse for details     
     */
    public static function create_profile_schema($useragent = '')
    {
        $tobject = new Padlock_Client_Profile($useragent);
        return $tobject->get_details();
    }
    
    /**
     * Initialze class data
     *
     * @access private
     * @param string $useragent A useragent string to parse for details     
     */
    private function initialize($useragent)
    {
        $this->set_useragent($useragent);
        $this->set_os();
        $this->set_browser();
        $this->set_ip();
    }
   
    /**
     * Parse a user provided useragent string - This overwrites current data
     *
     * @access public
     * @param string $useragent A useragent string to parse for details     
     */
    public function parse_from_string($useragent)
    {
        // Erase current details array if there are any
        $this->clear_details();
        $this->initialize($useragent);
    }
   
    /**
     * Parse the useragent string provided from the request header- This overwrites current data
     *
     * @access public     
     */
    public function parse_from_header()
    {
        // Erase current details array if there are any
        $this->clear_details();
        $this->initialize();
    }
   
    /**
     * Adds an entry into the details array
     *
     * @access private
     * @param string $token The name of the item we are adding
     * @param string $value The value of the item we are adding
     */
    private function add_detail($token, $value)
    {
        if (!isset($this->details[$token]))
        {
            $this->details[$token] = $value;
        }
    }
   
    /**
     * Sets the users complete user agent string
     *
     * @access private
     * @param string $useragent The useragent to parse [Optional]     
     * @return string The users user agent on success, boolean false on failure
     */
    private function set_useragent($useragent = '')
    {
        if (empty($useragent))
        {
            $useragent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : getenv('HTTP_USER_AGENT');
           
            if ($useragent === false)
            {
                throw new Exception('The user agent could not be fetched.');
            }
        }
       
        $this->add_detail('useragent', $useragent);
    }
   
    /**
     * Sets the IP property
     *
     * @author Robert Gonzalez <robert@everah.com>
     * @access private
     */
    private function set_ip()
    {
        $this->add_detail('client_ip', $this->get_ip_address());
    }
   
    /**
     * Sets browser information
     *
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @author Robert Gonzalez <robert@everah.com>
     * @access private
     * @return string browser name and version or false if unrecognized
     */
    private function set_browser()
    {
        $browser_regex = $this->get_browser_regex_string();
        $browser_name = 'Unknown';
        $browser_abbreviation = 'Unknown';
        $browser_major_version = 'Unknown';
        $browser_minor_version = 'Unknown';
        $browser_letter_version = null;
        $browser_full_version = 'Unknown';
        $browser_complete_name = 'Unknown';
        
        if (preg_match_all($browser_regex, $this->details['useragent'], $results))
        {
            // get the position of the last browser found
            $count = count($results[0])-1;
           
            // Browser name
            $browser_name = $results[1][$count];
           
            // Major version
            $browser_major_version = $results[2][$count];
           
            // insert findings into the container
            $browser_abbreviation = strtolower($results[1][$count]);
           
            // parse the minor version string and look for alpha chars
            preg_match('/([.\0-9]+)?([\.a-z0-9]+)?/i', $results[3][$count], $match);
           
            // Minor version handling
            $minor_version = isset($match[1]) ? $match[1] : '.0';
           
            // Letter version handling
            if (isset($match[2]))
            {
                $browser_letter_version = $match[2];
            }
           
            // Create the necessary detail values
            $this->add_detail('browser_name', $browser_name);
            $this->add_detail('browser_major_version', $browser_major_version);
            $this->add_detail('browser_minor_version', $minor_version);
            $this->add_detail('browser_full_version', $this->details['browser_major_version'].$this->details['browser_minor_version']);
            $this->add_detail('browser_full_name', $this->details['browser_name']. ' ' . $this->details['browser_full_version']);
            $this->add_detail('browser_abbreviation', $this->browser_list[$browser_abbreviation]);
           
            // Handle the registration of the letter version
            if (!is_null($browser_letter_version))
            {
                $this->add_detail('browser_letter_version', $browser_letter_version);
            }
        }
    }
   
    /**
     * Sets operating system and platform details
     *
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @return string os name and version or false in unrecognized os
     */
    private function set_os()
    {
        // regexes to use
        $regex_windows  = '/([^dar]win[dows]*)[\s]?([0-9a-z]*)[\w\s]?([a-z0-9.]*)/i';
        $regex_mac      = '/(68[k0]{1,3})|(ppc mac os x)|([p\S]{1,5}pc)|(darwin)/i';
        $regex_os2      = '/os\/2|ibm-webexplorer/i';
        $regex_sunos    = '/(sun|i86)[os\s]*([0-9]*)/i';
        $regex_irix     = '/(irix)[\s]*([0-9]*)/i';
        $regex_hpux     = '/(hp-ux)[\s]*([0-9]*)/i';
        $regex_aix      = '/aix([0-9]*)/i';
        $regex_dec      = '/dec|osfl|alphaserver|ultrix|alphastation/i';
        $regex_vms      = '/vax|openvms/i';
        $regex_sco      = '/sco|unix_sv/i';
        $regex_linux    = '/x11|inux/i';
        $regex_bsd      = '/(free)?(bsd)/i';
        $regex_amiga    = '/amiga[os]?/i';
       
        $client_platform = 'Unknown';
        $client_os = 'Unknown';
       
        // look for Windows Box
        if (preg_match_all($regex_windows, $this->details['useragent'], $match))
        {
            /** Windows has some of the most ridiculous HTTP_USER_AGENT strings */
            //$match[1][count($match[0])-1];
            $v  = $match[2][count($match[0])-1];
            $v2 = $match[3][count($match[0])-1];
            // Establish NT 5.1 as Windows XP
            if (stristr($v,'NT') && $v2 == 5.1)
            {
                $v = 'XP';
            }
            // Establish NT 5.0 and Windows 2000 as win2k
            elseif ($v == '2000')
            {
                $v = '2000';
            }
            elseif (stristr($v, 'NT') && $v2 == 5.0)
            {
                $v = '2000';
            }
            // Establish 9x 4.90 as Windows 98
            elseif (stristr($v, '9x') && $v2 == 4.9)
            {
                $v = '98';
            }
            // See if we're running windows 3.1
            elseif ($v.$v2 == '16bit')
            {
                $v = '3.1';
            }
            // otherwise display as is (31,95,98,NT,ME,XP)
            else
            {
                $v .= $v2;
            }
           
            // update browser info container array
            if (empty($v))
            {
                $v = 'Windows';
            }
           
            $client_os = $v;
            $client_platform = 'Windows';
        }
        //  look for amiga OS
        elseif (preg_match($regex_amiga, $this->details['useragent'], $match))
        {
            $client_platform = 'Amiga';
           
            if (stristr($this->details['useragent'], 'morphos'))
            {
                // checking for MorphOS
                $client_os = 'MorphOS';
            }
            elseif (stristr($this->details['useragent'], 'mc680x0'))
            {
                // checking for MC680x0
                $client_os = 'MC680x0';
            }
            elseif (stristr($this->details['useragent'], 'ppc'))
            {
                // checking for PPC
                $client_os = 'PPC';
            }
            elseif (preg_match('/(AmigaOS [\.1-9]?)/i', $this->details['useragent'], $match))
            {
                // checking for AmigaOS version string
                $client_os = $match[1];
            }
        }
        // look for OS2
        elseif (preg_match($regex_os2, $this->details['useragent']))
        {
            $client_os = 'OS2';
            $client_platform = 'OS2';
        }
        // look for mac
        // sets: platform = mac ; os = 68k or ppc
        elseif (preg_match($regex_mac, $this->details['useragent'], $match))
        {
            $client_platform = 'Mac';
            $os = !empty($match[1]) ? '68K' : '';
            $os = !empty($match[2]) ? 'OSX' : $os;
            $os = !empty($match[3]) ? 'PPC' : $os;
            $os = !empty($match[4]) ? 'OSX' : $os;
            $client_os = $os;
        }
        //  look for *nix boxes
        //  sunos sets: platform = *nix ; os = sun|sun4|sun5|suni86
        elseif (preg_match($regex_sunos, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            if (!stristr('sun', $match[1]))
            {
                $match[1] = 'Sun'.$match[1];
            }
            $client_os = $match[1].$match[2];
        }
        //  irix sets: platform = *nix ; os = irix|irix5|irix6|...
        elseif (preg_match($regex_irix, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = $match[1].$match[2];
        }
        //  hp-ux sets: platform = *nix ; os = hpux9|hpux10|...
        elseif (preg_match($regex_hpux, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $match[1] = str_replace('-', '', $match[1]);
            $match[2] = (int) $match[2];
            $client_os = $match[1].$match[2];
        }
        //  aix sets: platform = *nix ; os = aix|aix1|aix2|aix3|...
        elseif (preg_match($regex_aix, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'aix'.$match[1];
        }
        //  dec sets: platform = *nix ; os = dec
        elseif (preg_match($regex_dec, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'dec';
        }
        //  vms sets: platform = *nix ; os = vms
        elseif (preg_match($regex_vms, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'vms';
        }
        //  sco sets: platform = *nix ; os = sco
        elseif (preg_match($regex_sco, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'SCO';
        }
        //  unixware sets: platform = *nix ; os = unixware
        elseif (stristr($this->details['useragent'], 'unix_system_v'))
        {
            $client_platform = '*nix';
            $client_os = 'Unixware';
        }
        //  mpras sets: platform = *nix ; os = mpras
        elseif (stristr($this->details['useragent'], 'ncr'))
        {
            $client_platform = '*nix';
            $client_os = 'Mpras';
        }
        //  reliant sets: platform = *nix ; os = reliant
        elseif (stristr($this->details['useragent'], 'reliantunix'))
        {
            $client_platform  = '*nix';
            $client_os = 'Reliant';
        }
        //  sinix sets: platform = *nix ; os = sinix
        elseif (stristr($this->details['useragent'], 'sinix'))
        {
            $client_platform = '*nix';
            $client_os = 'Sinix';
        }
        //  bsd sets: platform = *nix ; os = bsd|freebsd
        elseif (preg_match($regex_bsd, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = $match[1].$match[2];
        }
        //  last one to look for
        //  linux sets: platform = *nix ; os = linux
        elseif (preg_match($regex_linux, $this->details['useragent'], $match))
        {
            $client_platform = '*nix';
            $client_os = 'Linux';
        }
       
        $this->add_detail('client_platform', $client_platform);
        $this->add_detail('client_os', $client_os);
    }
   
    /**
     * This simply sets a regular expression string to use for browser comparison
     *
     * @author Roger Raymond <epsilon7@users.sourceforge.net>
     * @access private
     * @return string A regular expression string
     */
    private function get_browser_regex_string()
    {
        $browsers = '';
        foreach ($this->browser_list as $k => $v)
        {
            if (!empty($browsers))
            {
                $browsers .= '|';
            }
           
            $browsers .= $k;
        }
       
        $version_string = '[\/\sa-z(]*([0-9]+)([\.0-9a-z]+)?';
        return "/($browsers)$version_string/i";
    }
   
    /**
     * Get the users IP address
     *
     * @access private
     * @return string IP Address of the user
     */
    private function get_ip_address()
    {
        return getenv('HTTP_CLIENT_IP') ? getenv('HTTP_CLIENT_IP') : getenv('REMOTE_ADDR');
    }
   
    /**
     * Gets the users browser information
     *
     * @access public
     * @return string The users browser
     */
    public function get_browser()
    {
        return $this->get_detail('browser_full_name');
    }
   
    /**
     * Gets the users complete user agent information
     *
     * @access public
     * @return string The users browser
     */
    public function get_useragent()
    {
        return $this->get_detail('useragent');
    }
   
    /**
     * Gets the users operating system
     *
     * @access public
     * @return string The users operating system
     */
    public function get_os()
    {
        return $this->get_detail('client_os');
    }
   
    /**
     * Gets the users operating system environment
     *
     * @access public
     * @return string The users platform and operating system
     */
    public function get_platform()
    {
        return $this->get_detail('client_platform') . ' ' . $this->get_detail('client_os');
    }
   
    /**
     * Gets the users IP Address
     *
     * @access public
     * @return string The users IP Address
     */
    public function get_ip()
    {
        return $this->get_detail('client_ip');
    }
   
    /**
     * Gets the detail requested
     *
     * @access public
     * @return string A detail entry if it exists, false otherwise
     */
    public function get_detail($detail)
    {
        if ($detail == 'all')
        {
            return $this->details;
        }
        else
        {
            return $this->has_detail($detail) ? $this->details[$detail] : 'Detail not found';
        }
    }

    /**
     * Gets all client details
     *
     * @access public
     * @return array An array of all information fetched from the client
     */
    public function get_details()
    {
        return $this->details;
    }
   
    /**
     * Checks whether a detail is available or not
     *
     * @access private
     * @return boolean True if the detail exists, false otherwise
     */
    private function has_detail($detail)
    {
        return isset($this->details[$detail]);
    }
    
    /**
     * Clears the current details array to prepare it to receive new data
     *
     * @access private
     */
    private function clear_details()
    {
        if (!empty($this->details))
        {
            $this->details = array();
        }
    }
}
?>
Unit tests are underway. I believe that I really need to learn how to unleash the power of unit testing as it seems like a foreign language to me at the moment.

Posted: Tue Apr 17, 2007 4:47 am
by Ollie Saunders
Looks good :)

Posted: Tue Apr 17, 2007 10:06 am
by RobertGonzalez
Thanks for the input ole. I just noticed that I need to adjust the class PHPDoc block to reflect the changes in usage. I will do that momentarily.