[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/installer/ -> PostgresInstaller.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1