[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * PostgreSQL-specific installer. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Deployment 22 */ 23 24 /** 25 * Class for setting up the MediaWiki database using Postgres. 26 * 27 * @ingroup Deployment 28 * @since 1.17 29 */ 30 class PostgresInstaller extends DatabaseInstaller { 31 32 protected $globalNames = array( 33 'wgDBserver', 34 'wgDBport', 35 'wgDBname', 36 'wgDBuser', 37 'wgDBpassword', 38 'wgDBmwschema', 39 ); 40 41 protected $internalDefaults = array( 42 '_InstallUser' => 'postgres', 43 ); 44 45 public $minimumVersion = '8.3'; 46 public $maxRoleSearchDepth = 5; 47 48 protected $pgConns = array(); 49 50 function getName() { 51 return 'postgres'; 52 } 53 54 public function isCompiled() { 55 return self::checkExtension( 'pgsql' ); 56 } 57 58 function getConnectForm() { 59 return $this->getTextBox( 60 'wgDBserver', 61 'config-db-host', 62 array(), 63 $this->parent->getHelpBox( 'config-db-host-help' ) 64 ) . 65 $this->getTextBox( 'wgDBport', 'config-db-port' ) . 66 Html::openElement( 'fieldset' ) . 67 Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . 68 $this->getTextBox( 69 'wgDBname', 70 'config-db-name', 71 array(), 72 $this->parent->getHelpBox( 'config-db-name-help' ) 73 ) . 74 $this->getTextBox( 75 'wgDBmwschema', 76 'config-db-schema', 77 array(), 78 $this->parent->getHelpBox( 'config-db-schema-help' ) 79 ) . 80 Html::closeElement( 'fieldset' ) . 81 $this->getInstallUserBox(); 82 } 83 84 function submitConnectForm() { 85 // Get variables from the request 86 $newValues = $this->setVarsFromRequest( array( 87 'wgDBserver', 'wgDBport', 'wgDBname', 'wgDBmwschema', 88 '_InstallUser', '_InstallPassword' 89 ) ); 90 91 // Validate them 92 $status = Status::newGood(); 93 if ( !strlen( $newValues['wgDBname'] ) ) { 94 $status->fatal( 'config-missing-db-name' ); 95 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) { 96 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] ); 97 } 98 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) { 99 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] ); 100 } 101 if ( !strlen( $newValues['_InstallUser'] ) ) { 102 $status->fatal( 'config-db-username-empty' ); 103 } 104 if ( !strlen( $newValues['_InstallPassword'] ) ) { 105 $status->fatal( 'config-db-password-empty', $newValues['_InstallUser'] ); 106 } 107 108 // Submit user box 109 if ( $status->isOK() ) { 110 $status->merge( $this->submitInstallUserBox() ); 111 } 112 if ( !$status->isOK() ) { 113 return $status; 114 } 115 116 $status = $this->getPgConnection( 'create-db' ); 117 if ( !$status->isOK() ) { 118 return $status; 119 } 120 /** 121 * @var $conn DatabaseBase 122 */ 123 $conn = $status->value; 124 125 // Check version 126 $version = $conn->getServerVersion(); 127 if ( version_compare( $version, $this->minimumVersion ) < 0 ) { 128 return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version ); 129 } 130 131 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) ); 132 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) ); 133 134 return Status::newGood(); 135 } 136 137 public function getConnection() { 138 $status = $this->getPgConnection( 'create-tables' ); 139 if ( $status->isOK() ) { 140 $this->db = $status->value; 141 } 142 143 return $status; 144 } 145 146 public function openConnection() { 147 return $this->openPgConnection( 'create-tables' ); 148 } 149 150 /** 151 * Open a PG connection with given parameters 152 * @param string $user User name 153 * @param string $password Password 154 * @param string $dbName Database name 155 * @param string $schema Database schema 156 * @return Status 157 */ 158 protected function openConnectionWithParams( $user, $password, $dbName, $schema ) { 159 $status = Status::newGood(); 160 try { 161 $db = DatabaseBase::factory( 'postgres', array( 162 'host' => $this->getVar( 'wgDBserver' ), 163 'user' => $user, 164 'password' => $password, 165 'dbname' => $dbName, 166 'schema' => $schema ) ); 167 $status->value = $db; 168 } catch ( DBConnectionError $e ) { 169 $status->fatal( 'config-connection-error', $e->getMessage() ); 170 } 171 172 return $status; 173 } 174 175 /** 176 * Get a special type of connection 177 * @param string $type See openPgConnection() for details. 178 * @return Status 179 */ 180 protected function getPgConnection( $type ) { 181 if ( isset( $this->pgConns[$type] ) ) { 182 return Status::newGood( $this->pgConns[$type] ); 183 } 184 $status = $this->openPgConnection( $type ); 185 186 if ( $status->isOK() ) { 187 /** 188 * @var $conn DatabaseBase 189 */ 190 $conn = $status->value; 191 $conn->clearFlag( DBO_TRX ); 192 $conn->commit( __METHOD__ ); 193 $this->pgConns[$type] = $conn; 194 } 195 196 return $status; 197 } 198 199 /** 200 * Get a connection of a specific PostgreSQL-specific type. Connections 201 * of a given type are cached. 202 * 203 * PostgreSQL lacks cross-database operations, so after the new database is 204 * created, you need to make a separate connection to connect to that 205 * database and add tables to it. 206 * 207 * New tables are owned by the user that creates them, and MediaWiki's 208 * PostgreSQL support has always assumed that the table owner will be 209 * $wgDBuser. So before we create new tables, we either need to either 210 * connect as the other user or to execute a SET ROLE command. Using a 211 * separate connection for this allows us to avoid accidental cross-module 212 * dependencies. 213 * 214 * @param string $type The type of connection to get: 215 * - create-db: A connection for creating DBs, suitable for pre- 216 * installation. 217 * - create-schema: A connection to the new DB, for creating schemas and 218 * other similar objects in the new DB. 219 * - create-tables: A connection with a role suitable for creating tables. 220 * 221 * @throws MWException 222 * @return Status On success, a connection object will be in the value member. 223 */ 224 protected function openPgConnection( $type ) { 225 switch ( $type ) { 226 case 'create-db': 227 return $this->openConnectionToAnyDB( 228 $this->getVar( '_InstallUser' ), 229 $this->getVar( '_InstallPassword' ) ); 230 case 'create-schema': 231 return $this->openConnectionWithParams( 232 $this->getVar( '_InstallUser' ), 233 $this->getVar( '_InstallPassword' ), 234 $this->getVar( 'wgDBname' ), 235 $this->getVar( 'wgDBmwschema' ) ); 236 case 'create-tables': 237 $status = $this->openPgConnection( 'create-schema' ); 238 if ( $status->isOK() ) { 239 /** 240 * @var $conn DatabaseBase 241 */ 242 $conn = $status->value; 243 $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) ); 244 $conn->query( "SET ROLE $safeRole" ); 245 } 246 247 return $status; 248 default: 249 throw new MWException( "Invalid special connection type: \"$type\"" ); 250 } 251 } 252 253 public function openConnectionToAnyDB( $user, $password ) { 254 $dbs = array( 255 'template1', 256 'postgres', 257 ); 258 if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) { 259 array_unshift( $dbs, $this->getVar( 'wgDBname' ) ); 260 } 261 $conn = false; 262 $status = Status::newGood(); 263 foreach ( $dbs as $db ) { 264 try { 265 $conn = new DatabasePostgres( 266 $this->getVar( 'wgDBserver' ), 267 $user, 268 $password, 269 $db ); 270 } catch ( DBConnectionError $error ) { 271 $conn = false; 272 $status->fatal( 'config-pg-test-error', $db, 273 $error->getMessage() ); 274 } 275 if ( $conn !== false ) { 276 break; 277 } 278 } 279 if ( $conn !== false ) { 280 return Status::newGood( $conn ); 281 } else { 282 return $status; 283 } 284 } 285 286 protected function getInstallUserPermissions() { 287 $status = $this->getPgConnection( 'create-db' ); 288 if ( !$status->isOK() ) { 289 return false; 290 } 291 /** 292 * @var $conn DatabaseBase 293 */ 294 $conn = $status->value; 295 $superuser = $this->getVar( '_InstallUser' ); 296 297 $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*', 298 array( 'rolname' => $superuser ), __METHOD__ ); 299 300 return $row; 301 } 302 303 protected function canCreateAccounts() { 304 $perms = $this->getInstallUserPermissions(); 305 if ( !$perms ) { 306 return false; 307 } 308 309 return $perms->rolsuper === 't' || $perms->rolcreaterole === 't'; 310 } 311 312 protected function isSuperUser() { 313 $perms = $this->getInstallUserPermissions(); 314 if ( !$perms ) { 315 return false; 316 } 317 318 return $perms->rolsuper === 't'; 319 } 320 321 public function getSettingsForm() { 322 if ( $this->canCreateAccounts() ) { 323 $noCreateMsg = false; 324 } else { 325 $noCreateMsg = 'config-db-web-no-create-privs'; 326 } 327 $s = $this->getWebUserBox( $noCreateMsg ); 328 329 return $s; 330 } 331 332 public function submitSettingsForm() { 333 $status = $this->submitWebUserBox(); 334 if ( !$status->isOK() ) { 335 return $status; 336 } 337 338 $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' ); 339 340 if ( $same ) { 341 $exists = true; 342 } else { 343 // Check if the web user exists 344 // Connect to the database with the install user 345 $status = $this->getPgConnection( 'create-db' ); 346 if ( !$status->isOK() ) { 347 return $status; 348 } 349 $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) ); 350 } 351 352 // Validate the create checkbox 353 if ( $this->canCreateAccounts() && !$same && !$exists ) { 354 $create = $this->getVar( '_CreateDBAccount' ); 355 } else { 356 $this->setVar( '_CreateDBAccount', false ); 357 $create = false; 358 } 359 360 if ( !$create && !$exists ) { 361 if ( $this->canCreateAccounts() ) { 362 $msg = 'config-install-user-missing-create'; 363 } else { 364 $msg = 'config-install-user-missing'; 365 } 366 367 return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) ); 368 } 369 370 if ( !$exists ) { 371 // No more checks to do 372 return Status::newGood(); 373 } 374 375 // Existing web account. Test the connection. 376 $status = $this->openConnectionToAnyDB( 377 $this->getVar( 'wgDBuser' ), 378 $this->getVar( 'wgDBpassword' ) ); 379 if ( !$status->isOK() ) { 380 return $status; 381 } 382 383 // The web user is conventionally the table owner in PostgreSQL 384 // installations. Make sure the install user is able to create 385 // objects on behalf of the web user. 386 if ( $same || $this->canCreateObjectsForWebUser() ) { 387 return Status::newGood(); 388 } else { 389 return Status::newFatal( 'config-pg-not-in-role' ); 390 } 391 } 392 393 /** 394 * Returns true if the install user is able to create objects owned 395 * by the web user, false otherwise. 396 * @return bool 397 */ 398 protected function canCreateObjectsForWebUser() { 399 if ( $this->isSuperUser() ) { 400 return true; 401 } 402 403 $status = $this->getPgConnection( 'create-db' ); 404 if ( !$status->isOK() ) { 405 return false; 406 } 407 $conn = $status->value; 408 $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid', 409 array( 'rolname' => $this->getVar( '_InstallUser' ) ), __METHOD__ ); 410 $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid', 411 array( 'rolname' => $this->getVar( 'wgDBuser' ) ), __METHOD__ ); 412 413 return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth ); 414 } 415 416 /** 417 * Recursive helper for canCreateObjectsForWebUser(). 418 * @param DatabaseBase $conn 419 * @param int $targetMember Role ID of the member to look for 420 * @param int $group Role ID of the group to look for 421 * @param int $maxDepth Maximum recursive search depth 422 * @return bool 423 */ 424 protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) { 425 if ( $targetMember === $group ) { 426 // A role is always a member of itself 427 return true; 428 } 429 // Get all members of the given group 430 $res = $conn->select( '"pg_catalog"."pg_auth_members"', array( 'member' ), 431 array( 'roleid' => $group ), __METHOD__ ); 432 foreach ( $res as $row ) { 433 if ( $row->member == $targetMember ) { 434 // Found target member 435 return true; 436 } 437 // Recursively search each member of the group to see if the target 438 // is a member of it, up to the given maximum depth. 439 if ( $maxDepth > 0 ) { 440 if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) { 441 // Found member of member 442 return true; 443 } 444 } 445 } 446 447 return false; 448 } 449 450 public function preInstall() { 451 $createDbAccount = array( 452 'name' => 'user', 453 'callback' => array( $this, 'setupUser' ), 454 ); 455 $commitCB = array( 456 'name' => 'pg-commit', 457 'callback' => array( $this, 'commitChanges' ), 458 ); 459 $plpgCB = array( 460 'name' => 'pg-plpgsql', 461 'callback' => array( $this, 'setupPLpgSQL' ), 462 ); 463 $schemaCB = array( 464 'name' => 'schema', 465 'callback' => array( $this, 'setupSchema' ) 466 ); 467 468 if ( $this->getVar( '_CreateDBAccount' ) ) { 469 $this->parent->addInstallStep( $createDbAccount, 'database' ); 470 } 471 $this->parent->addInstallStep( $commitCB, 'interwiki' ); 472 $this->parent->addInstallStep( $plpgCB, 'database' ); 473 $this->parent->addInstallStep( $schemaCB, 'database' ); 474 } 475 476 function setupDatabase() { 477 $status = $this->getPgConnection( 'create-db' ); 478 if ( !$status->isOK() ) { 479 return $status; 480 } 481 $conn = $status->value; 482 483 $dbName = $this->getVar( 'wgDBname' ); 484 485 $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1', 486 array( 'datname' => $dbName ), __METHOD__ ); 487 if ( !$exists ) { 488 $safedb = $conn->addIdentifierQuotes( $dbName ); 489 $conn->query( "CREATE DATABASE $safedb", __METHOD__ ); 490 } 491 492 return Status::newGood(); 493 } 494 495 function setupSchema() { 496 // Get a connection to the target database 497 $status = $this->getPgConnection( 'create-schema' ); 498 if ( !$status->isOK() ) { 499 return $status; 500 } 501 $conn = $status->value; 502 503 // Create the schema if necessary 504 $schema = $this->getVar( 'wgDBmwschema' ); 505 $safeschema = $conn->addIdentifierQuotes( $schema ); 506 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) ); 507 if ( !$conn->schemaExists( $schema ) ) { 508 try { 509 $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" ); 510 } catch ( DBQueryError $e ) { 511 return Status::newFatal( 'config-install-pg-schema-failed', 512 $this->getVar( '_InstallUser' ), $schema ); 513 } 514 } 515 516 // Select the new schema in the current connection 517 $conn->determineCoreSchema( $schema ); 518 519 return Status::newGood(); 520 } 521 522 function commitChanges() { 523 $this->db->commit( __METHOD__ ); 524 525 return Status::newGood(); 526 } 527 528 function setupUser() { 529 if ( !$this->getVar( '_CreateDBAccount' ) ) { 530 return Status::newGood(); 531 } 532 533 $status = $this->getPgConnection( 'create-db' ); 534 if ( !$status->isOK() ) { 535 return $status; 536 } 537 $conn = $status->value; 538 539 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) ); 540 $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) ); 541 542 // Check if the user already exists 543 $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) ); 544 if ( !$userExists ) { 545 // Create the user 546 try { 547 $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass"; 548 549 // If the install user is not a superuser, we need to make the install 550 // user a member of the new user's group, so that the install user will 551 // be able to create a schema and other objects on behalf of the new user. 552 if ( !$this->isSuperUser() ) { 553 $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) ); 554 } 555 556 $conn->query( $sql, __METHOD__ ); 557 } catch ( DBQueryError $e ) { 558 return Status::newFatal( 'config-install-user-create-failed', 559 $this->getVar( 'wgDBuser' ), $e->getMessage() ); 560 } 561 } 562 563 return Status::newGood(); 564 } 565 566 function getLocalSettings() { 567 $port = $this->getVar( 'wgDBport' ); 568 $schema = $this->getVar( 'wgDBmwschema' ); 569 570 return "# Postgres specific settings 571 \$wgDBport = \"{$port}\"; 572 \$wgDBmwschema = \"{$schema}\";"; 573 } 574 575 public function preUpgrade() { 576 global $wgDBuser, $wgDBpassword; 577 578 # Normal user and password are selected after this step, so for now 579 # just copy these two 580 $wgDBuser = $this->getVar( '_InstallUser' ); 581 $wgDBpassword = $this->getVar( '_InstallPassword' ); 582 } 583 584 public function createTables() { 585 $schema = $this->getVar( 'wgDBmwschema' ); 586 587 $status = $this->getConnection(); 588 if ( !$status->isOK() ) { 589 return $status; 590 } 591 592 /** 593 * @var $conn DatabaseBase 594 */ 595 $conn = $status->value; 596 597 if ( $conn->tableExists( 'archive' ) ) { 598 $status->warning( 'config-install-tables-exist' ); 599 $this->enableLB(); 600 601 return $status; 602 } 603 604 $conn->begin( __METHOD__ ); 605 606 if ( !$conn->schemaExists( $schema ) ) { 607 $status->fatal( 'config-install-pg-schema-not-exist' ); 608 609 return $status; 610 } 611 $error = $conn->sourceFile( $conn->getSchemaPath() ); 612 if ( $error !== true ) { 613 $conn->reportQueryError( $error, 0, '', __METHOD__ ); 614 $conn->rollback( __METHOD__ ); 615 $status->fatal( 'config-install-tables-failed', $error ); 616 } else { 617 $conn->commit( __METHOD__ ); 618 } 619 // Resume normal operations 620 if ( $status->isOk() ) { 621 $this->enableLB(); 622 } 623 624 return $status; 625 } 626 627 public function getGlobalDefaults() { 628 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require 629 // the use of a schema, so we need to set it here 630 return array( 631 'wgDBmwschema' => 'mediawiki', 632 ); 633 } 634 635 public function setupPLpgSQL() { 636 // Connect as the install user, since it owns the database and so is 637 // the user that needs to run "CREATE LANGAUGE" 638 $status = $this->getPgConnection( 'create-schema' ); 639 if ( !$status->isOK() ) { 640 return $status; 641 } 642 /** 643 * @var $conn DatabaseBase 644 */ 645 $conn = $status->value; 646 647 $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1, 648 array( 'lanname' => 'plpgsql' ), __METHOD__ ); 649 if ( $exists ) { 650 // Already exists, nothing to do 651 return Status::newGood(); 652 } 653 654 // plpgsql is not installed, but if we have a pg_pltemplate table, we 655 // should be able to create it 656 $exists = $conn->selectField( 657 array( '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ), 658 1, 659 array( 660 'pg_namespace.oid=relnamespace', 661 'nspname' => 'pg_catalog', 662 'relname' => 'pg_pltemplate', 663 ), 664 __METHOD__ ); 665 if ( $exists ) { 666 try { 667 $conn->query( 'CREATE LANGUAGE plpgsql' ); 668 } catch ( DBQueryError $e ) { 669 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) ); 670 } 671 } else { 672 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) ); 673 } 674 675 return Status::newGood(); 676 } 677 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |