MediaWiki  REL1_22
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( 'wgDBserver', 'wgDBport',
00087             'wgDBname', 'wgDBmwschema' ) );
00088 
00089         // Validate them
00090         $status = Status::newGood();
00091         if ( !strlen( $newValues['wgDBname'] ) ) {
00092             $status->fatal( 'config-missing-db-name' );
00093         } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
00094             $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
00095         }
00096         if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
00097             $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
00098         }
00099 
00100         // Submit user box
00101         if ( $status->isOK() ) {
00102             $status->merge( $this->submitInstallUserBox() );
00103         }
00104         if ( !$status->isOK() ) {
00105             return $status;
00106         }
00107 
00108         $status = $this->getPgConnection( 'create-db' );
00109         if ( !$status->isOK() ) {
00110             return $status;
00111         }
00115         $conn = $status->value;
00116 
00117         // Check version
00118         $version = $conn->getServerVersion();
00119         if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
00120             return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version );
00121         }
00122 
00123         $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
00124         $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
00125 
00126         return Status::newGood();
00127     }
00128 
00129     public function getConnection() {
00130         $status = $this->getPgConnection( 'create-tables' );
00131         if ( $status->isOK() ) {
00132             $this->db = $status->value;
00133         }
00134 
00135         return $status;
00136     }
00137 
00138     public function openConnection() {
00139         return $this->openPgConnection( 'create-tables' );
00140     }
00141 
00149     protected function openConnectionWithParams( $user, $password, $dbName ) {
00150         $status = Status::newGood();
00151         try {
00152             $db = new DatabasePostgres(
00153                 $this->getVar( 'wgDBserver' ),
00154                 $user,
00155                 $password,
00156                 $dbName
00157             );
00158             $status->value = $db;
00159         } catch ( DBConnectionError $e ) {
00160             $status->fatal( 'config-connection-error', $e->getMessage() );
00161         }
00162 
00163         return $status;
00164     }
00165 
00171     protected function getPgConnection( $type ) {
00172         if ( isset( $this->pgConns[$type] ) ) {
00173             return Status::newGood( $this->pgConns[$type] );
00174         }
00175         $status = $this->openPgConnection( $type );
00176 
00177         if ( $status->isOK() ) {
00181             $conn = $status->value;
00182             $conn->clearFlag( DBO_TRX );
00183             $conn->commit( __METHOD__ );
00184             $this->pgConns[$type] = $conn;
00185         }
00186 
00187         return $status;
00188     }
00189 
00216     protected function openPgConnection( $type ) {
00217         switch ( $type ) {
00218             case 'create-db':
00219                 return $this->openConnectionToAnyDB(
00220                     $this->getVar( '_InstallUser' ),
00221                     $this->getVar( '_InstallPassword' ) );
00222             case 'create-schema':
00223                 return $this->openConnectionWithParams(
00224                     $this->getVar( '_InstallUser' ),
00225                     $this->getVar( '_InstallPassword' ),
00226                     $this->getVar( 'wgDBname' ) );
00227             case 'create-tables':
00228                 $status = $this->openPgConnection( 'create-schema' );
00229                 if ( $status->isOK() ) {
00233                     $conn = $status->value;
00234                     $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00235                     $conn->query( "SET ROLE $safeRole" );
00236                 }
00237 
00238                 return $status;
00239             default:
00240                 throw new MWException( "Invalid special connection type: \"$type\"" );
00241         }
00242     }
00243 
00244     public function openConnectionToAnyDB( $user, $password ) {
00245         $dbs = array(
00246             'template1',
00247             'postgres',
00248         );
00249         if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
00250             array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
00251         }
00252         $conn = false;
00253         $status = Status::newGood();
00254         foreach ( $dbs as $db ) {
00255             try {
00256                 $conn = new DatabasePostgres(
00257                     $this->getVar( 'wgDBserver' ),
00258                     $user,
00259                     $password,
00260                     $db );
00261             } catch ( DBConnectionError $error ) {
00262                 $conn = false;
00263                 $status->fatal( 'config-pg-test-error', $db,
00264                     $error->getMessage() );
00265             }
00266             if ( $conn !== false ) {
00267                 break;
00268             }
00269         }
00270         if ( $conn !== false ) {
00271             return Status::newGood( $conn );
00272         } else {
00273             return $status;
00274         }
00275     }
00276 
00277     protected function getInstallUserPermissions() {
00278         $status = $this->getPgConnection( 'create-db' );
00279         if ( !$status->isOK() ) {
00280             return false;
00281         }
00285         $conn = $status->value;
00286         $superuser = $this->getVar( '_InstallUser' );
00287 
00288         $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
00289             array( 'rolname' => $superuser ), __METHOD__ );
00290 
00291         return $row;
00292     }
00293 
00294     protected function canCreateAccounts() {
00295         $perms = $this->getInstallUserPermissions();
00296         if ( !$perms ) {
00297             return false;
00298         }
00299 
00300         return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
00301     }
00302 
00303     protected function isSuperUser() {
00304         $perms = $this->getInstallUserPermissions();
00305         if ( !$perms ) {
00306             return false;
00307         }
00308 
00309         return $perms->rolsuper === 't';
00310     }
00311 
00312     public function getSettingsForm() {
00313         if ( $this->canCreateAccounts() ) {
00314             $noCreateMsg = false;
00315         } else {
00316             $noCreateMsg = 'config-db-web-no-create-privs';
00317         }
00318         $s = $this->getWebUserBox( $noCreateMsg );
00319 
00320         return $s;
00321     }
00322 
00323     public function submitSettingsForm() {
00324         $status = $this->submitWebUserBox();
00325         if ( !$status->isOK() ) {
00326             return $status;
00327         }
00328 
00329         $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
00330 
00331         if ( $same ) {
00332             $exists = true;
00333         } else {
00334             // Check if the web user exists
00335             // Connect to the database with the install user
00336             $status = $this->getPgConnection( 'create-db' );
00337             if ( !$status->isOK() ) {
00338                 return $status;
00339             }
00340             $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
00341         }
00342 
00343         // Validate the create checkbox
00344         if ( $this->canCreateAccounts() && !$same && !$exists ) {
00345             $create = $this->getVar( '_CreateDBAccount' );
00346         } else {
00347             $this->setVar( '_CreateDBAccount', false );
00348             $create = false;
00349         }
00350 
00351         if ( !$create && !$exists ) {
00352             if ( $this->canCreateAccounts() ) {
00353                 $msg = 'config-install-user-missing-create';
00354             } else {
00355                 $msg = 'config-install-user-missing';
00356             }
00357 
00358             return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
00359         }
00360 
00361         if ( !$exists ) {
00362             // No more checks to do
00363             return Status::newGood();
00364         }
00365 
00366         // Existing web account. Test the connection.
00367         $status = $this->openConnectionToAnyDB(
00368             $this->getVar( 'wgDBuser' ),
00369             $this->getVar( 'wgDBpassword' ) );
00370         if ( !$status->isOK() ) {
00371             return $status;
00372         }
00373 
00374         // The web user is conventionally the table owner in PostgreSQL
00375         // installations. Make sure the install user is able to create
00376         // objects on behalf of the web user.
00377         if ( $same || $this->canCreateObjectsForWebUser() ) {
00378             return Status::newGood();
00379         } else {
00380             return Status::newFatal( 'config-pg-not-in-role' );
00381         }
00382     }
00383 
00389     protected function canCreateObjectsForWebUser() {
00390         if ( $this->isSuperUser() ) {
00391             return true;
00392         }
00393 
00394         $status = $this->getPgConnection( 'create-db' );
00395         if ( !$status->isOK() ) {
00396             return false;
00397         }
00398         $conn = $status->value;
00399         $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
00400             array( 'rolname' => $this->getVar( '_InstallUser' ) ), __METHOD__ );
00401         $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
00402             array( 'rolname' => $this->getVar( 'wgDBuser' ) ), __METHOD__ );
00403 
00404         return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
00405     }
00406 
00415     protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
00416         if ( $targetMember === $group ) {
00417             // A role is always a member of itself
00418             return true;
00419         }
00420         // Get all members of the given group
00421         $res = $conn->select( '"pg_catalog"."pg_auth_members"', array( 'member' ),
00422             array( 'roleid' => $group ), __METHOD__ );
00423         foreach ( $res as $row ) {
00424             if ( $row->member == $targetMember ) {
00425                 // Found target member
00426                 return true;
00427             }
00428             // Recursively search each member of the group to see if the target
00429             // is a member of it, up to the given maximum depth.
00430             if ( $maxDepth > 0 ) {
00431                 if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) {
00432                     // Found member of member
00433                     return true;
00434                 }
00435             }
00436         }
00437 
00438         return false;
00439     }
00440 
00441     public function preInstall() {
00442         $createDbAccount = array(
00443             'name' => 'user',
00444             'callback' => array( $this, 'setupUser' ),
00445         );
00446         $commitCB = array(
00447             'name' => 'pg-commit',
00448             'callback' => array( $this, 'commitChanges' ),
00449         );
00450         $plpgCB = array(
00451             'name' => 'pg-plpgsql',
00452             'callback' => array( $this, 'setupPLpgSQL' ),
00453         );
00454         $schemaCB = array(
00455             'name' => 'schema',
00456             'callback' => array( $this, 'setupSchema' )
00457         );
00458 
00459         if ( $this->getVar( '_CreateDBAccount' ) ) {
00460             $this->parent->addInstallStep( $createDbAccount, 'database' );
00461         }
00462         $this->parent->addInstallStep( $commitCB, 'interwiki' );
00463         $this->parent->addInstallStep( $plpgCB, 'database' );
00464         $this->parent->addInstallStep( $schemaCB, 'database' );
00465     }
00466 
00467     function setupDatabase() {
00468         $status = $this->getPgConnection( 'create-db' );
00469         if ( !$status->isOK() ) {
00470             return $status;
00471         }
00472         $conn = $status->value;
00473 
00474         $dbName = $this->getVar( 'wgDBname' );
00475 
00476         $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
00477             array( 'datname' => $dbName ), __METHOD__ );
00478         if ( !$exists ) {
00479             $safedb = $conn->addIdentifierQuotes( $dbName );
00480             $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
00481         }
00482 
00483         return Status::newGood();
00484     }
00485 
00486     function setupSchema() {
00487         // Get a connection to the target database
00488         $status = $this->getPgConnection( 'create-schema' );
00489         if ( !$status->isOK() ) {
00490             return $status;
00491         }
00492         $conn = $status->value;
00493 
00494         // Create the schema if necessary
00495         $schema = $this->getVar( 'wgDBmwschema' );
00496         $safeschema = $conn->addIdentifierQuotes( $schema );
00497         $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00498         if ( !$conn->schemaExists( $schema ) ) {
00499             try {
00500                 $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
00501             } catch ( DBQueryError $e ) {
00502                 return Status::newFatal( 'config-install-pg-schema-failed',
00503                     $this->getVar( '_InstallUser' ), $schema );
00504             }
00505         }
00506 
00507         // Select the new schema in the current connection
00508         $conn->determineCoreSchema( $schema );
00509 
00510         return Status::newGood();
00511     }
00512 
00513     function commitChanges() {
00514         $this->db->commit( __METHOD__ );
00515 
00516         return Status::newGood();
00517     }
00518 
00519     function setupUser() {
00520         if ( !$this->getVar( '_CreateDBAccount' ) ) {
00521             return Status::newGood();
00522         }
00523 
00524         $status = $this->getPgConnection( 'create-db' );
00525         if ( !$status->isOK() ) {
00526             return $status;
00527         }
00528         $conn = $status->value;
00529 
00530         $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
00531         $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
00532 
00533         // Check if the user already exists
00534         $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
00535         if ( !$userExists ) {
00536             // Create the user
00537             try {
00538                 $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
00539 
00540                 // If the install user is not a superuser, we need to make the install
00541                 // user a member of the new user's group, so that the install user will
00542                 // be able to create a schema and other objects on behalf of the new user.
00543                 if ( !$this->isSuperUser() ) {
00544                     $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
00545                 }
00546 
00547                 $conn->query( $sql, __METHOD__ );
00548             } catch ( DBQueryError $e ) {
00549                 return Status::newFatal( 'config-install-user-create-failed',
00550                     $this->getVar( 'wgDBuser' ), $e->getMessage() );
00551             }
00552         }
00553 
00554         return Status::newGood();
00555     }
00556 
00557     function getLocalSettings() {
00558         $port = $this->getVar( 'wgDBport' );
00559         $schema = $this->getVar( 'wgDBmwschema' );
00560 
00561         return "# Postgres specific settings
00562 \$wgDBport = \"{$port}\";
00563 \$wgDBmwschema = \"{$schema}\";";
00564     }
00565 
00566     public function preUpgrade() {
00567         global $wgDBuser, $wgDBpassword;
00568 
00569         # Normal user and password are selected after this step, so for now
00570         # just copy these two
00571         $wgDBuser = $this->getVar( '_InstallUser' );
00572         $wgDBpassword = $this->getVar( '_InstallPassword' );
00573     }
00574 
00575     public function createTables() {
00576         $schema = $this->getVar( 'wgDBmwschema' );
00577 
00578         $status = $this->getConnection();
00579         if ( !$status->isOK() ) {
00580             return $status;
00581         }
00582 
00586         $conn = $status->value;
00587 
00588         if ( $conn->tableExists( 'archive' ) ) {
00589             $status->warning( 'config-install-tables-exist' );
00590             $this->enableLB();
00591 
00592             return $status;
00593         }
00594 
00595         $conn->begin( __METHOD__ );
00596 
00597         if ( !$conn->schemaExists( $schema ) ) {
00598             $status->fatal( 'config-install-pg-schema-not-exist' );
00599 
00600             return $status;
00601         }
00602         $error = $conn->sourceFile( $conn->getSchemaPath() );
00603         if ( $error !== true ) {
00604             $conn->reportQueryError( $error, 0, '', __METHOD__ );
00605             $conn->rollback( __METHOD__ );
00606             $status->fatal( 'config-install-tables-failed', $error );
00607         } else {
00608             $conn->commit( __METHOD__ );
00609         }
00610         // Resume normal operations
00611         if ( $status->isOk() ) {
00612             $this->enableLB();
00613         }
00614 
00615         return $status;
00616     }
00617 
00618     public function setupPLpgSQL() {
00619         // Connect as the install user, since it owns the database and so is
00620         // the user that needs to run "CREATE LANGAUGE"
00621         $status = $this->getPgConnection( 'create-schema' );
00622         if ( !$status->isOK() ) {
00623             return $status;
00624         }
00628         $conn = $status->value;
00629 
00630         $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1,
00631             array( 'lanname' => 'plpgsql' ), __METHOD__ );
00632         if ( $exists ) {
00633             // Already exists, nothing to do
00634             return Status::newGood();
00635         }
00636 
00637         // plpgsql is not installed, but if we have a pg_pltemplate table, we
00638         // should be able to create it
00639         $exists = $conn->selectField(
00640             array( '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ),
00641             1,
00642             array(
00643                 'pg_namespace.oid=relnamespace',
00644                 'nspname' => 'pg_catalog',
00645                 'relname' => 'pg_pltemplate',
00646             ),
00647             __METHOD__ );
00648         if ( $exists ) {
00649             try {
00650                 $conn->query( 'CREATE LANGUAGE plpgsql' );
00651             } catch ( DBQueryError $e ) {
00652                 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
00653             }
00654         } else {
00655             return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
00656         }
00657 
00658         return Status::newGood();
00659     }
00660 }