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", "Require all denied\n" );
187 return Status::newGood();
188 }
189
193 public function openConnection() {
194 $status = Status::newGood();
195 $dir = $this->getVar( 'wgSQLiteDataDir' );
196 $dbName = $this->getVar( 'wgDBname' );
197 try {
198 $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create(
199 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ]
200 );
201 $status->value = $db;
202 } catch ( DBConnectionError $e ) {
203 $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
204 }
205
206 return $status;
207 }
208
212 public function needsUpgrade() {
213 $dir = $this->getVar( 'wgSQLiteDataDir' );
214 $dbName = $this->getVar( 'wgDBname' );
215 // Don't create the data file yet
216 if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) {
217 return false;
218 }
219
220 // If the data file exists, look inside it
221 return parent::needsUpgrade();
222 }
223
227 public function setupDatabase() {
228 $dir = $this->getVar( 'wgSQLiteDataDir' );
229
230 # Double check (Only available in web installation). We checked this before but maybe someone
231 # deleted the data dir between then and now
232 $dir_status = self::checkDataDir( $dir );
233 if ( $dir_status->isGood() ) {
234 $res = self::createDataDir( $dir );
235 if ( !$res->isGood() ) {
236 return $res;
237 }
238 } else {
239 return $dir_status;
240 }
241
242 $db = $this->getVar( 'wgDBname' );
243
244 # Make the main and cache stub DB files
245 $status = Status::newGood();
246 $status->merge( $this->makeStubDBFile( $dir, $db ) );
247 $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
248 $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
249 $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
250 if ( !$status->isOK() ) {
251 return $status;
252 }
253
254 # Nuke the unused settings for clarity
255 $this->setVar( 'wgDBserver', '' );
256 $this->setVar( 'wgDBuser', '' );
257 $this->setVar( 'wgDBpassword', '' );
258 $this->setupSchemaVars();
259
260 # Create the l10n cache DB
261 try {
262 $conn = Database::factory(
263 'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
264 # @todo: don't duplicate l10n_cache definition, though it's very simple
265 $sql =
266<<<EOT
267 CREATE TABLE l10n_cache (
268 lc_lang BLOB NOT NULL,
269 lc_key TEXT NOT NULL,
270 lc_value BLOB NOT NULL,
271 PRIMARY KEY (lc_lang, lc_key)
272 );
273EOT;
274 $conn->query( $sql, __METHOD__ );
275 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
276 $conn->close( __METHOD__ );
277 } catch ( DBConnectionError $e ) {
278 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
279 }
280
281 # Create the job queue DB
282 try {
283 $conn = Database::factory(
284 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
285 # @todo: don't duplicate job definition, though it's very static
286 $sql =
287<<<EOT
288 CREATE TABLE job (
289 job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
290 job_cmd BLOB NOT NULL default '',
291 job_namespace INTEGER NOT NULL,
292 job_title TEXT NOT NULL,
293 job_timestamp BLOB NULL default NULL,
294 job_params BLOB NOT NULL,
295 job_random integer NOT NULL default 0,
296 job_attempts integer NOT NULL default 0,
297 job_token BLOB NOT NULL default '',
298 job_token_timestamp BLOB NULL default NULL,
299 job_sha1 BLOB NOT NULL default ''
300 );
301 CREATE INDEX job_sha1 ON job (job_sha1);
302 CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
303 CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
304 CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
305 CREATE INDEX job_timestamp ON job (job_timestamp);
306EOT;
307 $conn->query( $sql, __METHOD__ );
308 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
309 $conn->close( __METHOD__ );
310 } catch ( DBConnectionError $e ) {
311 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
312 }
313
314 # Open the main DB
315 return $this->getConnection();
316 }
317
323 protected function makeStubDBFile( $dir, $db ) {
324 $file = DatabaseSqlite::generateFileName( $dir, $db );
325
326 if ( file_exists( $file ) ) {
327 if ( !is_writable( $file ) ) {
328 return Status::newFatal( 'config-sqlite-readonly', $file );
329 }
330 return Status::newGood();
331 }
332
333 $oldMask = umask( 0177 );
334 if ( file_put_contents( $file, '' ) === false ) {
335 umask( $oldMask );
336 return Status::newFatal( 'config-sqlite-cant-create-db', $file );
337 }
338 umask( $oldMask );
339
340 return Status::newGood();
341 }
342
346 public function createTables() {
347 $status = parent::createTables();
348 if ( $status->isGood() ) {
349 $status = parent::createManualTables();
350 }
351
352 return $this->setupSearchIndex( $status );
353 }
354
355 public function createManualTables() {
356 // Already handled above. Do nothing.
357 return Status::newGood();
358 }
359
364 public function setupSearchIndex( &$status ) {
365 global $IP;
366
367 $module = DatabaseSqlite::getFulltextSearchModule();
368 $searchIndexSql = (string)$this->db->selectField(
369 $this->db->addIdentifierQuotes( 'sqlite_master' ),
370 'sql',
371 [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ],
372 __METHOD__
373 );
374 $fts3tTable = ( stristr( $searchIndexSql, 'fts' ) !== false );
375
376 if ( $fts3tTable && !$module ) {
377 $status->warning( 'config-sqlite-fts3-downgrade' );
378 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" );
379 } elseif ( !$fts3tTable && $module == 'FTS3' ) {
380 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" );
381 }
382
383 return $status;
384 }
385
389 public function getLocalSettings() {
390 $dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
391 // These tables have frequent writes and are thus split off from the main one.
392 // Since the code using these tables only uses transactions for writes then set
393 // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on first write.
394 return "# SQLite-specific settings
395\$wgSQLiteDataDir = \"{$dir}\";
396\$wgObjectCaches[CACHE_DB] = [
397 'class' => SqlBagOStuff::class,
398 'loggroup' => 'SQLBagOStuff',
399 'server' => [
400 'type' => 'sqlite',
401 'dbname' => 'wikicache',
402 'tablePrefix' => '',
403 'variables' => [ 'synchronous' => 'NORMAL' ],
404 'dbDirectory' => \$wgSQLiteDataDir,
405 'trxMode' => 'IMMEDIATE',
406 'flags' => 0
407 ]
408];
409\$wgObjectCaches['db-replicated'] = [
410 'factory' => 'Wikimedia\ObjectFactory\ObjectFactory::getObjectFromSpec',
411 'args' => [ [ 'factory' => 'ObjectCache::getInstance', 'args' => [ CACHE_DB ] ] ]
412];
413\$wgLocalisationCacheConf['storeServer'] = [
414 'type' => 'sqlite',
415 'dbname' => \"{\$wgDBname}_l10n_cache\",
416 'tablePrefix' => '',
417 'variables' => [ 'synchronous' => 'NORMAL' ],
418 'dbDirectory' => \$wgSQLiteDataDir,
419 'trxMode' => 'IMMEDIATE',
420 'flags' => 0
421];
422\$wgJobTypeConf['default'] = [
423 'class' => 'JobQueueDB',
424 'claimTTL' => 3600,
425 'server' => [
426 'type' => 'sqlite',
427 'dbname' => \"{\$wgDBname}_jobqueue\",
428 'tablePrefix' => '',
429 'variables' => [ 'synchronous' => 'NORMAL' ],
430 'dbDirectory' => \$wgSQLiteDataDir,
431 'trxMode' => 'IMMEDIATE',
432 'flags' => 0
433 ]
434];
435\$wgResourceLoaderUseObjectCacheForDeps = true;";
436 }
437}
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