2004-06-07 02:59:58 +00:00
|
|
|
<?php
|
2004-06-09 16:13:46 +00:00
|
|
|
# $Id$
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* DO NOT USE !!! Unless you want to help developping it.
|
|
|
|
|
*
|
|
|
|
|
* This file is an attempt to port the mysql database layer to postgreSQL. The
|
|
|
|
|
* only thing done so far is s/mysql/pg/ and dieing if function haven't been
|
|
|
|
|
* ported.
|
|
|
|
|
*
|
|
|
|
|
* As said brion 07/06/2004 :
|
|
|
|
|
* "table definitions need to be changed. fulltext index needs to work differently
|
|
|
|
|
* things that use the last insert id need to be changed. Probably other things
|
|
|
|
|
* need to be changed. various semantics may be different."
|
|
|
|
|
*
|
|
|
|
|
* Hashar
|
|
|
|
|
*
|
2004-09-03 23:00:01 +00:00
|
|
|
* @package MediaWiki
|
2004-09-02 23:28:24 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Depends on database
|
|
|
|
|
*/
|
2004-08-22 17:24:50 +00:00
|
|
|
require_once( 'Database.php' );
|
2004-08-06 22:30:47 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
*
|
2004-09-03 23:00:01 +00:00
|
|
|
* @package MediaWiki
|
2004-09-02 23:28:24 +00:00
|
|
|
*/
|
2004-07-10 03:09:26 +00:00
|
|
|
class DatabasePgsql extends Database {
|
|
|
|
|
var $mInsertId = NULL;
|
2004-08-20 12:46:40 +00:00
|
|
|
var $mLastResult = NULL;
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
function DatabasePgsql($server = false, $user = false, $password = false, $dbName = false,
|
2004-07-24 07:24:04 +00:00
|
|
|
$failFunction = false, $flags = 0, $tablePrefix = 'get from global' )
|
2004-06-07 02:59:58 +00:00
|
|
|
{
|
2004-07-24 07:24:04 +00:00
|
|
|
Database::Database( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
|
2004-07-24 07:24:04 +00:00
|
|
|
/* static */ function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
|
|
|
|
|
$failFunction = false, $flags = 0, $tablePrefix = 'get from global' )
|
2004-06-07 02:59:58 +00:00
|
|
|
{
|
2004-07-24 07:24:04 +00:00
|
|
|
return new DatabasePgsql( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Usually aborts on failure
|
|
|
|
|
* If the failFunction is set to a non-zero integer, returns success
|
|
|
|
|
*/
|
|
|
|
|
function open( $server, $user, $password, $dbName ) {
|
2004-07-10 03:09:26 +00:00
|
|
|
# Test for PostgreSQL support, to avoid suppressed fatal error
|
|
|
|
|
if ( !function_exists( 'pg_connect' ) ) {
|
|
|
|
|
die( "PostgreSQL functions missing, have you compiled PHP with the --with-pgsql option?\n" );
|
|
|
|
|
}
|
|
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
$this->close();
|
|
|
|
|
$this->mServer = $server;
|
|
|
|
|
$this->mUser = $user;
|
|
|
|
|
$this->mPassword = $password;
|
|
|
|
|
$this->mDBname = $dbName;
|
|
|
|
|
|
|
|
|
|
$success = false;
|
|
|
|
|
|
2004-08-22 17:24:50 +00:00
|
|
|
if ( '' != $dbName ) {
|
2004-06-07 02:59:58 +00:00
|
|
|
# start a database connection
|
|
|
|
|
@$this->mConn = pg_connect("host=$server dbname=$dbName user=$user password=$password");
|
|
|
|
|
if ( $this->mConn == false ) {
|
|
|
|
|
wfDebug( "DB connection error\n" );
|
|
|
|
|
wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
|
|
|
|
|
wfDebug( $this->lastError()."\n" );
|
2004-06-21 12:07:41 +00:00
|
|
|
} else {
|
|
|
|
|
$this->mOpened = true;
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $this->mConn;
|
|
|
|
|
}
|
|
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Closes a database connection, if it is open
|
|
|
|
|
* Returns success, true if already closed
|
|
|
|
|
*/
|
|
|
|
|
function close() {
|
2004-06-07 02:59:58 +00:00
|
|
|
$this->mOpened = false;
|
|
|
|
|
if ( $this->mConn ) {
|
|
|
|
|
return pg_close( $this->mConn );
|
|
|
|
|
} else {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2004-07-24 07:24:04 +00:00
|
|
|
function doQuery( $sql ) {
|
2004-08-20 12:46:40 +00:00
|
|
|
return $this->mLastResult=pg_query( $this->mConn , $sql);
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-07-24 07:24:04 +00:00
|
|
|
|
2004-08-22 17:24:50 +00:00
|
|
|
function queryIgnore( $sql, $fname = '' ) {
|
2004-07-10 03:09:26 +00:00
|
|
|
return $this->query( $sql, $fname, true );
|
|
|
|
|
}
|
|
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
function freeResult( $res ) {
|
|
|
|
|
if ( !@pg_free_result( $res ) ) {
|
|
|
|
|
wfDebugDieBacktrace( "Unable to free PostgreSQL result\n" );
|
|
|
|
|
}
|
|
|
|
|
}
|
2004-09-02 23:28:24 +00:00
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
function fetchObject( $res ) {
|
|
|
|
|
@$row = pg_fetch_object( $res );
|
|
|
|
|
# FIXME: HACK HACK HACK HACK debug
|
|
|
|
|
|
|
|
|
|
# TODO:
|
|
|
|
|
# hashar : not sure if the following test really trigger if the object
|
|
|
|
|
# fetching failled.
|
2004-06-10 06:37:12 +00:00
|
|
|
if( pg_last_error($this->mConn) ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
wfDebugDieBacktrace( 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
return $row;
|
|
|
|
|
}
|
2004-06-10 13:02:27 +00:00
|
|
|
|
|
|
|
|
function fetchRow( $res ) {
|
|
|
|
|
@$row = pg_fetch_array( $res );
|
|
|
|
|
if( pg_last_error($this->mConn) ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
wfDebugDieBacktrace( 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
|
2004-06-10 13:02:27 +00:00
|
|
|
}
|
|
|
|
|
return $row;
|
|
|
|
|
}
|
|
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
function numRows( $res ) {
|
|
|
|
|
@$n = pg_num_rows( $res );
|
2004-06-10 06:37:12 +00:00
|
|
|
if( pg_last_error($this->mConn) ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
wfDebugDieBacktrace( 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
return $n;
|
|
|
|
|
}
|
|
|
|
|
function numFields( $res ) { return pg_num_fields( $res ); }
|
|
|
|
|
function fieldName( $res, $n ) { return pg_field_name( $res, $n ); }
|
2004-07-10 03:09:26 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* This must be called after nextSequenceVal
|
|
|
|
|
*/
|
2004-06-07 02:59:58 +00:00
|
|
|
function insertId() {
|
2004-07-10 03:09:26 +00:00
|
|
|
return $this->mInsertId;
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
function dataSeek( $res, $row ) { return pg_result_seek( $res, $row ); }
|
2004-06-10 06:37:12 +00:00
|
|
|
function lastError() { return pg_last_error(); }
|
2004-07-24 07:24:04 +00:00
|
|
|
function lastErrno() { return 1; }
|
|
|
|
|
|
2004-06-11 14:32:05 +00:00
|
|
|
function affectedRows() {
|
|
|
|
|
return pg_affected_rows( $this->mLastResult );
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Returns information about an index
|
|
|
|
|
* If errors are explicitly ignored, returns NULL on failure
|
|
|
|
|
*/
|
|
|
|
|
function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
|
2004-06-09 16:13:46 +00:00
|
|
|
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
|
2004-07-10 03:09:26 +00:00
|
|
|
$res = $this->query( $sql, $fname );
|
2004-06-07 02:59:58 +00:00
|
|
|
if ( !$res ) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while ( $row = $this->fetchObject( $res ) ) {
|
|
|
|
|
if ( $row->Key_name == $index ) {
|
2004-07-10 03:09:26 +00:00
|
|
|
return $row;
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
return false;
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
function fieldInfo( $table, $field ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
wfDebugDieBacktrace( 'Database::fieldInfo() error : mysql_fetch_field() not implemented for postgre' );
|
2004-07-10 03:09:26 +00:00
|
|
|
/*
|
2004-06-07 02:59:58 +00:00
|
|
|
$res = $this->query( "SELECT * FROM '$table' LIMIT 1" );
|
|
|
|
|
$n = pg_num_fields( $res );
|
|
|
|
|
for( $i = 0; $i < $n; $i++ ) {
|
|
|
|
|
// FIXME
|
|
|
|
|
wfDebugDieBacktrace( "Database::fieldInfo() error : mysql_fetch_field() not implemented for postgre" );
|
|
|
|
|
$meta = mysql_fetch_field( $res, $i );
|
|
|
|
|
if( $field == $meta->name ) {
|
|
|
|
|
return $meta;
|
|
|
|
|
}
|
|
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
return false;*/
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-08-22 17:24:50 +00:00
|
|
|
function insertArray( $table, $a, $fname = 'Database::insertArray', $options = array() ) {
|
2004-07-10 03:09:26 +00:00
|
|
|
# PostgreSQL doesn't support options
|
2004-07-18 08:48:43 +00:00
|
|
|
# We have a go at faking one of them
|
|
|
|
|
# TODO: DELAYED, LOW_PRIORITY
|
|
|
|
|
|
2004-09-01 12:27:57 +00:00
|
|
|
if ( !is_array($options))
|
|
|
|
|
$options = array($options);
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'IGNORE', $options ) )
|
2004-07-24 07:24:04 +00:00
|
|
|
$oldIgnore = $this->ignoreErrors( true );
|
2004-09-01 12:27:57 +00:00
|
|
|
|
|
|
|
|
# IGNORE is performed using single-row inserts, ignoring errors in each
|
|
|
|
|
# FIXME: need some way to distiguish between key collision and other types of error
|
|
|
|
|
$oldIgnore = $this->ignoreErrors( true );
|
|
|
|
|
if ( !is_array( reset( $a ) ) ) {
|
|
|
|
|
$a = array( $a );
|
|
|
|
|
}
|
|
|
|
|
foreach ( $a as $row ) {
|
|
|
|
|
parent::insertArray( $table, $row, $fname, array() );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-09-01 12:27:57 +00:00
|
|
|
$this->ignoreErrors( $oldIgnore );
|
|
|
|
|
$retVal = true;
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'IGNORE', $options ) )
|
|
|
|
|
$this->ignoreErrors( $oldIgnore );
|
|
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
return $retVal;
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startTimer( $timeout )
|
|
|
|
|
{
|
|
|
|
|
global $IP;
|
2004-08-22 17:24:50 +00:00
|
|
|
wfDebugDieBacktrace( 'Database::startTimer() error : mysql_thread_id() not implemented for postgre' );
|
2004-07-10 03:09:26 +00:00
|
|
|
/*$tid = mysql_thread_id( $this->mConn );
|
|
|
|
|
exec( "php $IP/killthread.php $timeout $tid &>/dev/null &" );*/
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
function tableName( $name ) {
|
|
|
|
|
# First run any transformations from the parent object
|
|
|
|
|
$name = parent::tableName( $name );
|
|
|
|
|
|
|
|
|
|
# Now quote PG reserved keywords
|
|
|
|
|
switch( $name ) {
|
|
|
|
|
case 'user':
|
|
|
|
|
return '"user"';
|
|
|
|
|
case 'old':
|
|
|
|
|
return '"old"';
|
|
|
|
|
default:
|
|
|
|
|
return $name;
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
function strencode( $s ) {
|
|
|
|
|
return pg_escape_string( $s );
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Return the next in a sequence, save the value for retrieval via insertId()
|
|
|
|
|
*/
|
2004-07-10 03:09:26 +00:00
|
|
|
function nextSequenceValue( $seqName ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
$value = $this->getField(''," nextval('" . $seqName . "')");
|
2004-07-10 03:09:26 +00:00
|
|
|
$this->mInsertId = $value;
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* USE INDEX clause
|
|
|
|
|
* PostgreSQL doesn't have them and returns ""
|
|
|
|
|
*/
|
2004-07-10 03:09:26 +00:00
|
|
|
function useIndexClause( $index ) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
# REPLACE query wrapper
|
|
|
|
|
# PostgreSQL simulates this with a DELETE followed by INSERT
|
|
|
|
|
# $row is the row to insert, an associative array
|
|
|
|
|
# $uniqueIndexes is an array of indexes. Each element may be either a
|
|
|
|
|
# field name or an array of field names
|
|
|
|
|
#
|
|
|
|
|
# It may be more efficient to leave off unique indexes which are unlikely to collide.
|
|
|
|
|
# However if you do this, you run the risk of encountering errors which wouldn't have
|
|
|
|
|
# occurred in MySQL
|
2004-08-22 17:24:50 +00:00
|
|
|
function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
|
2004-07-10 03:09:26 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
|
2004-07-18 08:48:43 +00:00
|
|
|
# Single row case
|
|
|
|
|
if ( !is_array( reset( $rows ) ) ) {
|
|
|
|
|
$rows = array( $rows );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach( $rows as $row ) {
|
|
|
|
|
# Delete rows which collide
|
|
|
|
|
if ( $uniqueIndexes ) {
|
|
|
|
|
$sql = "DELETE FROM $table WHERE (";
|
|
|
|
|
$first = true;
|
|
|
|
|
foreach ( $uniqueIndexes as $index ) {
|
|
|
|
|
if ( $first ) {
|
|
|
|
|
$first = false;
|
|
|
|
|
} else {
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= ') OR (';
|
2004-07-15 15:09:32 +00:00
|
|
|
}
|
2004-07-18 08:48:43 +00:00
|
|
|
if ( is_array( $index ) ) {
|
|
|
|
|
$first2 = true;
|
|
|
|
|
$sql .= "(";
|
|
|
|
|
foreach ( $index as $col ) {
|
|
|
|
|
if ( $first2 ) {
|
|
|
|
|
$first2 = false;
|
|
|
|
|
} else {
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= ' AND ';
|
2004-07-18 08:48:43 +00:00
|
|
|
}
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= $col.'=' . $this->addQuotes( $row[$col] );
|
2004-07-18 08:48:43 +00:00
|
|
|
}
|
2004-07-10 03:09:26 +00:00
|
|
|
} else {
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= $index.'=' . $this->addQuotes( $row[$index] );
|
2004-07-10 03:09:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= ')';
|
2004-07-18 08:48:43 +00:00
|
|
|
$this->query( $sql, $fname );
|
2004-07-10 03:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
2004-07-18 08:48:43 +00:00
|
|
|
# Now insert the row
|
|
|
|
|
$sql = "INSERT INTO $table (" . $this->makeList( array_flip( $row ) ) .') VALUES (' .
|
|
|
|
|
$this->makeList( $row, LIST_COMMA ) . ')';
|
|
|
|
|
$this->query( $sql, $fname );
|
2004-07-10 03:09:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# DELETE where the condition is a join
|
|
|
|
|
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
|
|
|
|
|
if ( !$conds ) {
|
|
|
|
|
wfDebugDieBacktrace( 'Database::deleteJoin() called with empty $conds' );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
$delTable = $this->tableName( $delTable );
|
|
|
|
|
$joinTable = $this->tableName( $joinTable );
|
|
|
|
|
$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
|
|
|
|
|
if ( $conds != '*' ) {
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
2004-08-22 17:24:50 +00:00
|
|
|
$sql .= ')';
|
2004-07-10 03:09:26 +00:00
|
|
|
|
|
|
|
|
$this->query( $sql, $fname );
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-07-10 03:09:26 +00:00
|
|
|
# Returns the size of a text field, or -1 for "unlimited"
|
|
|
|
|
function textFieldSize( $table, $field ) {
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$res = $this->query( "SELECT $field FROM $table LIMIT 1", "Database::textFieldLength" );
|
|
|
|
|
$size = pg_field_size( $res, 0 );
|
|
|
|
|
$this->freeResult( $res );
|
|
|
|
|
return $size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lowPriorityOption() {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2004-07-15 15:09:32 +00:00
|
|
|
|
|
|
|
|
function limitResult($limit,$offset) {
|
|
|
|
|
return " LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
|
|
|
|
|
}
|
2004-07-18 08:48:43 +00:00
|
|
|
|
|
|
|
|
# FIXME: actually detecting deadlocks might be nice
|
|
|
|
|
function wasDeadlock() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2004-08-10 11:12:18 +00:00
|
|
|
|
|
|
|
|
# Return DB-style timestamp used for MySQL schema
|
|
|
|
|
function timestamp( $ts=0 ) {
|
|
|
|
|
return wfTimestamp(TS_DB,$ts);
|
|
|
|
|
}
|
2004-08-19 13:00:34 +00:00
|
|
|
|
|
|
|
|
function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
|
|
|
|
|
$message = "A database error has occurred\n" .
|
|
|
|
|
"Query: $sql\n" .
|
|
|
|
|
"Function: $fname\n" .
|
|
|
|
|
"Error: $errno $error\n";
|
|
|
|
|
wfDebugDieBacktrace($message);
|
|
|
|
|
}
|
2004-06-07 02:59:58 +00:00
|
|
|
}
|
|
|
|
|
|
2004-09-02 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Just an alias.
|
2004-09-03 23:00:01 +00:00
|
|
|
* @package MediaWiki
|
2004-09-02 23:28:24 +00:00
|
|
|
*/
|
2004-07-15 15:09:32 +00:00
|
|
|
class DatabasePostgreSQL extends DatabasePgsql {
|
2004-06-11 14:32:05 +00:00
|
|
|
}
|
|
|
|
|
|
2004-06-07 02:59:58 +00:00
|
|
|
?>
|