MediaWiki REL1_39
SqliteInstaller.php
Go to the documentation of this file.
1<?php
25use Wikimedia\AtEase\AtEase;
29
37
38 public static $minimumVersion = '3.8.0';
39 protected static $notMinimumVersionMessage = 'config-outdated-sqlite';
40
44 public $db;
45
46 protected $globalNames = [
47 'wgDBname',
48 'wgSQLiteDataDir',
49 ];
50
51 public function getName() {
52 return 'sqlite';
53 }
54
55 public function isCompiled() {
56 return self::checkExtension( 'pdo_sqlite' );
57 }
58
62 public function checkPrerequisites() {
63 // Bail out if SQLite is too old
64 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
65 $result = static::meetsMinimumRequirement( $db );
66 // Check for FTS3 full-text search module
67 if ( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) {
68 $result->warning( 'config-no-fts3' );
69 }
70
71 return $result;
72 }
73
74 public function getGlobalDefaults() {
75 global $IP;
76 $defaults = parent::getGlobalDefaults();
77 if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
78 $path = dirname( $_SERVER['DOCUMENT_ROOT'] );
79 } else {
80 // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT']
81 $path = $IP;
82 }
83 $defaults['wgSQLiteDataDir'] = str_replace(
84 [ '/', '\\' ],
85 DIRECTORY_SEPARATOR,
86 $path . '/data'
87 );
88 return $defaults;
89 }
90
91 public function getConnectForm() {
92 return $this->getTextBox(
93 'wgSQLiteDataDir',
94 'config-sqlite-dir', [],
95 $this->parent->getHelpBox( 'config-sqlite-dir-help' )
96 ) .
97 $this->getTextBox(
98 'wgDBname',
99 'config-db-name',
100 [],
101 $this->parent->getHelpBox( 'config-sqlite-name-help' )
102 );
103 }
104
112 private static function realpath( $path ) {
113 $result = realpath( $path );
114 if ( !$result ) {
115 return $path;
116 }
117
118 return $result;
119 }
120
124 public function submitConnectForm() {
125 $this->setVarsFromRequest( [ 'wgSQLiteDataDir', 'wgDBname' ] );
126
127 # Try realpath() if the directory already exists
128 $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) );
129 $result = self::checkDataDir( $dir );
130 if ( $result->isOK() ) {
131 # Try expanding again in case we've just created it
132 $dir = self::realpath( $dir );
133 $this->setVar( 'wgSQLiteDataDir', $dir );
134 }
135 # Table prefix is not used on SQLite, keep it empty
136 $this->setVar( 'wgDBprefix', '' );
137
138 return $result;
139 }
140
146 private static function checkDataDir( $dir ): Status {
147 if ( is_dir( $dir ) ) {
148 if ( !is_readable( $dir ) ) {
149 return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
150 }
151 } else {
152 // Check the parent directory if $dir not exists
153 if ( !is_writable( dirname( $dir ) ) ) {
155 if ( $webserverGroup !== null ) {
156 return Status::newFatal(
157 'config-sqlite-parent-unwritable-group',
158 $dir, dirname( $dir ), basename( $dir ),
159 $webserverGroup
160 );
161 } else {
162 return Status::newFatal(
163 'config-sqlite-parent-unwritable-nogroup',
164 $dir, dirname( $dir ), basename( $dir )
165 );
166 }
167 }
168 }
169 return Status::newGood();
170 }
171
176 private static function createDataDir( $dir ): Status {
177 if ( !is_dir( $dir ) ) {
178 AtEase::suppressWarnings();
179 $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
180 AtEase::restoreWarnings();
181 if ( !$ok ) {
182 return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
183 }
184 }
185 # Put a .htaccess file in case the user didn't take our advice
186 file_put_contents( "$dir/.htaccess",
187 "Require all denied\n" .
188 "Satisfy All\n" );
189 return Status::newGood();
190 }
191
195 public function openConnection() {
196 $status = Status::newGood();
197 $dir = $this->getVar( 'wgSQLiteDataDir' );
198 $dbName = $this->getVar( 'wgDBname' );
199 try {
200 $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create(
201 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ]
202 );
203 $status->value = $db;
204 } catch ( DBConnectionError $e ) {
205 $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
206 }
207
208 return $status;
209 }
210
214 public function needsUpgrade() {
215 $dir = $this->getVar( 'wgSQLiteDataDir' );
216 $dbName = $this->getVar( 'wgDBname' );
217 // Don't create the data file yet
218 if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) {
219 return false;
220 }
221
222 // If the data file exists, look inside it
223 return parent::needsUpgrade();
224 }
225
229 public function setupDatabase() {
230 $dir = $this->getVar( 'wgSQLiteDataDir' );
231
232 # Double check (Only available in web installation). We checked this before but maybe someone
233 # deleted the data dir between then and now
234 $dir_status = self::checkDataDir( $dir );
235 if ( $dir_status->isGood() ) {
236 $res = self::createDataDir( $dir );
237 if ( !$res->isGood() ) {
238 return $res;
239 }
240 } else {
241 return $dir_status;
242 }
243
244 $db = $this->getVar( 'wgDBname' );
245
246 # Make the main and cache stub DB files
247 $status = Status::newGood();
248 $status->merge( $this->makeStubDBFile( $dir, $db ) );
249 $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
250 $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
251 $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
252 if ( !$status->isOK() ) {
253 return $status;
254 }
255
256 # Nuke the unused settings for clarity
257 $this->setVar( 'wgDBserver', '' );
258 $this->setVar( 'wgDBuser', '' );
259 $this->setVar( 'wgDBpassword', '' );
260 $this->setupSchemaVars();
261
262 # Create the l10n cache DB
263 try {
264 $conn = Database::factory(
265 'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
266 # @todo: don't duplicate l10n_cache definition, though it's very simple
267 $sql =
268<<<EOT
269 CREATE TABLE l10n_cache (
270 lc_lang BLOB NOT NULL,
271 lc_key TEXT NOT NULL,
272 lc_value BLOB NOT NULL,
273 PRIMARY KEY (lc_lang, lc_key)
274 );
275EOT;
276 $conn->query( $sql, __METHOD__ );
277 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
278 $conn->close( __METHOD__ );
279 } catch ( DBConnectionError $e ) {
280 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
281 }
282
283 # Create the job queue DB
284 try {
285 $conn = Database::factory(
286 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
287 # @todo: don't duplicate job definition, though it's very static
288 $sql =
289<<<EOT
290 CREATE TABLE job (
291 job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
292 job_cmd BLOB NOT NULL default '',
293 job_namespace INTEGER NOT NULL,
294 job_title TEXT NOT NULL,
295 job_timestamp BLOB NULL default NULL,
296 job_params BLOB NOT NULL,
297 job_random integer NOT NULL default 0,
298 job_attempts integer NOT NULL default 0,
299 job_token BLOB NOT NULL default '',
300 job_token_timestamp BLOB NULL default NULL,
301 job_sha1 BLOB NOT NULL default ''
302 );
303 CREATE INDEX job_sha1 ON job (job_sha1);
304 CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
305 CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
306 CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
307 CREATE INDEX job_timestamp ON job (job_timestamp);
308EOT;
309 $conn->query( $sql, __METHOD__ );
310 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
311 $conn->close( __METHOD__ );
312 } catch ( DBConnectionError $e ) {
313 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
314 }
315
316 # Open the main DB
317 return $this->getConnection();
318 }
319
325 protected function makeStubDBFile( $dir, $db ) {
326 $file = DatabaseSqlite::generateFileName( $dir, $db );
327
328 if ( file_exists( $file ) ) {
329 if ( !is_writable( $file ) ) {
330 return Status::newFatal( 'config-sqlite-readonly', $file );
331 }
332 return Status::newGood();
333 }
334
335 $oldMask = umask( 0177 );
336 if ( file_put_contents( $file, '' ) === false ) {
337 umask( $oldMask );
338 return Status::newFatal( 'config-sqlite-cant-create-db', $file );
339 }
340 umask( $oldMask );
341
342 return Status::newGood();
343 }
344
348 public function createTables() {
349 $status = parent::createTables();
350 if ( $status->isGood() ) {
351 $status = parent::createManualTables();
352 }
353
354 return $this->setupSearchIndex( $status );
355 }
356
357 public function createManualTables() {
358 // Already handled above. Do nothing.
359 return Status::newGood();
360 }
361
366 public function setupSearchIndex( &$status ) {
367 global $IP;
368
369 $module = DatabaseSqlite::getFulltextSearchModule();
370 $searchIndexSql = (string)$this->db->selectField(
371 $this->db->addIdentifierQuotes( 'sqlite_master' ),
372 'sql',
373 [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ],
374 __METHOD__
375 );
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 first write.
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\$wgObjectCaches['db-replicated'] = [
412 'factory' => 'Wikimedia\ObjectFactory\ObjectFactory::getObjectFromSpec',
413 'args' => [ [ 'factory' => 'ObjectCache::getInstance', 'args' => [ CACHE_DB ] ] ]
414];
415\$wgLocalisationCacheConf['storeServer'] = [
416 'type' => 'sqlite',
417 'dbname' => \"{\$wgDBname}_l10n_cache\",
418 'tablePrefix' => '',
419 'variables' => [ 'synchronous' => 'NORMAL' ],
420 'dbDirectory' => \$wgSQLiteDataDir,
421 'trxMode' => 'IMMEDIATE',
422 'flags' => 0
423];
424\$wgJobTypeConf['default'] = [
425 'class' => 'JobQueueDB',
426 'claimTTL' => 3600,
427 'server' => [
428 'type' => 'sqlite',
429 'dbname' => \"{\$wgDBname}_jobqueue\",
430 'tablePrefix' => '',
431 'variables' => [ 'synchronous' => 'NORMAL' ],
432 'dbDirectory' => \$wgSQLiteDataDir,
433 'trxMode' => 'IMMEDIATE',
434 'flags' => 0
435 ]
436];
437\$wgResourceLoaderUseObjectCacheForDeps = true;";
438 }
439}
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:91
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Base class for DBMS-specific installation helper classes.
static checkExtension( $name)
Convenience function.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
getVar( $var, $default=null)
Get a variable, taking local defaults into account.
getTextBox( $var, $label, $attribs=[], $helpData="")
Get a labelled text box to configure a local variable.
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.
Service locator for MediaWiki core services.
Class for setting up the MediaWiki database using SQLLite.
setupSearchIndex(&$status)
getGlobalDefaults()
Get a name=>value map of MW configuration globals for the default values.
makeStubDBFile( $dir, $db)
getName()
Return the internal name, e.g.
DatabaseSqlite $db
getConnectForm()
Get HTML for a web form that configures this database.
static $notMinimumVersionMessage
createManualTables()
Create database tables from scratch.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
This is the SQLite database abstraction layer.
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:379
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42