MediaWiki  REL1_24
PostgresInstaller.php
Go to the documentation of this file.
00001 <?php
00030 class PostgresInstaller extends DatabaseInstaller {
00031 
00032     protected $globalNames = array(
00033         'wgDBserver',
00034         'wgDBport',
00035         'wgDBname',
00036         'wgDBuser',
00037         'wgDBpassword',
00038         'wgDBmwschema',
00039     );
00040 
00041     protected $internalDefaults = array(
00042         '_InstallUser' => 'postgres',
00043     );
00044 
00045     public $minimumVersion = '8.3';
00046     public $maxRoleSearchDepth = 5;
00047 
00048     protected $pgConns = array();
00049 
00050     function getName() {
00051         return 'postgres';
00052     }
00053 
00054     public function isCompiled() {
00055         return self::checkExtension( 'pgsql' );
00056     }
00057 
00058     function getConnectForm() {
00059         return $this->getTextBox(
00060             'wgDBserver',
00061             'config-db-host',
00062             array(),
00063             $this->parent->getHelpBox( 'config-db-host-help' )
00064         ) .
00065             $this->getTextBox( 'wgDBport', 'config-db-port' ) .
00066             Html::openElement( 'fieldset' ) .
00067             Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
00068             $this->getTextBox(
00069                 'wgDBname',
00070                 'config-db-name',
00071                 array(),
00072                 $this->parent->getHelpBox( 'config-db-name-help' )
00073             ) .
00074             $this->getTextBox(
00075                 'wgDBmwschema',
00076                 'config-db-schema',
00077                 array(),
00078                 $this->parent->getHelpBox( 'config-db-schema-help' )
00079             ) .
00080             Html::closeElement( 'fieldset' ) .
00081             $this->getInstallUserBox();
00082     }
00083 
00084     function submitConnectForm() {
00085         // Get variables from the request
00086         $newValues = $this->setVarsFromRequest( array(
00087             'wgDBserver', 'wgDBport', 'wgDBname', 'wgDBmwschema',
00088             '_InstallUser', '_InstallPassword'
00089         ) );
00090 
00091         // Validate them
00092         $status = Status::newGood();
00093         if ( !strlen( $newValues['wgDBname'] ) ) {
00094             $status->fatal( 'config-missing-db-name' );
00095         } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
00096             $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
00097         }
00098         if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
00099             $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
00100         }
00101         if ( !strlen( $newValues['_InstallUser'] ) ) {
00102             $status->fatal( 'config-db-username-empty' );
00103         }
00104         if ( !strlen( $newValues['_InstallPassword'] ) ) {
00105             $status->fatal( 'config-db-password-empty', $newValues['_InstallUser'] );
00106         }
00107 
00108         // Submit user box
00109         if ( $status->isOK() ) {
00110             $status->merge( $this->submitInstallUserBox() );
00111         }
00112         if ( !$status->isOK() ) {
00113             return $status;
00114         }
00115 
00116         $status = $this->getPgConnection( 'create-db' );
00117         if ( !$status->isOK() ) {
00118             return $status;
00119         }
00123         $conn = $status->value;
00124 
00125         // Check version
00126         $version = $conn->getServerVersion();
00127         if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
00128             return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version );
00129         }
00130 
00131         $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
00132         $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
00133 
00134         return Status::newGood();
00135     }
00136 
00137     public function getConnection() {
00138         $status = $this->getPgConnection( 'create-tables' );
00139         if ( $status->isOK() ) {
00140             $this->db = $status->value;
00141         }
00142 
00143         return $status;
00144     }
00145 
00146     public function openConnection() {
00147         return $this->openPgConnection( 'create-tables' );
00148     }
00149 
00158     protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
00159         $status = Status::newGood();
00160         try {
00161             $db = DatabaseBase::factory( 'postgres', array(
00162                 'host' => $this->getVar( 'wgDBserver' ),
00163                 'user' => $user,
00164                 'password' => $password,
00165                 'dbname' => $dbName,
00166                 'schema' => $schema ) );
00167             $status->value = $db;
00168         } catch ( DBConnectionError $e ) {
00169             $status->fatal( 'config-connection-error', $e->getMessage() );
00170         }
00171 
00172         return $status;
00173     }
00174 
00180     protected function getPgConnection( $type ) {
00181         if ( isset( $this->pgConns[$type] ) ) {
00182             return Status::newGood( $this->pgConns[$type] );
00183         }
00184         $status = $this->openPgConnection( $type );
00185 
00186         if ( $status->isOK() ) {
00190             $conn = $status->value;
00191             $conn->clearFlag( DBO_TRX );
00192             $conn->commit( __METHOD__ );
00193             $this->pgConns[$type] = $conn;
00194         }
00195 
00196         return $status;
00197     }
00198 
00224     protected function openPgConnection( $type ) {
00225         switch ( $type ) {
00226             case 'create-db':
00227                 return $this->openConnectionToAnyDB(
00228                     $this->getVar( '_InstallUser' ),
00229                     $this->getVar( '_InstallPassword' ) );
00230             case 'create-schema':
00231                 return $this->openConnectionWithParams(
00232                     $this->getVar( '_InstallUser' ),
00233                     $this->getVar( '_InstallPassword' ),
00234                     $this->getVar( 'wgDBname' ),
00235                     $this->getVar( 'wgDBmwschema' ) );
00236             case 'create-tables':
00237                 $status = $this->openPgConnection( 'create-schema' );
00238                 if ( $status->isOK() ) {
00242                     $conn = $status->value;
00243                     $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00244                     $conn->query( "SET ROLE $safeRole" );
00245                 }
00246 
00247                 return $status;
00248             default:
00249                 throw new MWException( "Invalid special connection type: \"$type\"" );
00250         }
00251     }
00252 
00253     public function openConnectionToAnyDB( $user, $password ) {
00254         $dbs = array(
00255             'template1',
00256             'postgres',
00257         );
00258         if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
00259             array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
00260         }
00261         $conn = false;
00262         $status = Status::newGood();
00263         foreach ( $dbs as $db ) {
00264             try {
00265                 $conn = $this->openConnectionWithParams(
00266                     $user,
00267                     $password,
00268                     $db,
00269                     $this->getVar( 'wgDBmwschema' ) );
00270             } catch ( DBConnectionError $error ) {
00271                 $conn = false;
00272                 $status->fatal( 'config-pg-test-error', $db,
00273                     $error->getMessage() );
00274             }
00275             if ( $conn !== false ) {
00276                 break;
00277             }
00278         }
00279         if ( $conn !== false ) {
00280             return Status::newGood( $conn );
00281         } else {
00282             return $status;
00283         }
00284     }
00285 
00286     protected function getInstallUserPermissions() {
00287         $status = $this->getPgConnection( 'create-db' );
00288         if ( !$status->isOK() ) {
00289             return false;
00290         }
00294         $conn = $status->value;
00295         $superuser = $this->getVar( '_InstallUser' );
00296 
00297         $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
00298             array( 'rolname' => $superuser ), __METHOD__ );
00299 
00300         return $row;
00301     }
00302 
00303     protected function canCreateAccounts() {
00304         $perms = $this->getInstallUserPermissions();
00305         if ( !$perms ) {
00306             return false;
00307         }
00308 
00309         return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
00310     }
00311 
00312     protected function isSuperUser() {
00313         $perms = $this->getInstallUserPermissions();
00314         if ( !$perms ) {
00315             return false;
00316         }
00317 
00318         return $perms->rolsuper === 't';
00319     }
00320 
00321     public function getSettingsForm() {
00322         if ( $this->canCreateAccounts() ) {
00323             $noCreateMsg = false;
00324         } else {
00325             $noCreateMsg = 'config-db-web-no-create-privs';
00326         }
00327         $s = $this->getWebUserBox( $noCreateMsg );
00328 
00329         return $s;
00330     }
00331 
00332     public function submitSettingsForm() {
00333         $status = $this->submitWebUserBox();
00334         if ( !$status->isOK() ) {
00335             return $status;
00336         }
00337 
00338         $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
00339 
00340         if ( $same ) {
00341             $exists = true;
00342         } else {
00343             // Check if the web user exists
00344             // Connect to the database with the install user
00345             $status = $this->getPgConnection( 'create-db' );
00346             if ( !$status->isOK() ) {
00347                 return $status;
00348             }
00349             $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
00350         }
00351 
00352         // Validate the create checkbox
00353         if ( $this->canCreateAccounts() && !$same && !$exists ) {
00354             $create = $this->getVar( '_CreateDBAccount' );
00355         } else {
00356             $this->setVar( '_CreateDBAccount', false );
00357             $create = false;
00358         }
00359 
00360         if ( !$create && !$exists ) {
00361             if ( $this->canCreateAccounts() ) {
00362                 $msg = 'config-install-user-missing-create';
00363             } else {
00364                 $msg = 'config-install-user-missing';
00365             }
00366 
00367             return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
00368         }
00369 
00370         if ( !$exists ) {
00371             // No more checks to do
00372             return Status::newGood();
00373         }
00374 
00375         // Existing web account. Test the connection.
00376         $status = $this->openConnectionToAnyDB(
00377             $this->getVar( 'wgDBuser' ),
00378             $this->getVar( 'wgDBpassword' ) );
00379         if ( !$status->isOK() ) {
00380             return $status;
00381         }
00382 
00383         // The web user is conventionally the table owner in PostgreSQL
00384         // installations. Make sure the install user is able to create
00385         // objects on behalf of the web user.
00386         if ( $same || $this->canCreateObjectsForWebUser() ) {
00387             return Status::newGood();
00388         } else {
00389             return Status::newFatal( 'config-pg-not-in-role' );
00390         }
00391     }
00392 
00398     protected function canCreateObjectsForWebUser() {
00399         if ( $this->isSuperUser() ) {
00400             return true;
00401         }
00402 
00403         $status = $this->getPgConnection( 'create-db' );
00404         if ( !$status->isOK() ) {
00405             return false;
00406         }
00407         $conn = $status->value;
00408         $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
00409             array( 'rolname' => $this->getVar( '_InstallUser' ) ), __METHOD__ );
00410         $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
00411             array( 'rolname' => $this->getVar( 'wgDBuser' ) ), __METHOD__ );
00412 
00413         return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
00414     }
00415 
00424     protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
00425         if ( $targetMember === $group ) {
00426             // A role is always a member of itself
00427             return true;
00428         }
00429         // Get all members of the given group
00430         $res = $conn->select( '"pg_catalog"."pg_auth_members"', array( 'member' ),
00431             array( 'roleid' => $group ), __METHOD__ );
00432         foreach ( $res as $row ) {
00433             if ( $row->member == $targetMember ) {
00434                 // Found target member
00435                 return true;
00436             }
00437             // Recursively search each member of the group to see if the target
00438             // is a member of it, up to the given maximum depth.
00439             if ( $maxDepth > 0 ) {
00440                 if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) {
00441                     // Found member of member
00442                     return true;
00443                 }
00444             }
00445         }
00446 
00447         return false;
00448     }
00449 
00450     public function preInstall() {
00451         $createDbAccount = array(
00452             'name' => 'user',
00453             'callback' => array( $this, 'setupUser' ),
00454         );
00455         $commitCB = array(
00456             'name' => 'pg-commit',
00457             'callback' => array( $this, 'commitChanges' ),
00458         );
00459         $plpgCB = array(
00460             'name' => 'pg-plpgsql',
00461             'callback' => array( $this, 'setupPLpgSQL' ),
00462         );
00463         $schemaCB = array(
00464             'name' => 'schema',
00465             'callback' => array( $this, 'setupSchema' )
00466         );
00467 
00468         if ( $this->getVar( '_CreateDBAccount' ) ) {
00469             $this->parent->addInstallStep( $createDbAccount, 'database' );
00470         }
00471         $this->parent->addInstallStep( $commitCB, 'interwiki' );
00472         $this->parent->addInstallStep( $plpgCB, 'database' );
00473         $this->parent->addInstallStep( $schemaCB, 'database' );
00474     }
00475 
00476     function setupDatabase() {
00477         $status = $this->getPgConnection( 'create-db' );
00478         if ( !$status->isOK() ) {
00479             return $status;
00480         }
00481         $conn = $status->value;
00482 
00483         $dbName = $this->getVar( 'wgDBname' );
00484 
00485         $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
00486             array( 'datname' => $dbName ), __METHOD__ );
00487         if ( !$exists ) {
00488             $safedb = $conn->addIdentifierQuotes( $dbName );
00489             $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
00490         }
00491 
00492         return Status::newGood();
00493     }
00494 
00495     function setupSchema() {
00496         // Get a connection to the target database
00497         $status = $this->getPgConnection( 'create-schema' );
00498         if ( !$status->isOK() ) {
00499             return $status;
00500         }
00501         $conn = $status->value;
00502 
00503         // Create the schema if necessary
00504         $schema = $this->getVar( 'wgDBmwschema' );
00505         $safeschema = $conn->addIdentifierQuotes( $schema );
00506         $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00507         if ( !$conn->schemaExists( $schema ) ) {
00508             try {
00509                 $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
00510             } catch ( DBQueryError $e ) {
00511                 return Status::newFatal( 'config-install-pg-schema-failed',
00512                     $this->getVar( '_InstallUser' ), $schema );
00513             }
00514         }
00515 
00516         // Select the new schema in the current connection
00517         $conn->determineCoreSchema( $schema );
00518 
00519         return Status::newGood();
00520     }
00521 
00522     function commitChanges() {
00523         $this->db->commit( __METHOD__ );
00524 
00525         return Status::newGood();
00526     }
00527 
00528     function setupUser() {
00529         if ( !$this->getVar( '_CreateDBAccount' ) ) {
00530             return Status::newGood();
00531         }
00532 
00533         $status = $this->getPgConnection( 'create-db' );
00534         if ( !$status->isOK() ) {
00535             return $status;
00536         }
00537         $conn = $status->value;
00538 
00539         $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00540         $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
00541 
00542         // Check if the user already exists
00543         $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
00544         if ( !$userExists ) {
00545             // Create the user
00546             try {
00547                 $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
00548 
00549                 // If the install user is not a superuser, we need to make the install
00550                 // user a member of the new user's group, so that the install user will
00551                 // be able to create a schema and other objects on behalf of the new user.
00552                 if ( !$this->isSuperUser() ) {
00553                     $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
00554                 }
00555 
00556                 $conn->query( $sql, __METHOD__ );
00557             } catch ( DBQueryError $e ) {
00558                 return Status::newFatal( 'config-install-user-create-failed',
00559                     $this->getVar( 'wgDBuser' ), $e->getMessage() );
00560             }
00561         }
00562 
00563         return Status::newGood();
00564     }
00565 
00566     function getLocalSettings() {
00567         $port = $this->getVar( 'wgDBport' );
00568         $schema = $this->getVar( 'wgDBmwschema' );
00569 
00570         return "# Postgres specific settings
00571 \$wgDBport = \"{$port}\";
00572 \$wgDBmwschema = \"{$schema}\";";
00573     }
00574 
00575     public function preUpgrade() {
00576         global $wgDBuser, $wgDBpassword;
00577 
00578         # Normal user and password are selected after this step, so for now
00579         # just copy these two
00580         $wgDBuser = $this->getVar( '_InstallUser' );
00581         $wgDBpassword = $this->getVar( '_InstallPassword' );
00582     }
00583 
00584     public function createTables() {
00585         $schema = $this->getVar( 'wgDBmwschema' );
00586 
00587         $status = $this->getConnection();
00588         if ( !$status->isOK() ) {
00589             return $status;
00590         }
00591 
00595         $conn = $status->value;
00596 
00597         if ( $conn->tableExists( 'archive' ) ) {
00598             $status->warning( 'config-install-tables-exist' );
00599             $this->enableLB();
00600 
00601             return $status;
00602         }
00603 
00604         $conn->begin( __METHOD__ );
00605 
00606         if ( !$conn->schemaExists( $schema ) ) {
00607             $status->fatal( 'config-install-pg-schema-not-exist' );
00608 
00609             return $status;
00610         }
00611         $error = $conn->sourceFile( $conn->getSchemaPath() );
00612         if ( $error !== true ) {
00613             $conn->reportQueryError( $error, 0, '', __METHOD__ );
00614             $conn->rollback( __METHOD__ );
00615             $status->fatal( 'config-install-tables-failed', $error );
00616         } else {
00617             $conn->commit( __METHOD__ );
00618         }
00619         // Resume normal operations
00620         if ( $status->isOk() ) {
00621             $this->enableLB();
00622         }
00623 
00624         return $status;
00625     }
00626 
00627     public function getGlobalDefaults() {
00628         // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
00629         // the use of a schema, so we need to set it here
00630         return array(
00631             'wgDBmwschema' => 'mediawiki',
00632         );
00633     }
00634 
00635     public function setupPLpgSQL() {
00636         // Connect as the install user, since it owns the database and so is
00637         // the user that needs to run "CREATE LANGAUGE"
00638         $status = $this->getPgConnection( 'create-schema' );
00639         if ( !$status->isOK() ) {
00640             return $status;
00641         }
00645         $conn = $status->value;
00646 
00647         $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1,
00648             array( 'lanname' => 'plpgsql' ), __METHOD__ );
00649         if ( $exists ) {
00650             // Already exists, nothing to do
00651             return Status::newGood();
00652         }
00653 
00654         // plpgsql is not installed, but if we have a pg_pltemplate table, we
00655         // should be able to create it
00656         $exists = $conn->selectField(
00657             array( '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ),
00658             1,
00659             array(
00660                 'pg_namespace.oid=relnamespace',
00661                 'nspname' => 'pg_catalog',
00662                 'relname' => 'pg_pltemplate',
00663             ),
00664             __METHOD__ );
00665         if ( $exists ) {
00666             try {
00667                 $conn->query( 'CREATE LANGUAGE plpgsql' );
00668             } catch ( DBQueryError $e ) {
00669                 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
00670             }
00671         } else {
00672             return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
00673         }
00674 
00675         return Status::newGood();
00676     }
00677 }