MediaWiki master
SqliteInstaller.php
Go to the documentation of this file.
1<?php
2
25namespace MediaWiki\Installer;
26
29use Wikimedia\AtEase\AtEase;
33
41
42 public static $minimumVersion = '3.8.0';
43 protected static $notMinimumVersionMessage = 'config-outdated-sqlite';
44
48 public $db;
49
50 protected $globalNames = [
51 'wgDBname',
52 'wgSQLiteDataDir',
53 ];
54
55 public function getName() {
56 return 'sqlite';
57 }
58
59 public function isCompiled() {
60 return self::checkExtension( 'pdo_sqlite' );
61 }
62
66 public function checkPrerequisites() {
67 // Bail out if SQLite is too old
69 $result = static::meetsMinimumRequirement( $db );
70 // Check for FTS3 full-text search module
72 $result->warning( 'config-no-fts3' );
73 }
74
75 return $result;
76 }
77
78 public function getGlobalDefaults() {
79 global $IP;
80 $defaults = parent::getGlobalDefaults();
81 if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
82 $path = dirname( $_SERVER['DOCUMENT_ROOT'] );
83 } else {
84 // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT']
85 $path = $IP;
86 }
87 $defaults['wgSQLiteDataDir'] = str_replace(
88 [ '/', '\\' ],
89 DIRECTORY_SEPARATOR,
90 $path . '/data'
91 );
92 return $defaults;
93 }
94
95 public function getConnectForm() {
96 return $this->getTextBox(
97 'wgSQLiteDataDir',
98 'config-sqlite-dir', [],
99 $this->parent->getHelpBox( 'config-sqlite-dir-help' )
100 ) .
101 $this->getTextBox(
102 'wgDBname',
103 'config-db-name',
104 [],
105 $this->parent->getHelpBox( 'config-sqlite-name-help' )
106 );
107 }
108
116 private static function realpath( $path ) {
117 return realpath( $path ) ?: $path;
118 }
119
123 public function submitConnectForm() {
124 $this->setVarsFromRequest( [ 'wgSQLiteDataDir', 'wgDBname' ] );
125
126 # Try realpath() if the directory already exists
127 $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) );
128 $result = self::checkDataDir( $dir );
129 if ( $result->isOK() ) {
130 # Try expanding again in case we've just created it
131 $dir = self::realpath( $dir );
132 $this->setVar( 'wgSQLiteDataDir', $dir );
133 }
134 # Table prefix is not used on SQLite, keep it empty
135 $this->setVar( 'wgDBprefix', '' );
136
137 return $result;
138 }
139
145 private static function checkDataDir( $dir ): Status {
146 if ( is_dir( $dir ) ) {
147 if ( !is_readable( $dir ) ) {
148 return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
149 }
150 } elseif ( !is_writable( dirname( $dir ) ) ) {
151 // Check the parent directory if $dir not exists
153 if ( $webserverGroup !== null ) {
154 return Status::newFatal(
155 'config-sqlite-parent-unwritable-group',
156 $dir, dirname( $dir ), basename( $dir ),
157 $webserverGroup
158 );
159 }
160
161 return Status::newFatal(
162 'config-sqlite-parent-unwritable-nogroup',
163 $dir, dirname( $dir ), basename( $dir )
164 );
165 }
166 return Status::newGood();
167 }
168
173 private static function createDataDir( $dir ): Status {
174 if ( !is_dir( $dir ) ) {
175 AtEase::suppressWarnings();
176 $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
177 AtEase::restoreWarnings();
178 if ( !$ok ) {
179 return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
180 }
181 }
182 # Put a .htaccess file in case the user didn't take our advice
183 file_put_contents( "$dir/.htaccess", "Deny from all\n" );
184 return Status::newGood();
185 }
186
190 public function openConnection() {
191 $status = Status::newGood();
192 $dir = $this->getVar( 'wgSQLiteDataDir' );
193 $dbName = $this->getVar( 'wgDBname' );
194 try {
195 $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create(
196 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ]
197 );
198 $status->value = $db;
199 } catch ( DBConnectionError $e ) {
200 $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
201 }
202
203 return $status;
204 }
205
209 public function needsUpgrade() {
210 $dir = $this->getVar( 'wgSQLiteDataDir' );
211 $dbName = $this->getVar( 'wgDBname' );
212 // Don't create the data file yet
213 if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) {
214 return false;
215 }
216
217 // If the data file exists, look inside it
218 return parent::needsUpgrade();
219 }
220
224 public function setupDatabase() {
225 $dir = $this->getVar( 'wgSQLiteDataDir' );
226
227 # Double check (Only available in web installation). We checked this before but maybe someone
228 # deleted the data dir between then and now
229 $dir_status = self::checkDataDir( $dir );
230 if ( $dir_status->isGood() ) {
231 $res = self::createDataDir( $dir );
232 if ( !$res->isGood() ) {
233 return $res;
234 }
235 } else {
236 return $dir_status;
237 }
238
239 $db = $this->getVar( 'wgDBname' );
240
241 # Make the main and cache stub DB files
242 $status = Status::newGood();
243 $status->merge( $this->makeStubDBFile( $dir, $db ) );
244 $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
245 $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
246 $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
247 if ( !$status->isOK() ) {
248 return $status;
249 }
250
251 # Nuke the unused settings for clarity
252 $this->setVar( 'wgDBserver', '' );
253 $this->setVar( 'wgDBuser', '' );
254 $this->setVar( 'wgDBpassword', '' );
255 $this->setupSchemaVars();
256
257 # Create the l10n cache DB
258 try {
259 $conn = ( new DatabaseFactory() )->create(
260 'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
261 # @todo: don't duplicate l10n_cache definition, though it's very simple
262 $sql =
263<<<EOT
264 CREATE TABLE l10n_cache (
265 lc_lang BLOB NOT NULL,
266 lc_key TEXT NOT NULL,
267 lc_value BLOB NOT NULL,
268 PRIMARY KEY (lc_lang, lc_key)
269 );
270EOT;
271 $conn->query( $sql, __METHOD__ );
272 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
273 $conn->close( __METHOD__ );
274 } catch ( DBConnectionError $e ) {
275 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
276 }
277
278 # Create the job queue DB
279 try {
280 $conn = ( new DatabaseFactory() )->create(
281 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
282 # @todo: don't duplicate job definition, though it's very static
283 $sql =
284<<<EOT
285 CREATE TABLE job (
286 job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
287 job_cmd BLOB NOT NULL default '',
288 job_namespace INTEGER NOT NULL,
289 job_title TEXT NOT NULL,
290 job_timestamp BLOB NULL default NULL,
291 job_params BLOB NOT NULL,
292 job_random integer NOT NULL default 0,
293 job_attempts integer NOT NULL default 0,
294 job_token BLOB NOT NULL default '',
295 job_token_timestamp BLOB NULL default NULL,
296 job_sha1 BLOB NOT NULL default ''
297 );
298 CREATE INDEX job_sha1 ON job (job_sha1);
299 CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
300 CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
301 CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
302 CREATE INDEX job_timestamp ON job (job_timestamp);
303EOT;
304 $conn->query( $sql, __METHOD__ );
305 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
306 $conn->close( __METHOD__ );
307 } catch ( DBConnectionError $e ) {
308 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
309 }
310
311 # Open the main DB
312 $mainConnStatus = $this->getConnection();
313 // Use WAL mode. This has better performance
314 // when the DB is being read and written concurrently.
315 // This causes the DB to be created in this mode
316 // so we only have to do this on creation.
317 $mainConnStatus->value->query( "PRAGMA journal_mode=WAL", __METHOD__ );
318 return $mainConnStatus;
319 }
320
326 protected function makeStubDBFile( $dir, $db ) {
327 $file = DatabaseSqlite::generateFileName( $dir, $db );
328
329 if ( file_exists( $file ) ) {
330 if ( !is_writable( $file ) ) {
331 return Status::newFatal( 'config-sqlite-readonly', $file );
332 }
333 return Status::newGood();
334 }
335
336 $oldMask = umask( 0177 );
337 if ( file_put_contents( $file, '' ) === false ) {
338 umask( $oldMask );
339 return Status::newFatal( 'config-sqlite-cant-create-db', $file );
340 }
341 umask( $oldMask );
342
343 return Status::newGood();
344 }
345
349 public function createTables() {
350 $status = parent::createTables();
351 if ( $status->isGood() ) {
352 $status = parent::createManualTables();
353 }
354
355 return $this->setupSearchIndex( $status );
356 }
357
358 public function createManualTables() {
359 // Already handled above. Do nothing.
360 return Status::newGood();
361 }
362
367 public function setupSearchIndex( &$status ) {
368 global $IP;
369
370 $module = DatabaseSqlite::getFulltextSearchModule();
371 $searchIndexSql = (string)$this->db->newSelectQueryBuilder()
372 ->select( 'sql' )
373 ->from( $this->db->addIdentifierQuotes( 'sqlite_master' ) )
374 ->where( [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ] )
375 ->caller( __METHOD__ )->fetchField();
376 $fts3tTable = ( stristr( $searchIndexSql, 'fts' ) !== false );
377
378 if ( $fts3tTable && !$module ) {
379 $status->warning( 'config-sqlite-fts3-downgrade' );
380 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" );
381 } elseif ( !$fts3tTable && $module == 'FTS3' ) {
382 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" );
383 }
384
385 return $status;
386 }
387
391 public function getLocalSettings() {
392 $dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
393 // These tables have frequent writes and are thus split off from the main one.
394 // Since the code using these tables only uses transactions for writes, then set
395 // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on the first write action.
396 return "# SQLite-specific settings
397\$wgSQLiteDataDir = \"{$dir}\";
398\$wgObjectCaches[CACHE_DB] = [
399 'class' => SqlBagOStuff::class,
400 'loggroup' => 'SQLBagOStuff',
401 'server' => [
402 'type' => 'sqlite',
403 'dbname' => 'wikicache',
404 'tablePrefix' => '',
405 'variables' => [ 'synchronous' => 'NORMAL' ],
406 'dbDirectory' => \$wgSQLiteDataDir,
407 'trxMode' => 'IMMEDIATE',
408 'flags' => 0
409 ]
410];
411\$wgLocalisationCacheConf['storeServer'] = [
412 'type' => 'sqlite',
413 'dbname' => \"{\$wgDBname}_l10n_cache\",
414 'tablePrefix' => '',
415 'variables' => [ 'synchronous' => 'NORMAL' ],
416 'dbDirectory' => \$wgSQLiteDataDir,
417 'trxMode' => 'IMMEDIATE',
418 'flags' => 0
419];
420\$wgJobTypeConf['default'] = [
421 'class' => 'JobQueueDB',
422 'claimTTL' => 3600,
423 'server' => [
424 'type' => 'sqlite',
425 'dbname' => \"{\$wgDBname}_jobqueue\",
426 'tablePrefix' => '',
427 'variables' => [ 'synchronous' => 'NORMAL' ],
428 'dbDirectory' => \$wgSQLiteDataDir,
429 'trxMode' => 'IMMEDIATE',
430 'flags' => 0
431 ]
432];
433\$wgResourceLoaderUseObjectCacheForDeps = true;";
434 }
435}
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:98
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Base class for DBMS-specific installation helper classes.
getVar( $var, $default=null)
Get a variable, taking local defaults into account.
static checkExtension( $name)
Convenience function.
getTextBox( $var, $label, $attribs=[], $helpData="")
Get a labelled text box to configure a local variable.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
setVar( $name, $value)
Convenience alias for $this->parent->setVar()
static maybeGetWebserverPrimaryGroup()
On POSIX systems return the primary group of the webserver we're running under.
Class for setting up the MediaWiki database using SQLLite.
createManualTables()
Create database tables from scratch.
getConnectForm()
Get HTML for a web form that configures this database.
getGlobalDefaults()
Get a name=>value map of MW configuration globals for the default values.
getName()
Return the internal name, e.g.
Service locator for MediaWiki core services.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Constructs Database objects.
This is the SQLite database abstraction layer.
static getFulltextSearchModule()
Returns version of currently supported SQLite fulltext search module or false if none present.
static newStandaloneInstance( $filename, array $p=[])