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 = [
39 'wgDBWindowsAuthentication',
42 protected $internalDefaults = [
43 '_InstallUser' => 'sa',
44 '_InstallWindowsAuthentication' => 'sqlauth',
45 '_WebWindowsAuthentication' => 'sqlauth',
48 // SQL Server 2005 RTM
49 // @todo Are SQL Express version numbers different?)
50 public $minimumVersion = '9.00.1399';
52 // These are schema-level privs
53 // Note: the web user will be created will full permissions if possible, this permission
54 // list is only used if we are unable to grant full permissions.
55 public $webUserPrivs = [
66 public function getName() {
73 public function isCompiled() {
74 return self
::checkExtension( 'sqlsrv' );
80 public function getConnectForm() {
81 if ( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth' ) {
82 $displayStyle = 'display: none;';
84 $displayStyle = 'display: block;';
87 return $this->getTextBox(
91 $this->parent
->getHelpBox( 'config-db-host-help' )
93 Html
::openElement( 'fieldset' ) .
94 Html
::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
95 $this->getTextBox( 'wgDBname', 'config-db-name', [ 'dir' => 'ltr' ],
96 $this->parent
->getHelpBox( 'config-db-name-help' ) ) .
97 $this->getTextBox( 'wgDBmwschema', 'config-db-schema', [ 'dir' => 'ltr' ],
98 $this->parent
->getHelpBox( 'config-db-schema-help' ) ) .
99 $this->getTextBox( 'wgDBprefix', 'config-db-prefix', [ 'dir' => 'ltr' ],
100 $this->parent
->getHelpBox( 'config-db-prefix-help' ) ) .
101 Html
::closeElement( 'fieldset' ) .
102 Html
::openElement( 'fieldset' ) .
103 Html
::element( 'legend', [], wfMessage( 'config-db-install-account' )->text() ) .
104 $this->getRadioSet( [
105 'var' => '_InstallWindowsAuthentication',
106 'label' => 'config-mssql-auth',
107 'itemLabelPrefix' => 'config-mssql-',
108 'values' => [ 'sqlauth', 'windowsauth' ],
111 'class' => 'showHideRadio',
112 'rel' => 'dbCredentialBox',
115 'class' => 'hideShowRadio',
116 'rel' => 'dbCredentialBox',
119 'help' => $this->parent
->getHelpBox( 'config-mssql-install-auth' )
121 Html
::openElement( 'div', [ 'id' => 'dbCredentialBox', 'style' => $displayStyle ] ) .
124 'config-db-username',
126 $this->parent
->getHelpBox( 'config-db-install-username' )
128 $this->getPasswordBox(
130 'config-db-password',
132 $this->parent
->getHelpBox( 'config-db-install-password' )
134 Html
::closeElement( 'div' ) .
135 Html
::closeElement( 'fieldset' );
138 public function submitConnectForm() {
139 // Get variables from the request.
140 $newValues = $this->setVarsFromRequest( [
148 $status = Status
::newGood();
149 if ( !strlen( $newValues['wgDBserver'] ) ) {
150 $status->fatal( 'config-missing-db-host' );
152 if ( !strlen( $newValues['wgDBname'] ) ) {
153 $status->fatal( 'config-missing-db-name' );
154 } elseif ( !preg_match( '/^[a-z0-9_]+$/i', $newValues['wgDBname'] ) ) {
155 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
157 if ( !preg_match( '/^[a-z0-9_]*$/i', $newValues['wgDBmwschema'] ) ) {
158 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
160 if ( !preg_match( '/^[a-z0-9_]*$/i', $newValues['wgDBprefix'] ) ) {
161 $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
163 if ( !$status->isOK() ) {
167 // Check for blank schema and remap to dbo
168 if ( $newValues['wgDBmwschema'] === '' ) {
169 $this->setVar( 'wgDBmwschema', 'dbo' );
173 $this->setVarsFromRequest( [
176 '_InstallWindowsAuthentication'
180 $status = $this->getConnection();
181 if ( !$status->isOK() ) {
185 * @var $conn Database
187 $conn = $status->value
;
190 $version = $conn->getServerVersion();
191 if ( version_compare( $version, $this->minimumVersion
) < 0 ) {
192 return Status
::newFatal( 'config-mssql-old', $this->minimumVersion
, $version );
201 public function openConnection() {
202 global $wgDBWindowsAuthentication;
203 $status = Status
::newGood();
204 $user = $this->getVar( '_InstallUser' );
205 $password = $this->getVar( '_InstallPassword' );
207 if ( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth' ) {
208 // Use Windows authentication for this connection
209 $wgDBWindowsAuthentication = true;
211 $wgDBWindowsAuthentication = false;
215 $db = Database
::factory( 'mssql', [
216 'host' => $this->getVar( 'wgDBserver' ),
218 'password' => $password,
221 'schema' => $this->getVar( 'wgDBmwschema' ),
222 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] );
223 $db->prepareStatements( false );
224 $db->scrollableCursor( false );
225 $status->value
= $db;
226 } catch ( DBConnectionError
$e ) {
227 $status->fatal( 'config-connection-error', $e->getMessage() );
233 public function preUpgrade() {
234 global $wgDBuser, $wgDBpassword;
236 $status = $this->getConnection();
237 if ( !$status->isOK() ) {
238 $this->parent
->showStatusError( $status );
243 * @var $conn Database
245 $conn = $status->value
;
246 $conn->selectDB( $this->getVar( 'wgDBname' ) );
248 # Normal user and password are selected after this step, so for now
249 # just copy these two
250 $wgDBuser = $this->getVar( '_InstallUser' );
251 $wgDBpassword = $this->getVar( '_InstallPassword' );
255 * Return true if the install user can create accounts
259 public function canCreateAccounts() {
260 $status = $this->getConnection();
261 if ( !$status->isOK() ) {
264 /** @var $conn Database */
265 $conn = $status->value
;
267 // We need the server-level ALTER ANY LOGIN permission to create new accounts
268 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( NULL, 'SERVER' )" );
270 'ALTER ANY LOGIN' => false,
271 'CONTROL SERVER' => false,
274 foreach ( $res as $row ) {
275 $serverPrivs[$row->permission_name
] = true;
278 if ( !$serverPrivs['ALTER ANY LOGIN'] ) {
282 // Check to ensure we can grant everything needed as well
283 // We can't actually tell if we have WITH GRANT OPTION for a given permission, so we assume we do
284 // and just check for the permission
285 // https://technet.microsoft.com/en-us/library/ms178569.aspx
286 // The following array sets up which permissions imply whatever permissions we specify
288 // schema database server
289 'DELETE' => [ 'DELETE', 'CONTROL SERVER' ],
290 'EXECUTE' => [ 'EXECUTE', 'CONTROL SERVER' ],
291 'INSERT' => [ 'INSERT', 'CONTROL SERVER' ],
292 'SELECT' => [ 'SELECT', 'CONTROL SERVER' ],
293 'UPDATE' => [ 'UPDATE', 'CONTROL SERVER' ],
296 $grantOptions = array_flip( $this->webUserPrivs
);
298 // Check for schema and db-level permissions, but only if the schema/db exists
299 $schemaPrivs = $dbPrivs = [
307 $dbPrivs['ALTER ANY USER'] = false;
309 if ( $this->databaseExists( $this->getVar( 'wgDBname' ) ) ) {
310 $conn->selectDB( $this->getVar( 'wgDBname' ) );
311 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( NULL, 'DATABASE' )" );
313 foreach ( $res as $row ) {
314 $dbPrivs[$row->permission_name
] = true;
317 // If the db exists, we need ALTER ANY USER privs on it to make a new user
318 if ( !$dbPrivs['ALTER ANY USER'] ) {
322 if ( $this->schemaExists( $this->getVar( 'wgDBmwschema' ) ) ) {
323 // wgDBmwschema is validated to only contain alphanumeric + underscore, so this is safe
324 $res = $conn->query( "SELECT permission_name FROM sys.fn_my_permissions( "
325 . "'{$this->getVar( 'wgDBmwschema' )}', 'SCHEMA' )" );
327 foreach ( $res as $row ) {
328 $schemaPrivs[$row->permission_name
] = true;
333 // Now check all the grants we'll need to be doing to see if we can
334 foreach ( $this->webUserPrivs
as $permission ) {
335 if ( ( isset( $schemaPrivs[$permission] ) && $schemaPrivs[$permission] )
336 ||
( isset( $dbPrivs[$implied[$permission][0]] )
337 && $dbPrivs[$implied[$permission][0]] )
338 ||
( isset( $serverPrivs[$implied[$permission][1]] )
339 && $serverPrivs[$implied[$permission][1]] )
341 unset( $grantOptions[$permission] );
345 if ( count( $grantOptions ) ) {
346 // Can't grant everything
356 public function getSettingsForm() {
357 if ( $this->canCreateAccounts() ) {
358 $noCreateMsg = false;
360 $noCreateMsg = 'config-db-web-no-create-privs';
363 $wrapperStyle = $this->getVar( '_SameAccount' ) ?
'display: none' : '';
364 $displayStyle = $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth'
367 $s = Html
::openElement( 'fieldset' ) .
368 Html
::element( 'legend', [], wfMessage( 'config-db-web-account' )->text() ) .
370 '_SameAccount', 'config-db-web-account-same',
371 [ 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ]
373 Html
::openElement( 'div', [ 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ] ) .
374 $this->getRadioSet( [
375 'var' => '_WebWindowsAuthentication',
376 'label' => 'config-mssql-auth',
377 'itemLabelPrefix' => 'config-mssql-',
378 'values' => [ 'sqlauth', 'windowsauth' ],
381 'class' => 'showHideRadio',
382 'rel' => 'dbCredentialBox',
385 'class' => 'hideShowRadio',
386 'rel' => 'dbCredentialBox',
389 'help' => $this->parent
->getHelpBox( 'config-mssql-web-auth' )
391 Html
::openElement( 'div', [ 'id' => 'dbCredentialBox', 'style' => $displayStyle ] ) .
392 $this->getTextBox( 'wgDBuser', 'config-db-username' ) .
393 $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
394 Html
::closeElement( 'div' );
396 if ( $noCreateMsg ) {
397 $s .= $this->parent
->getWarningBox( wfMessage( $noCreateMsg )->plain() );
399 $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
402 $s .= Html
::closeElement( 'div' ) . Html
::closeElement( 'fieldset' );
410 public function submitSettingsForm() {
411 $this->setVarsFromRequest( [
416 '_WebWindowsAuthentication'
419 if ( $this->getVar( '_SameAccount' ) ) {
420 $this->setVar( '_WebWindowsAuthentication', $this->getVar( '_InstallWindowsAuthentication' ) );
421 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
422 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
425 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
426 $this->setVar( 'wgDBuser', '' );
427 $this->setVar( 'wgDBpassword', '' );
428 $this->setVar( 'wgDBWindowsAuthentication', true );
430 $this->setVar( 'wgDBWindowsAuthentication', false );
433 if ( $this->getVar( '_CreateDBAccount' )
434 && $this->getVar( '_WebWindowsAuthentication' ) == 'sqlauth'
435 && strval( $this->getVar( 'wgDBpassword' ) ) == ''
437 return Status
::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
440 // Validate the create checkbox
441 $canCreate = $this->canCreateAccounts();
443 $this->setVar( '_CreateDBAccount', false );
446 $create = $this->getVar( '_CreateDBAccount' );
450 // Test the web account
451 $user = $this->getVar( 'wgDBuser' );
452 $password = $this->getVar( 'wgDBpassword' );
454 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
455 $user = 'windowsauth';
456 $password = 'windowsauth';
460 Database
::factory( 'mssql', [
461 'host' => $this->getVar( 'wgDBserver' ),
463 'password' => $password,
466 'tablePrefix' => $this->getVar( 'wgDBprefix' ),
467 'schema' => $this->getVar( 'wgDBmwschema' ),
469 } catch ( DBConnectionError
$e ) {
470 return Status
::newFatal( 'config-connection-error', $e->getMessage() );
474 return Status
::newGood();
477 public function preInstall() {
478 # Add our user callback to installSteps, right before the tables are created.
481 'callback' => [ $this, 'setupUser' ],
483 $this->parent
->addInstallStep( $callback, 'tables' );
489 public function setupDatabase() {
490 $status = $this->getConnection();
491 if ( !$status->isOK() ) {
494 /** @var Database $conn */
495 $conn = $status->value
;
496 $dbName = $this->getVar( 'wgDBname' );
497 $schemaName = $this->getVar( 'wgDBmwschema' );
498 if ( !$this->databaseExists( $dbName ) ) {
500 "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ),
504 $conn->selectDB( $dbName );
505 if ( !$this->schemaExists( $schemaName ) ) {
507 "CREATE SCHEMA " . $conn->addIdentifierQuotes( $schemaName ),
511 if ( !$this->catalogExists( $schemaName ) ) {
513 "CREATE FULLTEXT CATALOG " . $conn->addIdentifierQuotes( $schemaName ),
517 $this->setupSchemaVars();
525 public function setupUser() {
526 $dbUser = $this->getVar( 'wgDBuser' );
527 if ( $dbUser == $this->getVar( '_InstallUser' )
528 ||
( $this->getVar( '_InstallWindowsAuthentication' ) == 'windowsauth'
529 && $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) ) {
530 return Status
::newGood();
532 $status = $this->getConnection();
533 if ( !$status->isOK() ) {
537 $this->setupSchemaVars();
538 $dbName = $this->getVar( 'wgDBname' );
539 $this->db
->selectDB( $dbName );
540 $password = $this->getVar( 'wgDBpassword' );
541 $schemaName = $this->getVar( 'wgDBmwschema' );
543 if ( $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth' ) {
544 $dbUser = 'windowsauth';
545 $password = 'windowsauth';
548 if ( $this->getVar( '_CreateDBAccount' ) ) {
551 $tryToCreate = false;
554 $escUser = $this->db
->addIdentifierQuotes( $dbUser );
555 $escDb = $this->db
->addIdentifierQuotes( $dbName );
556 $escSchema = $this->db
->addIdentifierQuotes( $schemaName );
557 $grantableNames = [];
558 if ( $tryToCreate ) {
559 $escPass = $this->db
->addQuotes( $password );
561 if ( !$this->loginExists( $dbUser ) ) {
564 $this->db
->selectDB( 'master' );
565 $logintype = $this->getVar( '_WebWindowsAuthentication' ) == 'windowsauth'
567 : "WITH PASSWORD = $escPass";
568 $this->db
->query( "CREATE LOGIN $escUser $logintype" );
569 $this->db
->selectDB( $dbName );
570 $this->db
->query( "CREATE USER $escUser FOR LOGIN $escUser WITH DEFAULT_SCHEMA = $escSchema" );
572 $grantableNames[] = $dbUser;
573 } catch ( DBQueryError
$dqe ) {
574 $this->db
->rollback();
575 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
577 } elseif ( !$this->userExists( $dbUser ) ) {
580 $this->db
->selectDB( $dbName );
581 $this->db
->query( "CREATE USER $escUser FOR LOGIN $escUser WITH DEFAULT_SCHEMA = $escSchema" );
583 $grantableNames[] = $dbUser;
584 } catch ( DBQueryError
$dqe ) {
585 $this->db
->rollback();
586 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
589 $status->warning( 'config-install-user-alreadyexists', $dbUser );
590 $grantableNames[] = $dbUser;
594 // Try to grant to all the users we know exist or we were able to create
595 $this->db
->selectDB( $dbName );
596 foreach ( $grantableNames as $name ) {
598 // First try to grant full permissions
600 'BACKUP DATABASE', 'BACKUP LOG', 'CREATE FUNCTION', 'CREATE PROCEDURE',
601 'CREATE TABLE', 'CREATE VIEW', 'CREATE FULLTEXT CATALOG', 'SHOWPLAN'
603 $fullPrivList = implode( ', ', $fullPrivArr );
605 $this->db
->query( "GRANT $fullPrivList ON DATABASE :: $escDb TO $escUser", __METHOD__
);
606 $this->db
->query( "GRANT CONTROL ON SCHEMA :: $escSchema TO $escUser", __METHOD__
);
608 } catch ( DBQueryError
$dqe ) {
609 // If that fails, try to grant the limited subset specified in $this->webUserPrivs
611 $privList = implode( ', ', $this->webUserPrivs
);
612 $this->db
->rollback();
614 $this->db
->query( "GRANT $privList ON SCHEMA :: $escSchema TO $escUser", __METHOD__
);
616 } catch ( DBQueryError
$dqe ) {
617 $this->db
->rollback();
618 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
620 // Also try to grant SHOWPLAN on the db, but don't fail if we can't
621 // (just makes a couple things in mediawiki run slower since
622 // we have to run SELECT COUNT(*) instead of getting the query plan)
624 $this->db
->query( "GRANT SHOWPLAN ON DATABASE :: $escDb TO $escUser", __METHOD__
);
625 } catch ( DBQueryError
$dqe ) {
633 public function createTables() {
634 $status = parent
::createTables();
636 // Do last-minute stuff like fulltext indexes (since they can't be inside a transaction)
637 if ( $status->isOK() ) {
638 $searchindex = $this->db
->tableName( 'searchindex' );
639 $schema = $this->db
->addIdentifierQuotes( $this->getVar( 'wgDBmwschema' ) );
641 $this->db
->query( "CREATE FULLTEXT INDEX ON $searchindex (si_title, si_text) "
642 . "KEY INDEX si_page ON $schema" );
643 } catch ( DBQueryError
$dqe ) {
644 $status->fatal( 'config-install-tables-failed', $dqe->getText() );
651 public function getGlobalDefaults() {
652 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
653 // the use of a schema, so we need to set it here
654 return array_merge( parent
::getGlobalDefaults(), [
655 'wgDBmwschema' => 'mediawiki',
660 * Try to see if the login exists
661 * @param string $user Username to check
664 private function loginExists( $user ) {
665 $res = $this->db
->selectField( 'sys.sql_logins', 1, [ 'name' => $user ] );
670 * Try to see if the user account exists
671 * We assume we already have the appropriate database selected
672 * @param string $user Username to check
675 private function userExists( $user ) {
676 $res = $this->db
->selectField( 'sys.sysusers', 1, [ 'name' => $user ] );
681 * Try to see if a given database exists
682 * @param string $dbName Database name to check
685 private function databaseExists( $dbName ) {
686 $res = $this->db
->selectField( 'sys.databases', 1, [ 'name' => $dbName ] );
691 * Try to see if a given schema exists
692 * We assume we already have the appropriate database selected
693 * @param string $schemaName Schema name to check
696 private function schemaExists( $schemaName ) {
697 $res = $this->db
->selectField( 'sys.schemas', 1, [ 'name' => $schemaName ] );
702 * Try to see if a given fulltext catalog exists
703 * We assume we already have the appropriate database selected
704 * @param string $catalogName Catalog name to check
707 private function catalogExists( $catalogName ) {
708 $res = $this->db
->selectField( 'sys.fulltext_catalogs', 1, [ 'name' => $catalogName ] );
713 * Get variables to substitute into tables.sql and the SQL patch files.
717 public function getSchemaVars() {
719 'wgDBname' => $this->getVar( 'wgDBname' ),
720 'wgDBmwschema' => $this->getVar( 'wgDBmwschema' ),
721 'wgDBuser' => $this->getVar( 'wgDBuser' ),
722 'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
726 public function getLocalSettings() {
727 $schema = LocalSettingsGenerator
::escapePhpString( $this->getVar( 'wgDBmwschema' ) );
728 $prefix = LocalSettingsGenerator
::escapePhpString( $this->getVar( 'wgDBprefix' ) );
729 $windowsauth = $this->getVar( 'wgDBWindowsAuthentication' ) ?
'true' : 'false';
731 return "# MSSQL specific settings
732 \$wgDBWindowsAuthentication = {$windowsauth};
733 \$wgDBmwschema = \"{$schema}\";
734 \$wgDBprefix = \"{$prefix}\";";