3 * Microsoft SQL Server-specific installer.
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.
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.
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
25 * Class for setting up the MediaWiki database using Microsoft SQL Server.
30 class MssqlInstaller
extends DatabaseInstaller
{
32 protected $globalNames = array(
39 'wgDBWindowsAuthentication',
42 protected $internalDefaults = array(
43 '_InstallUser' => 'sa',
44 '_InstallWindowsAuthentication' => 'sqlauth',
45 '_WebWindowsAuthentication' => 'sqlauth',
48 public $minimumVersion = '9.00.1399'; // SQL Server 2005 RTM (TODO: are SQL Express version numbers different?)
50 // These are schema-level privs
51 // Note: the web user will be created will full permissions if possible, this permission
52 // list is only used if we are unable to grant full permissions.
53 public $webUserPrivs = array(
64 public function getName() {
71 public function isCompiled() {
72 return self
::checkExtension( 'sqlsrv' );
78 public function getConnectForm() {
79 if ( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth' ) {
80 $displayStyle = 'display: none;';
82 $displayStyle = 'display: block;';
85 return $this->getTextBox(
89 $this->parent
->getHelpBox( 'config-db-host-help' )
91 Html
::openElement( 'fieldset' ) .
92 Html
::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
93 $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ),
94 $this->parent
->getHelpBox( 'config-db-name-help' ) ) .
95 $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array( 'dir' => 'ltr' ),
96 $this->parent
->getHelpBox( 'config-db-schema-help' ) ) .
97 $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ),
98 $this->parent
->getHelpBox( 'config-db-prefix-help' ) ) .
99 Html
::closeElement( 'fieldset' ) .
100 Html
::openElement( 'fieldset' ) .
101 Html
::element( 'legend', array(), wfMessage( 'config-db-install-account' )->text() ) .
102 $this->getRadioSet( array(
103 'var' => '_InstallWindowsAuthentication',
104 'label' => 'config-mssql-auth',
105 'itemLabelPrefix' => 'config-mssql-',
106 'values' => array( 'sqlauth', 'windowsauth' ),
107 'itemAttribs' => array(
109 'class' => 'showHideRadio',
110 'rel' => 'dbCredentialBox',
112 'windowsauth' => array(
113 'class' => 'hideShowRadio',
114 'rel' => 'dbCredentialBox',
117 'help' => $this->parent
->getHelpBox( 'config-mssql-install-auth' )
119 Html
::openElement( 'div', array( 'id' => 'dbCredentialBox', 'style' => $displayStyle ) ) .
122 'config-db-username',
123 array( 'dir' => 'ltr' ),
124 $this->parent
->getHelpBox( 'config-db-install-username' )
126 $this->getPasswordBox(
128 'config-db-password',
129 array( 'dir' => 'ltr' ),
130 $this->parent
->getHelpBox( 'config-db-install-password' )
132 Html
::closeElement( 'div' ) .
133 Html
::closeElement( 'fieldset' );
136 public function submitConnectForm() {
137 // Get variables from the request.
138 $newValues = $this->setVarsFromRequest( array( 'wgDBserver', 'wgDBname', 'wgDBmwschema', 'wgDBprefix' ) );
141 $status = Status
::newGood();
142 if ( !strlen( $newValues['wgDBserver'] ) ) {
143 $status->fatal( 'config-missing-db-host' );
145 if ( !strlen( $newValues['wgDBname'] ) ) {
146 $status->fatal( 'config-missing-db-name' );
147 } elseif ( !preg_match( '/^[a-z0-9_]+$/i', $newValues['wgDBname'] ) ) {
148 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
150 if ( !preg_match( '/^[a-z0-9_]*$/i', $newValues['wgDBmwschema'] ) ) {
151 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
153 if ( !preg_match( '/^[a-z0-9_]*$/i', $newValues['wgDBprefix'] ) ) {
154 $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
156 if ( !$status->isOK() ) {
160 // Check for blank schema and remap to dbo
161 if ( $newValues['wgDBmwschema'] === '' ) {
162 $this->setVar( 'wgDBmwschema', 'dbo' );
166 $this->setVarsFromRequest( array( '_InstallUser', '_InstallPassword', '_InstallWindowsAuthentication' ) );
169 $status = $this->getConnection();
170 if ( !$status->isOK() ) {
174 * @var $conn DatabaseBase
176 $conn = $status->value
;
179 $version = $conn->getServerVersion();
180 if ( version_compare( $version, $this->minimumVersion
) < 0 ) {
181 return Status
::newFatal( 'config-mssql-old', $this->minimumVersion
, $version );
190 public function openConnection() {
191 global $wgDBWindowsAuthentication;
192 $status = Status
::newGood();
193 $user = $this->getVar( '_InstallUser' );
194 $password = $this->getVar( '_InstallPassword' );
196 if ( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth' ) {
197 // Use Windows authentication for this connection
198 $wgDBWindowsAuthentication = true;
200 $wgDBWindowsAuthentication = false;
204 $db = DatabaseBase
::factory( 'mssql', array(
205 'host' => $this->getVar( 'wgDBserver' ),
207 'password' => $password,
210 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
211 $db->prepareStatements( false );
212 $db->scrollableCursor( false );
213 $status->value
= $db;
214 } catch ( DBConnectionError
$e ) {
215 $status->fatal( 'config-connection-error', $e->getMessage() );
221 public function preUpgrade() {
222 global $wgDBuser, $wgDBpassword;
224 $status = $this->getConnection();
225 if ( !$status->isOK() ) {
226 $this->parent
->showStatusError( $status );
231 * @var $conn DatabaseBase
233 $conn = $status->value
;
234 $conn->selectDB( $this->getVar( 'wgDBname' ) );
236 # Normal user and password are selected after this step, so for now
237 # just copy these two
238 $wgDBuser = $this->getVar( '_InstallUser' );
239 $wgDBpassword = $this->getVar( '_InstallPassword' );
243 * Return true if the install user can create accounts
247 public function canCreateAccounts() {
248 $status = $this->getConnection();
249 if ( !$status->isOK() ) {
252 /** @var $conn DatabaseBase */
253 $conn = $status->value
;
255 // We need the server-level ALTER ANY LOGIN permission to create new accounts
256 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( NULL, 'SERVER' )" );
257 $serverPrivs = array(
258 'ALTER ANY LOGIN' => false,
259 'CONTROL SERVER' => false,
262 foreach ( $res as $row ) {
263 $serverPrivs[$row->permission_name
] = true;
266 if ( !$serverPrivs['ALTER ANY LOGIN'] ) {
270 // Check to ensure we can grant everything needed as well
271 // We can't actually tell if we have WITH GRANT OPTION for a given permission, so we assume we do
272 // and just check for the permission
273 // http://technet.microsoft.com/en-us/library/ms178569.aspx
274 // The following array sets up which permissions imply whatever permissions we specify
276 // schema database server
277 'DELETE' => array( 'DELETE', 'CONTROL SERVER' ),
278 'EXECUTE' => array( 'EXECUTE', 'CONTROL SERVER' ),
279 'INSERT' => array( 'INSERT', 'CONTROL SERVER' ),
280 'SELECT' => array( 'SELECT', 'CONTROL SERVER' ),
281 'UPDATE' => array( 'UPDATE', 'CONTROL SERVER' ),
284 $grantOptions = array_flip( $this->webUserPrivs
);
286 // Check for schema and db-level permissions, but only if the schema/db exists
287 $schemaPrivs = $dbPrivs = array(
295 $dbPrivs['ALTER ANY USER'] = false;
297 if ( $this->databaseExists( $this->getVar( 'wgDBname' ) ) ) {
298 $conn->selectDB( $this->getVar( 'wgDBname' ) );
299 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( NULL, 'DATABASE' )" );
301 foreach ( $res as $row ) {
302 $dbPrivs[$row->permission_name
] = true;
305 // If the db exists, we need ALTER ANY USER privs on it to make a new user
306 if ( !$dbPrivs['ALTER ANY USER'] ) {
310 if ( $this->schemaExists( $this->getVar( 'wgDBmwschema' ) ) ) {
311 // wgDBmwschema is validated to only contain alphanumeric + underscore, so this is safe
312 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( '{$this->getVar( 'wgDBmwschema' )}', 'SCHEMA' )" );
314 foreach ( $res as $row ) {
315 $schemaPrivs[$row->permission_name
] = true;
320 // Now check all the grants we'll need to be doing to see if we can
321 foreach ( $this->webUserPrivs
as $permission ) {
322 if ( ( isset( $schemaPrivs[$permission] ) && $schemaPrivs[$permission] )
323 ||
( isset( $dbPrivs[$implied[$permission][0]] ) && $dbPrivs[$implied[$permission][0]] )
324 ||
( isset( $serverPrivs[$implied[$permission][1]] ) && $serverPrivs[$implied[$permission][1]] ) ) {
326 unset( $grantOptions[$permission] );
330 if ( count( $grantOptions ) ) {
331 // Can't grant everything
341 public function getSettingsForm() {
342 if ( $this->canCreateAccounts() ) {
343 $noCreateMsg = false;
345 $noCreateMsg = 'config-db-web-no-create-privs';
347 $wrapperStyle = $this->getVar( '_SameAccount' ) ?
'display: none' : '';
348 $displayStyle = $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ?
'display: none' : '';
349 $s = Html
::openElement( 'fieldset' ) .
350 Html
::element( 'legend', array(), wfMessage( 'config-db-web-account' )->text() ) .
352 '_SameAccount', 'config-db-web-account-same',
353 array( 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' )
355 Html
::openElement( 'div', array( 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ) ) .
356 $this->getRadioSet( array(
357 'var' => '_WebWindowsAuthentication',
358 'label' => 'config-mssql-auth',
359 'itemLabelPrefix' => 'config-mssql-',
360 'values' => array( 'sqlauth', 'windowsauth' ),
361 'itemAttribs' => array(
363 'class' => 'showHideRadio',
364 'rel' => 'dbCredentialBox',
366 'windowsauth' => array(
367 'class' => 'hideShowRadio',
368 'rel' => 'dbCredentialBox',
371 'help' => $this->parent
->getHelpBox( 'config-mssql-web-auth' )
373 Html
::openElement( 'div', array( 'id' => 'dbCredentialBox', 'style' => $displayStyle ) ) .
374 $this->getTextBox( 'wgDBuser', 'config-db-username' ) .
375 $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
376 Html
::closeElement( 'div' );
378 if ( $noCreateMsg ) {
379 $s .= $this->parent
->getWarningBox( wfMessage( $noCreateMsg )->plain() );
381 $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
384 $s .= Html
::closeElement( 'div' ) . Html
::closeElement( 'fieldset' );
392 public function submitSettingsForm() {
393 $this->setVarsFromRequest(
394 array( 'wgDBuser', 'wgDBpassword', '_SameAccount', '_CreateDBAccount', '_WebWindowsAuthentication' )
397 if ( $this->getVar( '_SameAccount' ) ) {
398 $this->setVar( '_WebWindowsAuthentication', $this->getVar( '_InstallWindowsAuthentication' ) );
399 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
400 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
403 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
404 $this->setVar( 'wgDBuser', '' );
405 $this->setVar( 'wgDBpassword', '' );
406 $this->setVar( 'wgDBWindowsAuthentication', true );
408 $this->setVar( 'wgDBWindowsAuthentication', false );
411 if ( $this->getVar( '_CreateDBAccount' ) && $this->getVar( '_WebWindowsAuthentication' ) == 'sqlauth' && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
412 return Status
::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
415 // Validate the create checkbox
416 $canCreate = $this->canCreateAccounts();
418 $this->setVar( '_CreateDBAccount', false );
421 $create = $this->getVar( '_CreateDBAccount' );
425 // Test the web account
426 $user = $this->getVar( 'wgDBuser' );
427 $password = $this->getVar( 'wgDBpassword' );
429 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
430 $user = 'windowsauth';
431 $password = 'windowsauth';
435 DatabaseBase
::factory( 'mssql', array(
436 'host' => $this->getVar( 'wgDBserver' ),
438 'password' => $password,
441 'tablePrefix' => $this->getVar( 'wgDBprefix' ),
442 'schema' => $this->getVar( 'wgDBmwschema' ),
444 } catch ( DBConnectionError
$e ) {
445 return Status
::newFatal( 'config-connection-error', $e->getMessage() );
449 return Status
::newGood();
452 public function preInstall() {
453 # Add our user callback to installSteps, right before the tables are created.
456 'callback' => array( $this, 'setupUser' ),
458 $this->parent
->addInstallStep( $callback, 'tables' );
464 public function setupDatabase() {
465 $status = $this->getConnection();
466 if ( !$status->isOK() ) {
469 /** @var DatabaseBase $conn */
470 $conn = $status->value
;
471 $dbName = $this->getVar( 'wgDBname' );
472 $schemaName = $this->getVar( 'wgDBmwschema' );
473 if ( !$this->databaseExists( $dbName ) ) {
474 $conn->query( "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ), __METHOD__
);
475 $conn->selectDB( $dbName );
476 if ( !$this->schemaExists( $schemaName ) ) {
477 $conn->query( "CREATE SCHEMA " . $conn->addIdentifierQuotes( $schemaName ), __METHOD__
);
479 if ( !$this->catalogExists( $schemaName ) ) {
480 $conn->query( "CREATE FULLTEXT CATALOG " . $conn->addIdentifierQuotes( $schemaName ), __METHOD__
);
483 $this->setupSchemaVars();
491 public function setupUser() {
492 $dbUser = $this->getVar( 'wgDBuser' );
493 if ( $dbUser == $this->getVar( '_InstallUser' )
494 ||
( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth'
495 && $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) ) {
496 return Status
::newGood();
498 $status = $this->getConnection();
499 if ( !$status->isOK() ) {
503 $this->setupSchemaVars();
504 $dbName = $this->getVar( 'wgDBname' );
505 $this->db
->selectDB( $dbName );
506 $server = $this->getVar( 'wgDBserver' );
507 $password = $this->getVar( 'wgDBpassword' );
508 $schemaName = $this->getVar( 'wgDBmwschema' );
510 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
511 $dbUser = 'windowsauth';
512 $password = 'windowsauth';
515 if ( $this->getVar( '_CreateDBAccount' ) ) {
518 $tryToCreate = false;
521 $escUser = $this->db
->addIdentifierQuotes( $dbUser );
522 $escDb = $this->db
->addIdentifierQuotes( $dbName );
523 $escSchema = $this->db
->addIdentifierQuotes( $schemaName );
524 $grantableNames = array();
525 if ( $tryToCreate ) {
526 $escPass = $this->db
->addQuotes( $password );
528 if ( !$this->loginExists( $dbUser ) ) {
531 $this->db
->selectDB( 'master' );
532 $logintype = $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ?
'FROM WINDOWS' : "WITH PASSWORD = $escPass";
533 $this->db
->query( "CREATE LOGIN $escUser $logintype" );
534 $this->db
->selectDB( $dbName );
535 $this->db
->query( "CREATE USER $escUser FOR LOGIN $escUser WITH DEFAULT_SCHEMA = $escSchema" );
537 $grantableNames[] = $dbUser;
538 } catch ( DBQueryError
$dqe ) {
539 $this->db
->rollback();
540 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
542 } elseif ( !$this->userExists( $dbUser ) ) {
545 $this->db
->selectDB( $dbName );
546 $this->db
->query( "CREATE USER $escUser FOR LOGIN $escUser WITH DEFAULT_SCHEMA = $escSchema" );
548 $grantableNames[] = $dbUser;
549 } catch ( DBQueryError
$dqe ) {
550 $this->db
->rollback();
551 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
554 $status->warning( 'config-install-user-alreadyexists', $dbUser );
555 $grantableNames[] = $dbUser;
559 // Try to grant to all the users we know exist or we were able to create
560 $this->db
->selectDB( $dbName );
561 foreach ( $grantableNames as $name ) {
563 // First try to grant full permissions
564 $fullPrivArr = array(
565 'BACKUP DATABASE', 'BACKUP LOG', 'CREATE FUNCTION', 'CREATE PROCEDURE',
566 'CREATE TABLE', 'CREATE VIEW', 'CREATE FULLTEXT CATALOG', 'SHOWPLAN'
568 $fullPrivList = implode( ', ', $fullPrivArr );
570 $this->db
->query( "GRANT $fullPrivList ON DATABASE :: $escDb TO $escUser", __METHOD__
);
571 $this->db
->query( "GRANT CONTROL ON SCHEMA :: $escSchema TO $escUser", __METHOD__
);
573 } catch ( DBQueryError
$dqe ) {
574 // If that fails, try to grant the limited subset specified in $this->webUserPrivs
576 $privList = implode( ', ', $this->webUserPrivs
);
577 $this->db
->rollback();
579 $this->db
->query( "GRANT $privList ON SCHEMA :: $escSchema TO $escUser", __METHOD__
);
581 } catch ( DBQueryError
$dqe ) {
582 $this->db
->rollback();
583 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
585 // Also try to grant SHOWPLAN on the db, but don't fail if we can't
586 // (just makes a couple things in mediawiki run slower since
587 // we have to run SELECT COUNT(*) instead of getting the query plan)
589 $this->db
->query( "GRANT SHOWPLAN ON DATABASE :: $escDb TO $escUser", __METHOD__
);
590 } catch ( DBQueryError
$dqe ) {
598 public function createTables() {
599 $status = parent
::createTables();
601 // Do last-minute stuff like fulltext indexes (since they can't be inside a transaction)
602 if ( $status->isOk() ) {
603 $searchindex = $this->db
->tableName( 'searchindex' );
604 $schema = $this->db
->addIdentifierQuotes( $this->getVar( 'wgDBmwschema' ) );
606 $this->db
->query( "CREATE FULLTEXT INDEX ON $searchindex (si_title, si_text) KEY INDEX si_page ON $schema" );
607 } catch ( DBQueryError
$dqe ) {
608 $status->fatal( 'config-install-tables-failed', $dqe->getText() );
616 * Try to see if the login exists
617 * @param string $user Username to check
620 private function loginExists( $user ) {
621 $res = $this->db
->selectField( 'sys.sql_logins', 1, array( 'name' => $user ) );
626 * Try to see if the user account exists
627 * We assume we already have the appropriate database selected
628 * @param string $user Username to check
631 private function userExists( $user ) {
632 $res = $this->db
->selectField( 'sys.sysusers', 1, array( 'name' => $user ) );
637 * Try to see if a given database exists
638 * @param string $dbName Database name to check
641 private function databaseExists( $dbName ) {
642 $res = $this->db
->selectField( 'sys.databases', 1, array( 'name' => $dbName ) );
647 * Try to see if a given schema exists
648 * We assume we already have the appropriate database selected
649 * @param string $schemaName Schema name to check
652 private function schemaExists( $schemaName ) {
653 $res = $this->db
->selectField( 'sys.schemas', 1, array( 'name' => $schemaName ) );
658 * Try to see if a given fulltext catalog exists
659 * We assume we already have the appropriate database selected
660 * @param string $schemaName Catalog name to check
663 private function catalogExists( $catalogName ) {
664 $res = $this->db
->selectField( 'sys.fulltext_catalogs', 1, array( 'name' => $catalogName ) );
669 * Get variables to substitute into tables.sql and the SQL patch files.
673 public function getSchemaVars() {
675 'wgDBname' => $this->getVar( 'wgDBname' ),
676 'wgDBmwschema' => $this->getVar( 'wgDBmwschema' ),
677 'wgDBuser' => $this->getVar( 'wgDBuser' ),
678 'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
682 public function getLocalSettings() {
683 $schema = LocalSettingsGenerator
::escapePhpString( $this->getVar( 'wgDBmwschema' ) );
684 $prefix = LocalSettingsGenerator
::escapePhpString( $this->getVar( 'wgDBprefix' ) );
685 $windowsauth = $this->getVar( 'wgDBWindowsAuthentication' ) ?
'true' : 'false';
687 return "# MSSQL specific settings
688 \$wgDBWindowsAuthentication = {$windowsauth};
689 \$wgDBmwschema = \"{$schema}\";
690 \$wgDBprefix = \"{$prefix}\";";