1<?php // -*- mode:php; tab-width:4; c-basic-offset:4; intent-tabs-mode:nil; -*-
2/*
3 * This file contains the MAC class
4 *
5 * Copyright (c) 2006 Andrew Teixeira
6 *
7 * PHP Version 5
8 *
9 * LICENSE: This source file is subject to version 3.0 of the PHP license
10 * that is available through the world-wide-web at the following URI:
11 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
12 * the PHP License and are unable to obtain it through the web, please
13 * send a note to license@php.net so we can mail you a copy immediately.
14 *
15 * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
16 * @link      http://pear.php.net/package/Net_MAC
17 * @author    Andrew Teixeira <ateixeira@gmail.com>
18 *
19 * $Id: MAC.php,v 1.6 2007/05/01 18:03:57 atex Exp $
20 */
21
22/**
23 * Require PEAR/Exception.php since we will be using PEAR Exceptions
24 * in this class.
25 */
26require_once 'PEAR/Exception.php';
27
28/**
29 * Constant to represent the maximum length of a line in the
30 * manufacturers file.
31 */
32define('NET_MAC_LINE_MAXLENGTH', 256);
33
34/**
35 * Error constant: signifies no problem (OK)
36 */
37define('NET_MAC_ERROR_OK', 0);
38
39/**
40 * Error constant: signifies a bad option being passed to a function
41 */
42define('NET_MAC_ERROR_BADOPT', 1);
43
44/**
45 * Error constant: signifies bad data being passed to a function
46 */
47define('NET_MAC_ERROR_BADDATA', 2);
48
49/**
50 * Error constant: signifies a bad database connection
51 */
52define('NET_MAC_ERROR_BADDB', 3);
53
54/**
55 * Error constant: signifies a bad manufacturers file
56 */
57define('NET_MAC_ERROR_BADFILE', 4);
58
59
60/**
61 * Extension of the main PEAR_Exception Class for use with the Net_MAC
62 * class.
63 *
64 * @package   Net_MAC
65 * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
66 * @link      http://pear.php.net/package/Net_MAC
67 * @version   1.0
68 * @author    Andrew Teixeira <ateixeira@gmail.com>
69 *
70 * @access    public
71 */
72class Net_MAC_Exception extends PEAR_Exception {}
73
74
75/**
76 * Class to validate and cleanly format Media Access Control (MAC)
77 * addresses
78 *
79 * @package   Net_MAC
80 * @license   http://www.php.net/license/3_0.txt  PHP License 3.0
81 * @link      http://pear.php.net/package/Net_MAC
82 * @version   1.0
83 * @author    Andrew Teixeira <ateixeira@gmail.com>
84 *
85 * @access    public
86 */
87class Net_MAC
88{
89    /**
90     * The MAC address to work on.
91     *
92     * @access protected
93     * @var string
94     */
95    protected $_macaddr;
96
97    /**
98     * A database instance to use in looking up MAC-to-vendor
99     * relationships.
100     *
101     * @access protected
102     * @var object
103     */
104    protected $_db;
105
106    /**
107     * The options to use while connecting to the database.
108     *
109     * @access protected
110     * @var array
111     */
112    protected $_dbOptions;
113
114    /**
115     * The default constructor
116     *
117     * This is the default constructor that will create and populate a
118     * valid Net_MAC object.
119     *
120     * @access public
121     *
122     * @param object &$db A valid instantiated {@link
123     * http://pear.php.net/package/MDB2/ MDB2} object to use when
124     * adding/retrieving information from the database for MAC address
125     * vendors
126     * @param array $options An array of options to use with the
127     * database in retrieving MAC address vendors.  The associative
128     * array should have key/value pairs as follows:
129     *
130     * <ul>
131     *  <li><b>tablename</b>: The name of the table where MAC address
132     *  vendor information lives</li>
133     *  <li><b>macaddrcol</b>: The name of the column containing the
134     *  MAC address prefixes</li>
135     *  <li><b>vendorcol</b>: The name of the column containing the
136     *  vendor name</li>
137     *  <li><b>desccol</b>: The name of the column containing any
138     *  extra descriptive information derived from the vendor list</li>
139     * </ul>
140     *
141     * @return void No return value.  A {@link Net_MAC_Exception
142     * Net_MAC_Exception} Exception object will be thrown if there is
143     * an error during construction so the constructor should be
144     * called from a try/catch block
145     */
146    function __construct(&$db, $options = NULL) {
147        require_once 'MDB2.php';
148
149        $this->_db = NULL;
150        $this->_macaddr = NULL;
151        $this->_dbOptions = NULL;
152
153        if (!$db instanceof MDB2_Driver_Common) {
154            throw new Net_MAC_Exception('Bad database object', NET_MAC_ERROR_BADDB);
155        }
156
157        if (!is_array($options)) {
158            throw new Net_MAC_Exception('Second parameter must be an array', NET_MAC_ERROR_BADOPT);
159        }
160
161        if (!isset($options['tablename'])) {
162            throw new Net_MAC_Exception('No table name given in options', NET_MAC_ERROR_BADDATA);
163        }
164
165        if (!isset($options['macaddrcol'])) {
166            throw new Net_MAC_Exception('No MAC address column name given in options', NET_MAC_ERROR_BADDATA);
167        }
168
169        if (!isset($options['vendorcol'])) {
170            throw new Net_MAC_Exception('No vendor column name given in options', NET_MAC_ERROR_BADDATA);
171        }
172
173        if (!isset($options['desccol'])) {
174            throw new Net_MAC_Exception('No description column name given in options', NET_MAC_ERROR_BADDATA);
175        }
176
177        $this->_db = $db;
178        $this->_db->loadModule('Extended');
179        $this->_dbOptions = $options;
180    } /* end default constructor */
181
182    /**
183     * Checks a MAC address
184     *
185     * This function will check a MAC address to make sure it is
186     * valid.
187     *
188     * @access static
189     *
190     * @param string $input The string containing the MAC Address
191     * @param string $delimiter The string representing the delimiter
192     * to use when verifying the MAC Address
193     *
194     * @return string <b>true</b> if the MAC address is valid,
195     * <b>false</b> otherwise
196     */
197    static function check($input, $delimiter = ':')
198    {
199        // Check for 6 octets without any punctuation
200        $retval = preg_match('/^([0-9a-fA-F][0-9a-fA-F]\Q'.$delimiter.'\E){5}([0-9a-fA-F][0-9a-fA-F]){1}$/', $input);
201
202        return $retval;
203    } /* end method check */
204
205    /**
206     * Sets the MAC address
207     *
208     * This method will set the MAC address in the class.
209     *
210     * @access public
211     *
212     * @param string $macaddr The string representing the MAC address
213     * @param string $delimiter The string representing the delimiter
214     * to use when verifying the MAC Address
215     *
216     * @return bool Returns <b>true</b> if the MAC address is set
217     * correctly, <b>false</b> otherwise
218     */
219    function setMAC($macaddr, $delimiter = ':')
220    {
221        $validMAC = self::check($macaddr, $delimiter);
222        if ($validMAC) {
223            $this->_macaddr = $macaddr;
224            return true;
225        }
226
227        return false;
228    } /* end method setMac */
229
230    /**
231     * Formats a MAC address
232     *
233     * This function will format a MAC address into XX:XX:XX:XX:XX:XX
234     * format from whatever format is passed to the function.  The
235     * delimiter (: in the example above) will be replaced with
236     * whatever string is passed to the $delimiter parameter
237     * (default :).
238     *
239     * @access static
240     *
241     * @param string $input The string containing the MAC Address
242     * @param string $delimiter The string representing the delimiter
243     * to use when formatting the MAC Address
244     * @param bool $uppercase If set to true (default), the
245     * hexadecimal values in the MAC Address will be returned in
246     * uppercase.  If false, the hexadecimal values will be returned
247     * in lowercase.
248     *
249     * @return string The formatted MAC Address or <b>false</b> on
250     * error
251     */
252    static function format($input, $delimiter = ':', $uppercase = true)
253    {
254        /* Replace all characters not in a valid MAC address with
255         * nothing.  We are going to be testing for a MAC address as
256         * XXXXXXXXXXXX instead of XX:XX:XX:XX:XX:XX
257         */
258        $macaddr = preg_replace('/[g-zG-Z]*\W*_*/', '', $input);
259
260        /* If $uppercase is true, set all the alpha characters to
261         * uppercase, otherwise set all the alpha characters to
262         * lowercase
263         */
264        $macaddr = ($uppercase) ? strtoupper($macaddr) : strtolower($macaddr);
265
266        // Check for 6 octets without any punctuation
267        if (!preg_match('/^([0-9a-fA-F][0-9a-fA-F]){6}$/', $macaddr)) {
268            return false;
269        }
270
271        // Add back in the $delimiter delimiters
272        $macaddr = preg_replace('/([0-9a-fA-F][0-9a-fA-F]){1}/', '$1' . $delimiter, $macaddr);
273
274        // Remove the trailing $delimiter
275        $macaddr = preg_replace('/' . $delimiter . '$/', '', $macaddr);
276
277        return $macaddr;
278    } /* end method format */
279
280    /**
281     * Import a manufacturers' file to the database
282     *
283     * This method will parse a manufacturers' file, such as the one
284     * from {@link
285     * http://anonsvn.wireshark.org/wireshark/trunk/manuf}, containing
286     * a list of MAC address prefix-to-vendor relationships.  If the
287     * $doReturn parameter is <b>false</b>, then the data will be
288     * imported into the database defined by the factory of this
289     * class.  However, if $doReturn is <b>true</b>, then the return
290     * will be an associative array with the key being the MAC address
291     * prefix and the data being an associative array with the keys
292     * 'vendor' and 'description'.
293     *
294     * @access public
295     *
296     * @param string $file The filename or URL of the manufacturers'
297     * file to parse.
298     * @param bool $doReturn If <b>true</b>, an array will be
299     * returned, if <b>false</b>, the data will be imported into the
300     * database. (default: false)
301     *
302     * @return mixed If $doReturn is true, the method will return an
303     * array.  Otherwise, the method will return <b>true</b> on
304     * success.  A {@link Net_MAC_Exception Net_MAC_Exception}
305     * Exception object will be thrown on failure in either case.
306     */
307    function importVendors($file, $doReturn = false) {
308        if ($file == NULL) {
309            throw new Net_MAC_Exception('No file or URL given', NET_MAC_ERROR_BADFILE);
310        }
311
312        $fp = @fopen($file, 'r');
313        if ($fp == false) {
314            throw new Net_MAC_Exception('Cannot open the file or URL given', NET_MAC_ERROR_BADFILE);
315        }
316
317        if ($doReturn) {
318            $retArr = array();
319        }
320        else {
321            // Prepare parameters for MDB2->buildManipSQL()
322            $fields = array ($this->_dbOptions['macaddrcol'],
323                             $this->_dbOptions['vendorcol'],
324                             $this->_dbOptions['desccol']);
325
326            $sql = $this->_db->buildManipSQL($this->_dbOptions['tablename'],
327                                             $fields, MDB2_AUTOQUERY_INSERT);
328
329            $query = $this->_db->prepare($sql);
330        }
331
332        while ($line = fgets($fp, NET_MAC_LINE_MAXLENGTH)) {
333            // Remove comments
334            if ( preg_match('/^\#/', $line) ) {
335                continue;
336            }
337
338            if ( preg_match('/^([0-9A-Fa-f][0-9A-Fa-f]:){2}([0-9A-Fa-f][0-9A-Fa-f]){1}/', $line) ) {
339                $pieces = preg_split('/\s+/', $line);
340
341                // Since the file is space-delimited, we now need to
342                // reconstruct the description field if it existed
343                $desc = NULL;
344                for ($i = 3; $i < count($pieces); $i++) {
345                    $desc .= $pieces[$i].' ';
346                }
347                $desc = rtrim($desc);
348
349                if ( (isset($pieces[0])) && (isset($pieces[1])) ) {
350                    if ($doReturn) {
351                        $retArr[$pieces[0]] = array('vendor' => $pieces[1],
352                                                    'description' => $desc);
353                    }
354                    else {
355                        $values = array($pieces[0], $pieces[1], $desc);
356                        $result = $query->execute($values);
357                    }
358                }
359            }
360        }
361
362        return ($doReturn) ? $retArr : true;
363    } /* end method importVendors */
364
365    /**
366     * Finds the vendor for a MAC address
367     *
368     * This method will search through the database to find a vendor
369     * that matches the MAC address stored in the class using {@link
370     * setMAC setMAC}.  If the $macList parameter is set, the method
371     * will use the array stored in $macList as the data source to
372     * find the MAC vendor instead of the database.  The array would
373     * have to be an array with the same characteristics as one
374     * returned from the {@link importVendors importVendors} method
375     * when using the $doReturn parameter.
376     *
377     * @access public
378     *
379     * @param bool $getDescription If set to true, the return value
380     * will be an array with keys 'vendor' and 'description'.
381     * Normally the method will simply return the vendor name.
382     * @param array $macList An optional list of MAC-to-vendor
383     * relationships to search instead of using the
384     * database. (default: NULL)
385     *
386     * @return mixed Returns an associative array if $getDescription
387     * is <b>true</b>, returns a string with the vendor name if
388     * $getDescription is <b>false</b>.  If the MAC vendor cannot be
389     * found in the vendor list, <b>false</b> is returned.
390     */
391    function findVendor($getDescription = false, $macList = NULL)
392    {
393        if ($macList == NULL) {
394            $macaddrcol = $this->_dbOptions['macaddrcol'];
395            $vendorcol = $this->_dbOptions['vendorcol'];
396            $desccol = $this->_dbOptions['desccol'];
397
398            /* The manufacturers' list only uses the first 3 octets,
399             * so we need to get that portion of the stored MAC
400             * address. */
401            $macaddr = substr(self::format($this->_macaddr, ':'), 0, 8);
402
403            // Prepare parameters for MDB2->buildManipSQL()
404            $fields = array ($macaddrcol, $vendorcol, $desccol);
405            $where = "$macaddrcol = '$macaddr'";
406
407            $sql = $this->_db->buildManipSQL($this->_dbOptions['tablename'],
408                                             $fields, MDB2_AUTOQUERY_SELECT,
409                                             $where);
410
411            $query = $this->_db->prepare($sql);
412            $result = $query->execute();
413
414            if ( (MDB2::isError($result)) || ($result->numRows() == 0) ) {
415                return false;
416            }
417
418            $macInfo = $result->fetchRow(MDB2_FETCHMODE_ASSOC);
419
420            if ($getDescription) {
421                return array($vendorcol => $macInfo[$vendorcol],
422                             $desccol => $macInfo[$desccol]);
423            }
424            else {
425                return $macInfo[$vendorcol];
426            }
427        }
428        else {
429            return false;
430        }
431    } /* end method findVendor */
432
433} /* end class Net_MAC */
434
435/*
436 * Local variables:
437 * tab-width: 4
438 * c-basic-offset: 4
439 * indent-tabs-mode: nil
440 * End:
441 */
442?>
443