MediaWiki 1.40.4
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 return realpath( $path ) ?: $path;
114 }
115
119 public function submitConnectForm() {
120 $this->setVarsFromRequest( [ 'wgSQLiteDataDir', 'wgDBname' ] );
121
122 # Try realpath() if the directory already exists
123 $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) );
124 $result = self::checkDataDir( $dir );
125 if ( $result->isOK() ) {
126 # Try expanding again in case we've just created it
127 $dir = self::realpath( $dir );
128 $this->setVar( 'wgSQLiteDataDir', $dir );
129 }
130 # Table prefix is not used on SQLite, keep it empty
131 $this->setVar( 'wgDBprefix', '' );
132
133 return $result;
134 }
135
141 private static function checkDataDir( $dir ): Status {
142 if ( is_dir( $dir ) ) {
143 if ( !is_readable( $dir ) ) {
144 return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
145 }
146 } else {
147 // Check the parent directory if $dir not exists
148 if ( !is_writable( dirname( $dir ) ) ) {
150 if ( $webserverGroup !== null ) {
151 return Status::newFatal(
152 'config-sqlite-parent-unwritable-group',
153 $dir, dirname( $dir ), basename( $dir ),
154 $webserverGroup
155 );
156 } else {
157 return Status::newFatal(
158 'config-sqlite-parent-unwritable-nogroup',
159 $dir, dirname( $dir ), basename( $dir )
160 );
161 }
162 }
163 }
164 return Status::newGood();
165 }
166
171 private static function createDataDir( $dir ): Status {
172 if ( !is_dir( $dir ) ) {
173 AtEase::suppressWarnings();
174 $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
175 AtEase::restoreWarnings();
176 if ( !$ok ) {
177 return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
178 }
179 }
180 # Put a .htaccess file in case the user didn't take our advice
181 file_put_contents( "$dir/.htaccess", "Require all denied\n" );
182 return Status::newGood();
183 }
184
188 public function openConnection() {
189 $status = Status::newGood();
190 $dir = $this->getVar( 'wgSQLiteDataDir' );
191 $dbName = $this->getVar( 'wgDBname' );
192 try {
193 $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create(
194 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ]
195 );
196 $status->value = $db;
197 } catch ( DBConnectionError $e ) {
198 $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
199 }
200
201 return $status;
202 }
203
207 public function needsUpgrade() {
208 $dir = $this->getVar( 'wgSQLiteDataDir' );
209 $dbName = $this->getVar( 'wgDBname' );
210 // Don't create the data file yet
211 if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) {
212 return false;
213 }
214
215 // If the data file exists, look inside it
216 return parent::needsUpgrade();
217 }
218
222 public function setupDatabase() {
223 $dir = $this->getVar( 'wgSQLiteDataDir' );
224
225 # Double check (Only available in web installation). We checked this before but maybe someone
226 # deleted the data dir between then and now
227 $dir_status = self::checkDataDir( $dir );
228 if ( $dir_status->isGood() ) {
229 $res = self::createDataDir( $dir );
230 if ( !$res->isGood() ) {
231 return $res;
232 }
233 } else {
234 return $dir_status;
235 }
236
237 $db = $this->getVar( 'wgDBname' );
238
239 # Make the main and cache stub DB files
240 $status = Status::newGood();
241 $status->merge( $this->makeStubDBFile( $dir, $db ) );
242 $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
243 $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
244 $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
245 if ( !$status->isOK() ) {
246 return $status;
247 }
248
249 # Nuke the unused settings for clarity
250 $this->setVar( 'wgDBserver', '' );
251 $this->setVar( 'wgDBuser', '' );
252 $this->setVar( 'wgDBpassword', '' );
253 $this->setupSchemaVars();
254
255 # Create the l10n cache DB
256 try {
257 $conn = ( new DatabaseFactory() )->create(
258 'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
259 # @todo: don't duplicate l10n_cache definition, though it's very simple
260 $sql =
261<<<EOT
262 CREATE TABLE l10n_cache (
263 lc_lang BLOB NOT NULL,
264 lc_key TEXT NOT NULL,
265 lc_value BLOB NOT NULL,
266 PRIMARY KEY (lc_lang, lc_key)
267 );
268EOT;
269 $conn->query( $sql, __METHOD__ );
270 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
271 $conn->close( __METHOD__ );
272 } catch ( DBConnectionError $e ) {
273 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
274 }
275
276 # Create the job queue DB
277 try {
278 $conn = ( new DatabaseFactory() )->create(
279 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
280 # @todo: don't duplicate job definition, though it's very static
281 $sql =
282<<<EOT
283 CREATE TABLE job (
284 job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
285 job_cmd BLOB NOT NULL default '',
286 job_namespace INTEGER NOT NULL,
287 job_title TEXT NOT NULL,
288 job_timestamp BLOB NULL default NULL,
289 job_params BLOB NOT NULL,
290 job_random integer NOT NULL default 0,
291 job_attempts integer NOT NULL default 0,
292 job_token BLOB NOT NULL default '',
293 job_token_timestamp BLOB NULL default NULL,
294 job_sha1 BLOB NOT NULL default ''
295 );
296 CREATE INDEX job_sha1 ON job (job_sha1);
297 CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
298 CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
299 CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
300 CREATE INDEX job_timestamp ON job (job_timestamp);
301EOT;
302 $conn->query( $sql, __METHOD__ );
303 $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
304 $conn->close( __METHOD__ );
305 } catch ( DBConnectionError $e ) {
306 return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
307 }
308
309 # Open the main DB
310 return $this->getConnection();
311 }
312
318 protected function makeStubDBFile( $dir, $db ) {
319 $file = DatabaseSqlite::generateFileName( $dir, $db );
320
321 if ( file_exists( $file ) ) {
322 if ( !is_writable( $file ) ) {
323 return Status::newFatal( 'config-sqlite-readonly', $file );
324 }
325 return Status::newGood();
326 }
327
328 $oldMask = umask( 0177 );
329 if ( file_put_contents( $file, '' ) === false ) {
330 umask( $oldMask );
331 return Status::newFatal( 'config-sqlite-cant-create-db', $file );
332 }
333 umask( $oldMask );
334
335 return Status::newGood();
336 }
337
341 public function createTables() {
342 $status = parent::createTables();
343 if ( $status->isGood() ) {
344 $status = parent::createManualTables();
345 }
346
347 return $this->setupSearchIndex( $status );
348 }
349
350 public function createManualTables() {
351 // Already handled above. Do nothing.
352 return Status::newGood();
353 }
354
359 public function setupSearchIndex( &$status ) {
360 global $IP;
361
362 $module = DatabaseSqlite::getFulltextSearchModule();
363 $searchIndexSql = (string)$this->db->selectField(
364 $this->db->addIdentifierQuotes( 'sqlite_master' ),
365 'sql',
366 [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ],
367 __METHOD__
368 );
369 $fts3tTable = ( stristr( $searchIndexSql, 'fts' ) !== false );
370
371 if ( $fts3tTable && !$module ) {
372 $status->warning( 'config-sqlite-fts3-downgrade' );
373 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" );
374 } elseif ( !$fts3tTable && $module == 'FTS3' ) {
375 $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" );
376 }
377
378 return $status;
379 }
380
384 public function getLocalSettings() {
385 $dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
386 // These tables have frequent writes and are thus split off from the main one.
387 // Since the code using these tables only uses transactions for writes then set
388 // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on first write.
389 return "# SQLite-specific settings
390\$wgSQLiteDataDir = \"{$dir}\";
391\$wgObjectCaches[CACHE_DB] = [
392 'class' => SqlBagOStuff::class,
393 'loggroup' => 'SQLBagOStuff',
394 'server' => [
395 'type' => 'sqlite',
396 'dbname' => 'wikicache',
397 'tablePrefix' => '',
398 'variables' => [ 'synchronous' => 'NORMAL' ],
399 'dbDirectory' => \$wgSQLiteDataDir,
400 'trxMode' => 'IMMEDIATE',
401 'flags' => 0
402 ]
403];
404\$wgObjectCaches['db-replicated'] = [
405 'factory' => 'Wikimedia\ObjectFactory\ObjectFactory::getObjectFromSpec',
406 'args' => [ [ 'factory' => 'ObjectCache::getInstance', 'args' => [ CACHE_DB ] ] ]
407];
408\$wgLocalisationCacheConf['storeServer'] = [
409 'type' => 'sqlite',
410 'dbname' => \"{\$wgDBname}_l10n_cache\",
411 'tablePrefix' => '',
412 'variables' => [ 'synchronous' => 'NORMAL' ],
413 'dbDirectory' => \$wgSQLiteDataDir,
414 'trxMode' => 'IMMEDIATE',
415 'flags' => 0
416];
417\$wgJobTypeConf['default'] = [
418 'class' => 'JobQueueDB',
419 'claimTTL' => 3600,
420 'server' => [
421 'type' => 'sqlite',
422 'dbname' => \"{\$wgDBname}_jobqueue\",
423 'tablePrefix' => '',
424 'variables' => [ 'synchronous' => 'NORMAL' ],
425 'dbDirectory' => \$wgSQLiteDataDir,
426 'trxMode' => 'IMMEDIATE',
427 'flags' => 0
428 ]
429];
430\$wgResourceLoaderUseObjectCacheForDeps = true;";
431 }
432}
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:93
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:88
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:46
Constructs Database objects.
This is the SQLite database abstraction layer.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42