PostgreSQL install fixes:
authorTim Starling <tstarling@users.mediawiki.org>
Fri, 10 Jun 2011 11:32:57 +0000 (11:32 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Fri, 10 Jun 2011 11:32:57 +0000 (11:32 +0000)
* Made PG throw a DBQueryError when it gets a query error, instead of DBUnexpectedError. Apparently this mistake goes back to r14625, when exceptions were first introduced. Did it by removing reportQueryError(), the DatabaseBase version works fine.
* Fixed several places where there was an attempt to check for a query error by checking if the result of query() was false. This never worked. Used try/catch instead.
* Made the DBConnectionError messages go on one line so that they don't mess up the formatting in the installer.
* In DatabasePostgres::selectDB(), only disconnect and reconnect if the DB name is actually changing.
* Made DatabasePostgres::schemaExists() less weird and scary.
* Added DatabasePostgres::roleExists() for use by the installer.
* Removed the PostgreSQL-specific hack to make _InstallUser have a default other than "root". Made _InstallUser into a proper DBMS-specific internal variable instead, since every DBMS we support so far needs a different default.
* Removed the $dbName parameters from openConnection/getConnection, and got rid of $this->useAdmin. Implemented a more sophisticated caching scheme instead. Partial revert of r89389 and r81440.
* When connecting as the install user before DB creation, and when testing the web user's credentials, try a few different database names and use whichever one works.
* Instead of connecting as the web user to create tables, I used SET ROLE. It seems cleaner and more like what the other DBMSes do during installation. "SET ROLE wikiuser" requires the same privileges as "CREATE SCHEMA ... AUTHORIZATION wikiuser", so it's unlikely to break anything.
* In the area of web account creation, fixed various minor logic errors and introduced more informative error messages at the submit stage, pre-install. Show a helpful error message if the web user exists already and the install user can't do the relevant SET ROLE.
* Split schema creation out to a separate install step.
* When creating an account as a non-superuser, add the administrative account to the new account's group. This is necessary to avoid a fatal error during installation (bug 28845).
* Removed code which alters an existing web user to have appropriate search paths and permissions. This may break other apps and is not necessary. As in other DBMSes, If the web user exists, it is the responsibility of the sysadmin to ensure that it has appropriate permissions.
* Rewrote setupPLpgSQL() to use the query builder functions.

includes/db/DatabasePostgres.php
includes/installer/DatabaseInstaller.php
includes/installer/Ibm_db2Installer.php
includes/installer/Installer.i18n.php
includes/installer/Installer.php
includes/installer/MysqlInstaller.php
includes/installer/OracleInstaller.php
includes/installer/PostgresInstaller.php
includes/installer/SqliteInstaller.php

index 94c0b80..4f85ba7 100644 (file)
@@ -186,7 +186,7 @@ class DatabasePostgres extends DatabaseBase {
                        wfDebug( "DB connection error\n" );
                        wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
                        wfDebug( $this->lastError() . "\n" );
-                       throw new DBConnectionError( $this, $phpError );
+                       throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
                }
 
                $this->mOpened = true;
@@ -218,7 +218,11 @@ class DatabasePostgres extends DatabaseBase {
         * @return
         */
        function selectDB( $db ) {
-               return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+               if ( $this->mDBname !== $db ) {
+                       return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+               } else {
+                       return true;
+               }
        }
 
        function makeConnectionString( $vars ) {
@@ -762,23 +766,6 @@ class DatabasePostgres extends DatabaseBase {
                return $valuedata;
        }
 
-       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               // Ignore errors during error handling to avoid infinite recursion
-               $ignore = $this->ignoreErrors( true );
-               $this->mErrorCount++;
-
-               if ( $ignore || $tempIgnore ) {
-                       wfDebug( "SQL ERROR (ignored): $error\n" );
-                       $this->ignoreErrors( $ignore );
-               } else {
-                       $message = "A database error has occurred.  Did you forget to run maintenance/update.php after upgrading?  See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
-                               "Query: $sql\n" .
-                               "Function: $fname\n" .
-                               "Error: $errno $error\n";
-                       throw new DBUnexpectedError( $this, $message );
-               }
-       }
-
        /**
         * @return string wikitext of a link to the server software's web site
         */
@@ -894,20 +881,21 @@ SQL;
        }
 
        /**
-        * Query whether a given schema exists. Returns the name of the owner
+        * Query whether a given schema exists. Returns true if it does, false if it doesn't.
         */
        function schemaExists( $schema ) {
-               $eschema = str_replace( "'", "''", $schema );
-               $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
-                               ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
-               $res = $this->query( $SQL );
-               if ( $res && $res->numRows() ) {
-                       $row = $res->fetchObject();
-                       $owner = $row->rolname;
-               } else {
-                       $owner = false;
-               }
-               return $owner;
+               $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
+                       array( 'nspname' => $schema ), __METHOD__ );
+               return (bool)$exists;
+       }
+
+       /**
+        * Returns true if a given role (i.e. user) exists, false otherwise.
+        */
+       function roleExists( $roleName ) {
+               $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
+                       array( 'rolname' => $roleName ), __METHOD__ );
+               return (bool)$exists;
        }
 
        function fieldInfo( $table, $field ) {
index 87097b3..a6778c3 100644 (file)
@@ -102,7 +102,7 @@ abstract class DatabaseInstaller {
         *
         * @return Status
         */
-       public abstract function openConnection( $dbName = null );
+       public abstract function openConnection();
 
        /**
         * Create the database and return a Status object indicating success or
@@ -121,14 +121,12 @@ abstract class DatabaseInstaller {
         *
         * @return Status
         */
-       public function getConnection( $dbName = null ) {
-               if ( isset($this->db) && $this->db ) { /* Weirdly get E_STRICT
-                                                                                               * errors without the
-                                                                                               * isset */
+       public function getConnection() {
+               if ( $this->db ) {
                        return Status::newGood( $this->db );
                }
 
-               $status = $this->openConnection( $dbName );
+               $status = $this->openConnection();
                if ( $status->isOK() ) {
                        $this->db = $status->value;
                        // Enable autocommit
index c817aa1..246d590 100644 (file)
@@ -24,6 +24,10 @@ class Ibm_db2Installer extends DatabaseInstaller {
                'wgDBmwschema',
        );
 
+       protected $internalDefaults = array(
+               '_InstallUser' => 'db2admin'
+       );
+
        /**
         * Get the DB2 database extension name
         * @return string
@@ -113,7 +117,7 @@ class Ibm_db2Installer extends DatabaseInstaller {
         * Open a DB2 database connection
         * @return Status
         */
-       public function openConnection( $dbName = null ) {
+       public function openConnection() {
                $status = Status::newGood();
                try {
                        $db = new DatabaseIbm_db2(
@@ -244,4 +248,4 @@ class Ibm_db2Installer extends DatabaseInstaller {
        public function __construct($parent) {
                parent::__construct($parent);
        }
-}
\ No newline at end of file
+}
index 6a198e9..8e83070 100644 (file)
@@ -211,6 +211,7 @@ but it will not let you store characters above the [http://en.wikipedia.org/wiki
        'config-db-schema'                => 'Schema for MediaWiki',
        'config-db-schema-help'           => 'This schema will usually be fine.
 Only change it if you know you need to.',
+       'config-pg-test-error'            => "Cannot connect to database '''$1''': $2",
        'config-sqlite-dir'               => 'SQLite data directory:',
        'config-sqlite-dir-help'          => "SQLite stores all data in a single file.
 
@@ -486,6 +487,7 @@ If you still want to make changes, press back.',
        'config-install-step-failed'      => 'failed',
        'config-install-extensions'       => 'Including extensions',
        'config-install-database'         => 'Setting up database',
+       'config-install-schema'           => 'Creating schema',
        'config-install-pg-schema-not-exist' => 'PostgreSQL schema does not exist.',
        'config-install-pg-schema-failed' => 'Tables creation failed.
 Make sure that the user "$1" can write to the schema "$2".',
@@ -493,10 +495,17 @@ Make sure that the user "$1" can write to the schema "$2".',
        'config-install-pg-plpgsql'       => 'Checking for language PL/pgSQL',
        'config-pg-no-plpgsql'            => 'You need to install the language PL/pgSQL in the database $1',
        'config-pg-no-create-privs'       => 'The account you specified for installation does not have enough privileges to create an account.',
+       'config-pg-not-in-role'           => 'The account you specified for the web user already exists.
+The account you specified for installation is not a superuser and is not a member of the web user\'s role, so it is unable to create objects owned by the web user.
+
+MediaWiki currently requires that the tables be owned by the web user. Please specify another web account name, or click "back" and specify a suitably privileged install user.',
        'config-install-user'             => 'Creating database user',
        'config-install-user-alreadyexists' => 'User "$1" already exists',
        'config-install-user-create-failed' => 'Creating user "$1" failed: $2',
        'config-install-user-grant-failed'  => 'Granting permission to user "$1" failed: $2',
+       'config-install-user-missing'     => 'The specified user "$1" does not exist.',
+       'config-install-user-missing-create' => 'The specified user "$1" does not exist.
+Please click the "create account" checkbox below if you want to create it.',
        'config-install-tables'           => 'Creating tables',
        'config-install-tables-exist'     => "'''Warning''': MediaWiki tables seem to already exist.
 Skipping creation.",
index 008b071..55de557 100644 (file)
@@ -161,7 +161,6 @@ abstract class Installer {
                '_UpgradeDone' => false,
                '_InstallDone' => false,
                '_Caches' => array(),
-               '_InstallUser' => 'root',
                '_InstallPassword' => '',
                '_SameAccount' => true,
                '_CreateDBAccount' => false,
index 6603a0c..3bb8c11 100644 (file)
@@ -27,6 +27,7 @@ class MysqlInstaller extends DatabaseInstaller {
        protected $internalDefaults = array(
                '_MysqlEngine' => 'InnoDB',
                '_MysqlCharset' => 'binary',
+               '_InstallUser' => 'root',
        );
 
        public $supportedEngines = array( 'InnoDB', 'MyISAM' );
@@ -126,7 +127,7 @@ class MysqlInstaller extends DatabaseInstaller {
        /**
         * @return Status
         */
-       public function openConnection( $dbName = null ) {
+       public function openConnection() {
                $status = Status::newGood();
                try {
                        $db = new DatabaseMysql(
index 6b3f78e..175baf0 100644 (file)
@@ -24,7 +24,8 @@ class OracleInstaller extends DatabaseInstaller {
 
        protected $internalDefaults = array(
                '_OracleDefTS' => 'USERS',
-               '_OracleTempTS' => 'TEMP'
+               '_OracleTempTS' => 'TEMP',
+               '_InstallUser' => 'SYSDBA',
        );
 
        public $minimumVersion = '9.0.1'; // 9iR1
@@ -127,7 +128,7 @@ class OracleInstaller extends DatabaseInstaller {
                return $status;
        }
 
-       public function openConnection( $dbName = null ) {
+       public function openConnection() {
                $status = Status::newGood();
                try {
                        $db = new DatabaseOracle(
index f927c01..501f673 100644 (file)
@@ -23,8 +23,14 @@ class PostgresInstaller extends DatabaseInstaller {
                'wgDBmwschema',
        );
 
+       protected $internalDefaults = array(
+               '_InstallUser' => 'postgres',
+       );
+
        var $minimumVersion = '8.3';
-       private $useAdmin = false;
+       var $maxRoleSearchDepth = 5;
+
+       protected $pgConns = array();
 
        function getName() {
                return 'postgres';
@@ -35,11 +41,6 @@ class PostgresInstaller extends DatabaseInstaller {
        }
 
        function getConnectForm() {
-               // If this is our first time here, switch the default user presented in the form
-               if ( ! $this->getVar('_switchedInstallUser') ) {
-                       $this->setVar('_InstallUser', 'postgres');
-                       $this->setVar('_switchedInstallUser', true);
-               }
                return
                        $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
                        $this->getTextBox( 'wgDBport', 'config-db-port' ) .
@@ -75,87 +76,185 @@ class PostgresInstaller extends DatabaseInstaller {
                        return $status;
                }
 
-               $this->useAdmin = true;
-               // Try to connect
-               $status->merge( $this->getConnection() );
-               if ( !$status->isOK() ) {
-                       return $status;
-               }
-
-               //Make sure install user can create
-               if( !$this->canCreateAccounts() ) {
-                       $status->fatal( 'config-pg-no-create-privs' );
-               }
+               $status = $this->getPgConnection( 'create-db' );
                if ( !$status->isOK() ) {
                        return $status;
                }
+               $conn = $status->value;
 
                // Check version
-               $version = $this->db->getServerVersion();
+               $version = $conn->getServerVersion();
                if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
                        return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version );
                }
 
                $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
                $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
+               return Status::newGood();
+       }
+
+       public function getConnection() {
+               $status = $this->getPgConnection( 'create-tables' );
+               if ( $status->isOK() ) {
+                       $this->db = $status->value;
+               }
                return $status;
        }
 
-       public function openConnection( $dbName = null ) {
+       public function openConnection() {
+               return $this->openPgConnection( 'create-tables' );
+       }
+
+       /**
+        * Open a PG connection with given parameters
+        * @param $user User name
+        * @param $password Password
+        * @param $dbName Database name
+        * @return Status
+        */
+       protected function openConnectionWithParams( $user, $password, $dbName ) {
                $status = Status::newGood();
                try {
-                       if ( $this->useAdmin ) {
-                               if ( $dbName === null ) $dbName = 'postgres';
+                       $db = new DatabasePostgres(
+                               $this->getVar( 'wgDBserver' ),
+                               $user,
+                               $password,
+                               $dbName);
+                       $status->value = $db;
+               } catch ( DBConnectionError $e ) {
+                       $status->fatal( 'config-connection-error', $e->getMessage() );
+               }
+               return $status;
+       }
 
-                               $db = new DatabasePostgres(
-                                       $this->getVar( 'wgDBserver' ),
+       /**
+        * Get a special type of connection
+        * @param $type See openPgConnection() for details.
+        * @return Status
+        */
+       protected function getPgConnection( $type ) {
+               if ( isset( $this->pgConns[$type] ) ) {
+                       return Status::newGood( $this->pgConns[$type] );
+               }
+               $status = $this->openPgConnection( $type );
+
+               if ( $status->isOK() ) {
+                       $conn = $status->value;
+                       $conn->clearFlag( DBO_TRX );
+                       $conn->commit();
+                       $this->pgConns[$type] = $conn;
+               }
+               return $status;
+       }
+
+       /**
+        * Get a connection of a specific PostgreSQL-specific type. Connections
+        * of a given type are cached.
+        *
+        * PostgreSQL lacks cross-database operations, so after the new database is 
+        * created, you need to make a separate connection to connect to that 
+        * database and add tables to it. 
+        *
+        * New tables are owned by the user that creates them, and MediaWiki's 
+        * PostgreSQL support has always assumed that the table owner will be 
+        * $wgDBuser. So before we create new tables, we either need to either 
+        * connect as the other user or to execute a SET ROLE command. Using a 
+        * separate connection for this allows us to avoid accidental cross-module 
+        * dependencies.
+        *
+        * @param $type The type of connection to get:
+        *    - create-db:     A connection for creating DBs, suitable for pre-
+        *                     installation.
+        *    - create-schema: A connection to the new DB, for creating schemas and 
+        *                     other similar objects in the new DB.
+        *    - create-tables: A connection with a role suitable for creating tables.
+        *
+        * @return A Status object. On success, a connection object will be in the 
+        *   value member.
+        */
+       protected function openPgConnection( $type ) {
+               switch ( $type ) {
+                       case 'create-db':
+                               return $this->openConnectionToAnyDB(
+                                       $this->getVar( '_InstallUser' ), 
+                                       $this->getVar( '_InstallPassword' ) );
+                       case 'create-schema':
+                               return $this->openConnectionWithParams( 
                                        $this->getVar( '_InstallUser' ),
                                        $this->getVar( '_InstallPassword' ),
-                                       $dbName );
-                       } else {
-                               if ( $dbName === null ) $dbName = $this->getVar( 'wgDBname' );
+                                       $this->getVar( 'wgDBname' ) );
+                       case 'create-tables':
+                               $status = $this->openPgConnection( 'create-schema' );
+                               if ( $status->isOK() ) {
+                                       $conn = $status->value;
+                                       $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
+                                       $conn->query( "SET ROLE $safeRole" );
+                               }
+                               return $status;
+                       default:
+                               throw new MWException( "Invalid special connection type: \"$type\"" );
+               }
+       }
 
-                               $db = new DatabasePostgres(
+       public function openConnectionToAnyDB( $user, $password ) {
+               $dbs = array(
+                       'template1',
+                       'postgres',
+               );
+               if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
+                       array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
+               }
+               $status = Status::newGood();
+               foreach ( $dbs as $db ) {
+                       try {
+                               $conn = new DatabasePostgres(
                                        $this->getVar( 'wgDBserver' ),
-                                       $this->getVar( 'wgDBuser' ),
-                                       $this->getVar( 'wgDBpassword' ),
-                                       $dbName );
+                                       $user,
+                                       $password,
+                                       $db );
+                       } catch ( DBConnectionError $error ) {
+                               $conn = false;
+                               $status->fatal( 'config-pg-test-error', $db,
+                                       $error->getMessage() );
+                       }
+                       if ( $conn !== false ) {
+                               break;
                        }
-
-                       if( $db === null ) throw new DBConnectionError("Unknown problem while connecting.");
-                       $safeschema = $db->addIdentifierQuotes( $this->getVar( 'wgDBmwschema' ) );
-                       if( $db->schemaExists( $this->getVar( 'wgDBmwschema' ) ) ) $db->query( "SET search_path = $safeschema" );
-
-                       $status->value = $db;
-               } catch ( DBConnectionError $e ) {
-                       $status->fatal( 'config-connection-error', $e->getMessage() );
                }
-               return $status;
+               if ( $conn !== false ) {
+                       return Status::newGood( $conn );
+               } else {
+                       return $status;
+               }
        }
 
-       protected function canCreateAccounts() {
-               $this->useAdmin = true;
-               $status = $this->getConnection();
+       protected function getInstallUserPermissions() {
+               $status = $this->getPgConnection( 'create-db' );
                if ( !$status->isOK() ) {
                        return false;
                }
                $conn = $status->value;
-
                $superuser = $this->getVar( '_InstallUser' );
 
-               $rights = $conn->selectField( 'pg_catalog.pg_roles',
-                       'CASE WHEN rolsuper then 1
-                                 WHEN rolcreatedb then 2
-                                 ELSE 3
-                        END as rights',
-                       array( 'rolname' => $superuser ), __METHOD__
-               );
+               $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*', 
+                       array( 'rolname' => $superuser ), __METHOD__ );
+               return $row;
+       }
 
-               if( !$rights || $rights == 3 ) {
+       protected function canCreateAccounts() {
+               $perms = $this->getInstallUserPermissions();
+               if ( !$perms ) {
                        return false;
                }
+               return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
+       }
 
-               return true;
+       protected function isSuperUser() {
+               $perms = $this->getInstallUserPermissions();
+               if ( !$perms ) {
+                       return false;
+               }
+               return $perms->rolsuper === 't';                
        }
 
        public function getSettingsForm() {
@@ -175,26 +274,110 @@ class PostgresInstaller extends DatabaseInstaller {
                        return $status;
                }
 
+               $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
+
+               if ( !$same ) {
+                       // Check if the web user exists
+                       // Connect to the database with the install user
+                       $status = $this->getPgConnection( 'create-db' );
+                       if ( !$status->isOK() ) {
+                               return $status;
+                       }
+                       $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
+               }
+
                // Validate the create checkbox
-               if ( !$this->canCreateAccounts() ) {
+               if ( $this->canCreateAccounts() && !$same && !$exists ) {
+                       $create = $this->getVar( '_CreateDBAccount' );
+               } else {
                        $this->setVar( '_CreateDBAccount', false );
                        $create = false;
-               } else {
-                       $create = $this->getVar( '_CreateDBAccount' );
                }
 
-               // Don't test the web account if it is the same as the admin.
-               if ( !$create && $this->getVar( 'wgDBuser' ) != $this->getVar( '_InstallUser' ) ) {
-                       // Test the web account
-                       try {
-                               $this->useAdmin = false;
-                               return $this->openConnection();
-                       } catch ( DBConnectionError $e ) {
-                               return Status::newFatal( 'config-connection-error', $e->getMessage() );
+               if ( !$create && !$exists ) {
+                       if ( $this->canCreateAccounts() ) {
+                               $msg = 'config-install-user-missing-create';
+                       } else {
+                               $msg = 'config-install-user-missing';
                        }
+                       return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
                }
 
-               return Status::newGood();
+               if ( !$exists ) {
+                       // No more checks to do
+                       return Status::newGood();
+               }
+
+               // Existing web account. Test the connection.
+               $status = $this->openConnectionToAnyDB( 
+                       $this->getVar( 'wgDBuser' ),
+                       $this->getVar( 'wgDBpassword' ) );
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               // The web user is conventionally the table owner in PostgreSQL 
+               // installations. Make sure the install user is able to create 
+               // objects on behalf of the web user.
+               if ( $this->canCreateObjectsForWebUser() ) {
+                       return Status::newGood();
+               } else {
+                       return Status::newFatal( 'config-pg-not-in-role' );
+               }
+       }
+
+       /**
+        * Returns true if the install user is able to create objects owned
+        * by the web user, false otherwise.
+        */
+       protected function canCreateObjectsForWebUser() {
+               if ( $this->isSuperUser() ) {
+                       return true;
+               }
+
+               $status = $this->getPgConnection( 'create-db' );
+               if ( !$status->isOK() ) {
+                       return false;
+               }
+               $conn = $status->value;
+               $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
+                       array( 'rolname' => $this->getVar( '_InstallUser' ) ), __METHOD__ );
+               $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
+                       array( 'rolname' => $this->getVar( 'wgDBuser' ) ), __METHOD__ );
+
+               return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
+       }
+
+       /**
+        * Recursive helper for canCreateObjectsForWebUser().
+        * @param $conn Database object
+        * @param $targetMember Role ID of the member to look for
+        * @param $group Role ID of the group to look for
+        * @param $maxDepth Maximum recursive search depth
+        */
+       protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
+               if ( $targetMember === $group ) {
+                       // A role is always a member of itself
+                       return true;
+               }
+               // Get all members of the given group
+               $res = $conn->select( '"pg_catalog"."pg_auth_members"', array( 'member' ),
+                       array( 'roleid' => $group ), __METHOD__ );
+               foreach ( $res as $row ) {
+                       if ( $row->member == $targetMember ) {
+                               // Found target member
+                               return true;
+                       }
+                       // Recursively search each member of the group to see if the target
+                       // is a member of it, up to the given maximum depth.
+                       if ( $maxDepth > 0 ) {
+                               if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) {
+                                       // Found member of member
+                                       return true;
+                               }
+                       }
+               }
+               return false;
        }
 
        public function preInstall() {
@@ -206,8 +389,13 @@ class PostgresInstaller extends DatabaseInstaller {
                        'name' => 'pg-plpgsql',
                        'callback' => array( $this, 'setupPLpgSQL' ),
                );
+               $schemaCB = array(
+                       'name' => 'schema',
+                       'callback' => array( $this, 'setupSchema' )
+               );
                $this->parent->addInstallStep( $commitCB, 'interwiki' );
                $this->parent->addInstallStep( $plpgCB, 'database' );
+               $this->parent->addInstallStep( $schemaCB, 'database' );
                if( $this->getVar( '_CreateDBAccount' ) ) {
                        $this->parent->addInstallStep( array(
                                'name' => 'user',
@@ -217,12 +405,10 @@ class PostgresInstaller extends DatabaseInstaller {
        }
 
        function setupDatabase() {
-               $this->useAdmin = true;
-               $status = $this->getConnection();
+               $status = $this->getPgConnection( 'create-db' );
                if ( !$status->isOK() ) {
                        return $status;
                }
-               $this->setupSchemaVars();
                $conn = $status->value;
 
                $dbName = $this->getVar( 'wgDBname' );
@@ -231,46 +417,45 @@ class PostgresInstaller extends DatabaseInstaller {
                $safeschema = $conn->addIdentifierQuotes( $schema );
                $safeuser = $conn->addIdentifierQuotes( $user );
 
-               $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $conn->addQuotes( $dbName );
-               $rows = $conn->numRows( $conn->query( $SQL ) );
-               $safedb = $conn->addIdentifierQuotes( $dbName );
-               if( !$rows ) {
+               $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
+                       array( 'datname' => $dbName ), __METHOD__ );
+               if ( !$exists ) {
+                       $safedb = $conn->addIdentifierQuotes( $dbName );
                        $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
-                       $conn->query( "GRANT ALL ON DATABASE $safedb to $safeuser", __METHOD__ );
-               } else {
-                       $conn->query( "GRANT ALL ON DATABASE $safedb TO $safeuser", __METHOD__ );
                }
+               return Status::newGood();
+       }
 
-               // Now that we've established the real database exists, connect to it
-               // Because we do not want the same connection, forcibly expire the existing conn
-               $this->db = null;
-               $this->useAdmin = false;
-               $status = $this->getConnection();
+       function setupSchema() {
+               // Get a connection to the target database
+               $status = $this->getPgConnection( 'create-schema' );
                if ( !$status->isOK() ) {
                        return $status;
                }
                $conn = $status->value;
 
+               // Create the schema if necessary
+               $schema = $this->getVar( 'wgDBmwschema' );
+               $safeschema = $conn->addIdentifierQuotes( $schema );
+               $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
                if( !$conn->schemaExists( $schema ) ) {
-                       $result = $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
-                       if( !$result ) {
-                               $status->fatal( 'config-install-pg-schema-failed', $user, $schema );
+                       try {
+                               $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
+                       } catch ( DBQueryError $e ) {
+                               return Status::newFatal( 'config-install-pg-schema-failed', 
+                                       $this->getVar( '_InstallUser' ), $schema );
                        }
-               } else {
-                       $safeschema2 = $conn->addQuotes( $schema );
-                       $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
-                               "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n" .
-                               "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n" .
-                               "AND p.relkind IN ('r','S','v')\n";
-                       $SQL .= "UNION\n";
-                       $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
-                               "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n" .
-                               "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n" .
-                               "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
-                       $conn->query( "SET search_path = $safeschema" );
-                       $res = $conn->query( $SQL );
                }
-               return $status;
+
+               // If we created a user, alter it now to search the new schema by default
+               if ( $this->getVar( '_CreateDBAccount' ) ) {
+                       $conn->query( "ALTER ROLE $safeuser SET search_path = $safeschema, public", 
+                               __METHOD__ );
+               }
+
+               // Select the new schema in the current connection
+               $conn->query( "SET search_path = $safeschema" );
+               return Status::newGood();
        }
 
        function commitChanges() {
@@ -283,34 +468,39 @@ class PostgresInstaller extends DatabaseInstaller {
                        return Status::newGood();
                }
 
-               $this->useAdmin = true;
-               $status = $this->getConnection();
-
+               $status = $this->getPgConnection( 'create-db' );
                if ( !$status->isOK() ) {
                        return $status;
                }
+               $conn = $status->value;
 
                $schema = $this->getVar( 'wgDBmwschema' );
-               $safeuser = $this->db->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
-               $safeusercheck = $this->db->addQuotes( $this->getVar( 'wgDBuser' ) );
-               $safepass = $this->db->addQuotes( $this->getVar( 'wgDBpassword' ) );
-               $safeschema = $this->db->addIdentifierQuotes( $schema );
+               $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
+               $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
+               $safeschema = $conn->addIdentifierQuotes( $schema );
 
-               $rows = $this->db->numRows(
-                       $this->db->query( "SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $safeusercheck" )
-               );
-               if ( $rows < 1 ) {
-                       $res = $this->db->query( "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass", __METHOD__ );
-                       if ( $res !== true && !( $res instanceOf ResultWrapper ) ) {
-                               $status->fatal( 'config-install-user-failed', $this->getVar( 'wgDBuser' ), $res );
-                       }
-                       if( $status->isOK() ) {
-                               $this->db->query("ALTER ROLE $safeuser LOGIN");
+               // Check if the user already exists
+               $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
+               if ( !$userExists ) {
+                       // Create the user
+                       try {
+                               $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
+                               
+                               // If the install user is not a superuser, we need to make the install 
+                               // user a member of the new user's group, so that the install user will
+                               // be able to create a schema and other objects on behalf of the new user.
+                               if ( !$this->isSuperUser() ) {
+                                       $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
+                               }
+
+                               $conn->query( $sql, __METHOD__ );
+                       } catch ( DBQueryError $e ) {
+                               return Status::newFatal( 'config-install-user-create-failed', 
+                                       $this->getVar( 'wgDBuser' ), $e->getMessage() );
                        }
                }
-               $this->db->query("ALTER ROLE $safeuser SET search_path = $safeschema, public");
 
-               return $status;
+               return Status::newGood();
        }
 
        function getLocalSettings() {
@@ -334,32 +524,30 @@ class PostgresInstaller extends DatabaseInstaller {
        public function createTables() {
                $schema = $this->getVar( 'wgDBmwschema' );
 
-               $this->db = null;
-               $this->useAdmin = false;
                $status = $this->getConnection();
                if ( !$status->isOK() ) {
                        return $status;
                }
+               $conn = $status->value;
 
-               if( $this->db->tableExists( 'user' ) ) {
+               if( $conn->tableExists( 'user' ) ) {
                        $status->warning( 'config-install-tables-exist' );
                        return $status;
                }
 
-               $this->db->begin( __METHOD__ );
+               $conn->begin( __METHOD__ );
 
-               // getConnection() should have already selected the schema if it exists
-               if( !$this->db->schemaExists( $schema ) ) {
-                       $status->error( 'config-install-pg-schema-not-exist' );
+               if( !$conn->schemaExists( $schema ) ) {
+                       $status->fatal( 'config-install-pg-schema-not-exist' );
                        return $status;
                }
-               $error = $this->db->sourceFile( $this->db->getSchema() );
+               $error = $conn->sourceFile( $conn->getSchema() );
                if( $error !== true ) {
-                       $this->db->reportQueryError( $error, 0, '', __METHOD__ );
-                       $this->db->rollback( __METHOD__ );
+                       $conn->reportQueryError( $error, 0, '', __METHOD__ );
+                       $conn->rollback( __METHOD__ );
                        $status->fatal( 'config-install-tables-failed', $error );
                } else {
-                       $this->db->commit( __METHOD__ );
+                       $conn->commit( __METHOD__ );
                }
                // Resume normal operations
                if( $status->isOk() ) {
@@ -369,34 +557,40 @@ class PostgresInstaller extends DatabaseInstaller {
        }
 
        public function setupPLpgSQL() {
-               $this->db = null;
-               $this->useAdmin = true;
-               $dbName = $this->getVar( 'wgDBname' );
-               $status = $this->getConnection( $dbName );
+               // Connect as the install user, since it owns the database and so is 
+               // the user that needs to run "CREATE LANGAUGE"
+               $status = $this->getPgConnection( 'create-schema' );
                if ( !$status->isOK() ) {
                        return $status;
                }
-               $this->db = $status->value;
+               $conn = $status->value;
+
+               $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1,
+                       array( 'lanname' => 'plpgsql' ), __METHOD__ );
+               if ( $exists ) {
+                       // Already exists, nothing to do
+                       return Status::newGood();
+               }
 
-               /* Admin user has to be connected to the db it just
-                  created to satisfy ownership requirements for
-                  "CREATE LANGAUGE" */
-               $rows = $this->db->numRows(
-                       $this->db->query( "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'" )
-               );
-               if ( $rows < 1 ) {
-                       // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
-                       $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
-                               "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
-                       $rows = $this->db->numRows( $this->db->query( $SQL ) );
-                       if ( $rows >= 1 ) {
-                               $result = $this->db->query( 'CREATE LANGUAGE plpgsql' );
-                               if ( !$result ) {
-                                       return Status::newFatal( 'config-pg-no-plpgsql', $dbName );
-                               }
-                       } else {
-                               return Status::newFatal( 'config-pg-no-plpgsql', $dbName );
+               // plpgsql is not installed, but if we have a pg_pltemplate table, we 
+               // should be able to create it
+               $exists = $conn->selectField(
+                       array( '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ),
+                       1,
+                       array(
+                               'pg_namespace.oid=relnamespace',
+                               'nspname' => 'pg_catalog',
+                               'relname' => 'pg_pltemplate',
+                       ),
+                       __METHOD__ );
+               if ( $exists ) {
+                       try {
+                               $conn->query( 'CREATE LANGUAGE plpgsql' );
+                       } catch ( DBQueryError $e ) {
+                               return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
                        }
+               } else {
+                       return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
                }
                return Status::newGood();
        }
index c942878..144e710 100644 (file)
@@ -122,7 +122,7 @@ class SqliteInstaller extends DatabaseInstaller {
        /**
         * @return Status
         */
-       public function openConnection( $dbName = null ) {
+       public function openConnection() {
                global $wgSQLiteDataDir;
 
                $status = Status::newGood();