Source for file TeraWurflDatabase_MongoDB.php
Documentation is available at TeraWurflDatabase_MongoDB.php
* Tera_WURFL - PHP MySQL driven WURFL
* Tera-WURFL was written by Steve Kamerman, and is based on the
* Java WURFL Evolution package by Luca Passani and WURFL PHP Tools by Andrea Trassati.
* This version uses a MySQL database to store the entire WURFL file, multiple patch
* files, and a persistent caching mechanism to provide extreme performance increases.
* @package TeraWurflDatabase
* @author Steve Kamerman <stevekamerman AT gmail.com>
* @author Simon Harris <tw AT pointbeing.net>
* @version Stable 2.1.3 $Date: 2010/09/18 15:43:21
* @license http://www.mozilla.org/MPL/ MPL Vesion 1.1
* Provides connectivity from Tera-WURFL to MongoDB
* @package TeraWurflDatabase
* MongoDB connection options
* @link http://us.php.net/manual/en/mongo.construct.php
protected static $CONNECTION_OPTIONS = array(
"persist" => "Tera-WURFL",
* @todo See if this can be implemented
protected static $PREALLOC_SIZE = 31457280;
self::$MERGE = TeraWurflConfig::$TABLE_PREFIX. 'Merge';
// Device Table Functions (device, hybrid, patch) --------------------------
* @return array The device capabilities
$device = $this->mergecoll->findOne($tofind);
// is this really an Exception? Cloned from MySQL5 driver
throw new Exception("Tried to lookup an invalid WURFL Device ID: $wurflID");
return $device['capabilities'];
* @param string $tablename
$res = $this->mergecoll->find(array("matcher"=> $matcher));
foreach ($res as $device) {
$data[$device['deviceID']] = $device['user_agent'];
* Exact match by user agent string
* @param string $userAgent
return $data['deviceID'];
* RIS == Reduction in String (reduce string one char at a time)
* @param string $userAgent
* @param UserAgentMatcher $matcher
* @return string A TW Device ID
$toexec = 'function(ua, tolerance, matcher) { return performRis(ua, tolerance, matcher) }';
$args = array(utf8_encode($userAgent), $tolerance, $matcher->tableSuffix());
$response = $this->dbcon->execute($toexec, $args);
if ( !empty($response['ok']) && $response['ok'] == 1 && !empty($response['retval'])) {
return $response['retval'];
$response = $this->dbcon->execute('function(deviceID){ return performFallback(deviceID) }',array($wurflID));
$data = $response['retval'];
$tw->toLog("WURFL Error: device {$data[count($data)-1]['id']} falls back on an inexistent device: {$data[count($data)-1]['fall_back']}",LOG_ERR,__CLASS__. '::'.__FUNCTION__ );
$insert_errors = array();
// insert records into a new temp table until we know everything is OK
$temptable = self::$MERGE . self::$DB_TEMP_EXT;
// create this collection manually since it is fixed
$this->dbcon->command(array(
"size" => self::$PREALLOC_SIZE,
// "autoIndexId" => false
$collection = $this->dbcon->selectCollection($temptable);
foreach ($tables as $table => $devices) {
foreach ($devices as $device) {
'deviceID' => $device['id'],
'user_agent' => $device['user_agent'],
'fall_back' => $device['fall_back'],
'match' => preg_match('/^DO_NOT_MATCH/',$device['user_agent'])? false: true,
'actual_device_root'=> (isset ($device['actual_device_root']) ) ? $device['actual_device_root'] : '',
'capabilities' => $device,
$collection->batchInsert($matcherbatch);
$insert_errors[] = 'DB server reported error on id "' . $device['id'] . '": ' . $e->getMessage();
if (count($insert_errors) > 0) {
// Roll back changes, and leave the temp table in the DB for manual inspection
$this->mergecoll->ensureIndex(array('deviceID' => 1), array("unique"=> true,"dropDups"=> true,"background"=> true,"safe"=> false));
$this->mergecoll->ensureIndex(array('user_agent' => 1), array("unique"=> false,"dropDups"=> false,"background"=> true,"safe"=> false));
$this->mergecoll->ensureIndex(array('fall_back' => 1), array("unique"=> false,"dropDups"=> false,"background"=> true,"safe"=> false));
$this->mergecoll->ensureIndex(array('matcher' => 1), array("unique"=> false,"dropDups"=> false,"background"=> true,"safe"=> false));
$this->mergecoll->ensureIndex(array('match' => 1), array("unique"=> false,"dropDups"=> false,"background"=> true,"safe"=> false));
$collection = $this->dbcon->selectCollection($name);
//$collection->ensureIndex(array('id' => 1), array("unique"=>true,"dropDups"=>true,"background"=>false,"safe"=>true));
// Cache Table Functions ---------------------------------------------------
* Drops, creates and indexes the cache table
$name = TeraWurflConfig::$TABLE_PREFIX. 'Cache';
$collection = $this->dbcon->selectCollection($name);
$collection->ensureIndex(array('user_agent' => 1), array("unique"=> true,"dropDups"=> true,"background"=> true,"safe"=> false));
* @param string $userAgent
* @return array Should return (bool) false or the device array
'user_agent' => $userAgent,
$device = $cachecoll->findOne($tofind);
if ( !is_null($device) ) {
return $device['cache_data'];
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
* @param string $userAgent
* @return boolean Whether the insert was successful
$cachecoll->insert($toinsert, array('safe' => true));
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
// Use this instance to rebuild the cache and to facilitate logging
$temptable = TeraWurflConfig::$TABLE_PREFIX. 'Cache' . self::$DB_TEMP_EXT;
$tempcoll = $this->dbcon->selectCollection($temptable);
$cachecoll = $this->dbcon->selectCollection($cachetable);
/* @var $fromcache MongoCursor */
$fromcache = $tempcoll->find(array(),array("user_agent" => 1));
// migrate cached items from old cache
if (0 == $fromcache->count()) {
// No records in cache table == nothing to rebuild
$rebuilder->toLog('Rebuilt cache table, existing table was empty - this is very unusual.', LOG_WARNING, __FUNCTION__ );
foreach ($fromcache as $item) {
// Just looking the device up will force it to be cached
$rebuilder->getDeviceCapabilitiesFromAgent($item['user_agent']);
// Reset the number of queries since we're not going to re-instantiate the object
$rebuilder->db->numQueries = 0;
$rebuilder->toLog('Rebuilt cache table.', LOG_NOTICE, __FUNCTION__ );
// Supporting DB functions -------------------------------------------------
$status = $this->dbcon->command(array("serverStatus"=> 1));
return "MongoDB ". $status['version'];
* Runs any stored procs needed to set up db
// clear the db.system.js collection
$collection = $this->dbcon->selectCollection('system.js');
$collection->remove(array("_id"=> "performRis"));
function performRis(ua, tolerance, matcher) {
while (curlen >= tolerance) {
var toMatch = ua.substr(0, curlen);
toMatch = toMatch.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\\\$&");
var matchReg = new RegExp('^' + toMatch);
var device = db. $merge.findOne({match:true, matcher:matcher, user_agent: matchReg},{deviceID:1});
array('_id' => 'performRis','value' => new MongoCode($performRis)),
$collection->remove(array("_id"=> "performFallback"));
function performFallback(deviceID){
var current_fall_back = deviceID;
while(current_fall_back != 'root' && i++ < 30){
res = db. $merge.findOne({deviceID:current_fall_back},{capabilities:1});
tree.push(res.capabilities);
current_fall_back = res.capabilities.fall_back;
array('_id' => 'performFallback','value' => new MongoCode($performFallback)),
* Establishes connection to database (does not check for DB sanity)
$this->dbcon = $this->mongo->selectDB(TeraWurflConfig::$DB_SCHEMA);
if (!empty(TeraWurflConfig::$DB_USER) && !empty(TeraWurflConfig::$DB_PASS)) {
$this->dbcon->authenticate(TeraWurflConfig::$DB_USER, TeraWurflConfig::$DB_PASS);
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
$collection->save(array('_id'=> $key,'value'=> $value));
$collection = $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX. 'Settings');
$record = $collection->findOne(array('_id'=> $key),array('value'));
if(is_null($record)) return null;
* Prepare raw text for use in queries (adding quotes if necessary)
$collections = $this->dbcon->listCollections();
foreach ($collections as $coll) {
$output[] = $coll->getName();
$rawstats = $this->dbcon->command(array('collStats' => $table));
$stats['rows'] = $rawstats['count'];
$collection = $this->dbcon->selectCollection($table);
'actual_device_root' => 1,
$res = $collection->find($tofind);
$stats['actual_devices'] = $res->count();
$stats['bytesize'] = $rawstats['storageSize'];
$cachecoll = $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX. 'Cache');
$cached = $cachecoll->find(array(), array('user_agent'));
$cached->sort(array('user_agent'));
foreach ($cached as $device) {
$uas[] = $device['user_agent'];
// Low-level collection management methods ---------------------------------
* @param string $collectionname
* @return boolean Whether the operation was successful
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
* @param string $collectionname
* @return boolean Whether the operation was successful
// NOTE: the MongoCollection::dropCollection() method leaks memory, do not use
$col = $this->dbcon->selectCollection($collectionname);
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
* @param string $collectionname
* @return boolean Whether the operation was successful
$this->dbcon->createCollection($collectionname);
$this->errors[] = $e->__toString() . ', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
$admindb = $this->mongo->admin;
'renameCollection' => $dbname . '.' . $from,
'to' => $dbname . '.' . $to,
// methods pending implementation ------------------------------------------
// LD == Levesthein Distance
throw new Exception("Error: this function (LD) is not yet implemented in MongoDB");
// methods enforced by parent class, but never called ----------------------
|