RegAPI for PHP5

Hey y'all. I rewrote the RegAPI libraries to work with PHP5 without the compatibility library. It's not a drop-in replacement, but should be pretty easy to use. The documentation is in the code, and see the index.php example for some usage. I tried to make it fairly robust, but I pretty much suck at PHP, so please lemme know if y'all find anything untoward. >_>
It consists of two files...
A configuration file, config.php:
<?php
// Get your CAPS from https://secure-web2.secondlife.com/developers/third_party_reg/#what_are_cap
//	and fill them in here.
define('URI_CREATE_USER', 'FILL_ME_IN_WITH_YOUR_CAPS_URL');
define('URI_GET_LAST_NAMES', 'FILL_ME_IN_WITH_YOUR_CAPS_URL');
define('URI_CHECK_NAME', 'FILL_ME_IN_WITH_YOUR_CAPS_URL');
define('URI_GET_ERROR_CODES', 'FILL_ME_IN_WITH_YOUR_CAPS_URL');
?>
And the library, RegAPI.php:
<?php
////////////////////////////////////////////////////////////////////////////////
//
// Hacked up by Liandra Ceawlin, Version 1, Tue Jul 21 11:53:34 EDT 2009
//
//	I'm not really much of a PHP coder, so if you find any bugs or see things
// in this code that are utterly atrocious, please IM me in-world or email
// support@ceawlin.com. Good luck with it!
//
////////////////////////////////////////////////////////////////////////////////
//
// Yadda yadda so I don't get sued when it breaks.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
////////////////////////////////////////////////////////////////////////////////
require( 'config.php' );
// Check to see if <username> (first name) and <last_name_id> (an integer code
//	as returned from regapiGetLastNames below) are a valid pair to create a
//	new account with. If they are, the function returns true. If not, it
//	returns false. It also returns false if there was a query error, but you
//	can check to see if regapiGetCurrentErrors() returns anything to see if
//	there was an error or not.
function	regapiCheckName( $username, $last_name_id )
{
	// Yea, maybe this is kinda lame, but seriously easier than constructing
	//	a DOM object and ->saveXML()ing. >_>
	
	// Build the XML.
	$str = "<llsd><map><key>username</key><string>".$username.
			"</string><key>last_name_id</key><integer>".$last_name_id.
			"</integer></map></llsd>";
	
	// POST it and get a DOM object back.
	$dom = regapiHttpPostDOM( URI_CHECK_NAME, $str );
	if( regapiProcessDOMForErrors($dom) )
		return false;
	
	// Parse out the return value and return boolean.
	$nodelist = $dom->getElementsByTagName( "boolean" );
	if( $nodelist != NULL && $nodelist->length )
		return $nodelist->item(0)->textContent == 'true';
	regapiSetUnknownError();
	return false;
}
// Create a new user, using the required fields <username> (first name),
//	<last_name_id> (an integer code as returned from regapiGetLastNames below),
//	<email>, and <password>. <options> is an associative array of optional
//	parameters (see https://wiki.secondlife.com/wiki/Registration_API_Reference#Parameters_2),
//	where the key is the option name and the value is the, uh, value of that
//	option. The function returns the agent's UUID.
// Returns NULL on error, then you can access the error codes with
//	regapiGetCurrentErrors().
function	regapiCreateUser( $username, $last_name_id, $email, $password, $dob, $options )
{
	// A mapping of options and their types, used in verification and XML
	//	creation.
	$valid_options = array(
			"username"			=> "string",
			"last_name_id"		=> "integer",
			"email"				=> "string",
			"password"			=> "string",
			"dob"				=> "string",
			"limited_to_estate"	=> "integer",
			"start_region_name"	=> "string",
			"start_local_x"		=> "float",
			"start_local_y"		=> "float",
			"start_local_z"		=> "float",
			"start_look_at_x"	=> "float",
			"start_look_at_y"	=> "float"
		);
	
	// Remove any invalid optional, uh, options.
	if( !is_array($options) )
		$options = array();
	foreach( $options as $opt => $val )
	{
		if( !array_key_exists($opt,$valid_options) )
		{
			?>Warning: Unrecognized option <?php echo $opt; ?> removed from regapiCreateUser list.<?php
			unset( $options[$opt] );
		}
	}
	
	// Add the required options to the option list.
	$options["username"]		= $username;
	$options["last_name_id"]	= $last_name_id;
	$options["email"]			= $email;
	$options["password"]		= $password;
	$options["dob"]				= $dob;
	
	// Build the XML.
	// Yea, maybe this is kinda lame, but seriously easier than constructing
	//	a DOM object and ->saveXML()ing. >_>
	$str = "<llsd><map>";
	foreach( $options as $opt => $val )
	{
		$type = $valid_options[$opt];
		$str = $str."<key>".$opt."</key><".$type.">".$val."</".$type.">";
	}
	$str = $str."</map></llsd>";
	
	// POST it and get a DOM object back.
	$dom = regapiHttpPostDOM( URI_CREATE_USER, $str );
	if( regapiProcessDOMForErrors($dom) )
		return NULL;
		
	// Parse out the return value and return agent uuid string.
	$nodelist = $dom->getElementsByTagName( "string" );
	if( $nodelist != NULL && $nodelist->length )
		return $nodelist->item(0)->textContent;
	regapiSetUnknownError();
	return NULL;
}
// This function returns an associative 2d array of error codes, with the first
//	key being the integer code, and the second key being either "desc" for the
//	short error description, or "message" for the verbose error message. See
//	the example index.php file for example of usage.
// Returns NULL on error, then you can access the error codes with
//	regapiGetCurrentErrors(), although you won't be able to look up the error
//	code text, lol.
function regapiGetErrorCodes()
{
	global $regAPI_errorcodecache;
	
	// We cache the first query for error codes so that subsequent calls to not
	//	cause more queries.
	if( $regAPI_errorcodecache != NULL )
		return $regAPI_errorcodecache;
	
	$errors = array();
	
	$dom = regapiHttpGetDOM( URI_GET_ERROR_CODES );
	if( regapiProcessDOMForErrors($dom) )
		return NULL;
	
	// Get <llsd>.
	$node = $dom->documentElement;
	// Move to enclosing <array>.
	if( $node != NULL )
		$node = $node->firstChild;
	// move to first sub-<array>.
	if( $node != NULL && $node->nodeName == "array" )
		$node = $node->firstChild;
	else
	{
		regapiSetUnknownError();
		return NULL;
	}
	// Move through sub-<array>s and build the error list.
	while( $node != NULL && $node->nodeName == "array" )
	{
		// Build the array from the sub-<array> children.
		$child = $node->firstChild;
		// Get code.
		if( $child != NULL )
		{
			$code = $child->textContent;
			$child = $child->nextSibling;
		}
		else
		{
			regapiSetUnknownError();
			return NULL;
		}
		// Get short text.
		if( $child != NULL )
		{
			$desc = $child->textContent;
			$child = $child->nextSibling;
		}
		else
		{
			regapiSetUnknownError();
			return NULL;
		}
		// Get verbose error message.
		if( $child != NULL )
			$message = $child->textContent;
		else
		{
			regapiSetUnknownError();
			return NULL;
		}
		$errors[$code]["desc"] = $desc;
		$errors[$code]["message"] = $message;
		$node = $node->nextSibling;
	}
	
	$regAPI_errorcodecache = $errors;
	return $errors;
}
// Returns an associative array of available last names, in alphabetical
//	order, where key is the name string and value is the numeric id
//	corresponding to that name. If you pass <num> of 1 or more, it will
//	randomly choose that many names from the list of available names and return
//	them. If <num> is negative, it will return all available names. If you
//	pass something into the <username> parameter, only last names that are
//	available with the specified username (first name) will be returned.
//	WARNING: Passing something into <username> is currently very slow, and gets
//	even slower the larger <num> is...
// Returns NULL on error, then you can access the error codes with
//	regapiGetCurrentErrors().
function	regapiGetLastNames( $username, $num )
{
	$dom = regapiHttpGetDOM( URI_GET_LAST_NAMES );
	if( regapiProcessDOMForErrors($dom) )
		return NULL;
	$names = array();
		
	$nodelist = $dom->getElementsByTagName( "map" );
	if( $nodelist != NULL && $nodelist->length )
	{
		// This will only return the names in the first map, but there is
		//	only one map anyway.
		$node = $nodelist->item(0)->firstChild;
		while( $node != NULL )
		{
			$k = $node->textContent;
			$node = $node->nextSibling;
			if( $node == NULL_KEY )
			{
				// This should never happen.
				return rval;
			}
			$s = $node->textContent;
			$node = $node->nextSibling;
			$names[$s] = $k;
		}
	}
	$rval = array();
	while( ($num>=0&&count($rval)<$num) || ($num<0&&count($names)) )
	{
		if( $num < 0 )
		{
			$rval = $names;
			$names = array();
		}
		else
		{
			$entries = array_rand( $names, $num-count($rval) );
			foreach( $entries as $k )
			{
				if( $username=="" || $username==NULL || regapiCheckName($username,$names[$k]) )
					$rval[$k] = $names[$k];
				unset( $names[$k] );
			}
		}
		if( !count($names) )
			break;
	}
	ksort( $rval );
	return $rval;
}
function	regapiGetCurrentErrors()
{
	global $regAPI_lasterrors;
	if( !count($regAPI_lasterrors) )
		return NULL;
	return $regAPI_lasterrors;
}
////////////////////////////////////////////////////////////////////////////////
// Internal things below here. You probably don't need to worry about them.
////////////////////////////////////////////////////////////////////////////////
$regAPI_lasterrors		= array();
$regAPI_errorcodecache	= NULL;
// Utility function for random errors not reported via XML returns from queries.
//	We just set a 0 (undefined) error.
function	regapiSetUnknownError()
{
	global $regAPI_lasterrors;
	$regAPI_lasterrors = array();
	$regAPI_lasterrors[] = 0;
}
// This function re-sets the lasterrors array, and returns true if the XML
//	looks like an error message, false otherwise.
function	regapiProcessDOMForErrors( $dom )
{
	global $regAPI_lasterrors;
	
	$regAPI_lasterrors = array();
	
	if( $dom == NULL )
		return true;
	// Get <llsd>.
	$node = $dom->documentElement;
	// Move to enclosing <array>.
	if( $node != NULL )
		$node = $node->firstChild;
	// Move to first <integer> node, if we're really in an <array>.
	if( $node != NULL && $node->nodeName == "array" )
		$node = $node->firstChild;
	else
		return false;
	$startnode = $node;
	// Scan over child nodes to see if they are all integers.
	while( $node != NULL )
	{
		if( $node->nodeName != "integer" )
			return false;
		$node = $node->nextSibling;
	}
	// If we get here, then the array is all integers, and it looks like an
	//	error message. So we add the error codes to the array.
	$node = $startnode;
	while( $node != NULL )
	{
		$regAPI_lasterrors[] = $node->textContent;
		$node = $node->nextSibling;
	}
	return true;
}
// Send a GET request to <url> and return a DOM object, or NULL on error.
function	regapiHttpGetDOM( $url )
{
	$ch = curl_init( $url );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, TRUE );
 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, FALSE );
	$doc = curl_exec( $ch );
	$status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
	curl_close( $ch );
	if( $status >= 400 )
		return NULL;
 
	$dom = new DOMDocument();
	if( !(@$dom->loadXML($doc)) )
		return NULL;
	return $dom;
}
// Send a POST request to <url> and return a DOM object, or NULL on error.
function	regapiHttpPostDOM( $url, $str )
{
	$ch = curl_init( $url );
 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, FALSE );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, TRUE );
	curl_setopt( $ch, CURLOPT_POST, TRUE );
	curl_setopt( $ch, CURLOPT_FAILONERROR, 1 );
	curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
	curl_setopt( $ch, CURLOPT_HTTPHEADER, Array("Content-Type: application/xml") );
	curl_setopt( $ch, CURLOPT_POSTFIELDS, $str );
	$doc = curl_exec( $ch );
	$status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
	curl_close( $ch );
	if( $status >= 400 )
		return NULL;
	
	$dom = new DOMDocument();
	if( !(@$dom->loadXML($doc)) )
		return NULL;
	return $dom;
}
?>
And here's a modified version of the example index.php that uses the new library:
<?php
require( 'RegAPI.php' );
function get_months()
{
 $months = array();
 for( $i=1; $i<=12; $i++ )
 {
 $key = date( 'n', mktime(0, 0, 0, $i, 1, 2000) );
 $value = date( 'M.', mktime(0, 0, 0, $i, 1, 2000) );
 $months[sprintf("%02d", $key)] = $value;
 }
 return $months;
}
 
function get_years()
{
 $today = getdate();
 $max_year = $today['year'] - 90;
 $min_year = $today['year'] - 13;
 
 $years = array();
 for( $i=$min_year; $i>=$max_year; $i-- )
 $years[$i] = $i;
 return $years;
}
 
function get_days()
{
 $days = array();
 for( $i=1; $i<=31; $i++ )
 $days[sprintf("%02d",$i)] = sprintf("%02d",$i);
 return $days;
}
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
	$username = $_POST['username'];
	$last_name_id = (int)$_POST['last_name_id'];
	$email = $_POST['email'];
	$password = $_POST['password'];
	$dob = $_POST['dob_year'].'-'.$_POST['dob_month'].'-'.$_POST['dob_day'];
		
	if( regapiCheckName($username,$last_name_id) )
	{
		$uuid = regapiCreateUser( $username, $last_name_id, $email, $password, $dob, NULL );
		if( $uuid )
		{
			echo "Created ".$uuid."!";
		}
		else
		{
			if( $errors=regapiGetCurrentErrors() )
			{
				$errorcodes = regapiGetErrorCodes();
				foreach( $errors as $e )
				{
					if( array_key_exists($e,$errorcodes) )
						echo $errorcodes[$e]["message"]."";
					else
						echo "Unknown error.";
				}
			}
		}
	}
	else
	{
		echo "That name is not available.";
	}
}
?>
Create Second Life Account
 
<form action="<?php print $_SERVER['PHP_SELF']; ?>" method="post">
 
 First name:
 <input type="text" name="username" size="25" maxlength="31" value="" />
 Last name:
 
 <select name="last_name_id">
 <?php
	 $last_names = regapiGetLastNames( NULL, -1 );
	 foreach( $last_names as $name => $last_name_id )
	 print '<option value="'.$last_name_id.'">'.$name.'</option>';
 ?>
 </select>
 
 Password:
 <input type="password" name="password" size="20" value="" />
 Email:
 <input type="text" name="email" size="35" value="" />
 Date of brith:
 
 <select name="dob_day">
 <?php
 $days = get_days();
 foreach ($days as $key => $value) { print '<option value="'.$key.'" '.$selected.'>'.$value.'</option>'; }
 ?>
 </select>
 
 <select name="dob_month">
 <?php
 $months = get_months();
 foreach ($months as $key => $value) { print '<option value="'.$key.'" '.$selected.'>'.$value.'</option>'; }
 ?>
 </select>
 
 <select name="dob_year">
 <?php
 $years = get_years();
 foreach ($years as $key => $value) { print '<option value="'.$key.'" '.$selected.'>'.$value.'</option>'; } 
 ?>
 </select>
 
 
 <input type="submit" value="Create SL Account" />
 
</form>