MediaWiki  master
SqliteInstaller.php
Go to the documentation of this file.
1 <?php
25 use 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->getServerVersion() );
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 ) ) ) {
149  $webserverGroup = Installer::maybeGetWebserverPrimaryGroup();
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", "Deny from all\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  );
268 EOT;
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);
301 EOT;
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.
Definition: Installer.php:766
static escapePhpString( $string)
Returns the escaped version of a string of php code.
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.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
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