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  $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 ) ) ) {
154  $webserverGroup = Installer::maybeGetWebserverPrimaryGroup();
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", "Deny from all\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  );
273 EOT;
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);
306 EOT;
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  if ( file_exists( $file ) ) {
326  if ( !is_writable( $file ) ) {
327  return Status::newFatal( 'config-sqlite-readonly', $file );
328  }
329  } elseif ( file_put_contents( $file, '' ) === false ) {
330  return Status::newFatal( 'config-sqlite-cant-create-db', $file );
331  }
332 
333  return Status::newGood();
334  }
335 
339  public function createTables() {
340  $status = parent::createTables();
341  if ( $status->isGood() ) {
342  $status = parent::createManualTables();
343  }
344 
345  return $this->setupSearchIndex( $status );
346  }
347 
348  public function createManualTables() {
349  // Already handled above. Do nothing.
350  return Status::newGood();
351  }
352 
357  public function setupSearchIndex( &$status ) {
358  global $IP;
359 
360  $module = DatabaseSqlite::getFulltextSearchModule();
361  $searchIndexSql = (string)$this->db->selectField(
362  $this->db->addIdentifierQuotes( 'sqlite_master' ),
363  'sql',
364  [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ],
365  __METHOD__
366  );
367  $fts3tTable = ( stristr( $searchIndexSql, 'fts' ) !== false );
368 
369  if ( $fts3tTable && !$module ) {
370  $status->warning( 'config-sqlite-fts3-downgrade' );
371  $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" );
372  } elseif ( !$fts3tTable && $module == 'FTS3' ) {
373  $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" );
374  }
375 
376  return $status;
377  }
378 
382  public function getLocalSettings() {
383  $dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
384  // These tables have frequent writes and are thus split off from the main one.
385  // Since the code using these tables only uses transactions for writes then set
386  // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on first write.
387  return "# SQLite-specific settings
388 \$wgSQLiteDataDir = \"{$dir}\";
389 \$wgObjectCaches[CACHE_DB] = [
390  'class' => SqlBagOStuff::class,
391  'loggroup' => 'SQLBagOStuff',
392  'server' => [
393  'type' => 'sqlite',
394  'dbname' => 'wikicache',
395  'tablePrefix' => '',
396  'variables' => [ 'synchronous' => 'NORMAL' ],
397  'dbDirectory' => \$wgSQLiteDataDir,
398  'trxMode' => 'IMMEDIATE',
399  'flags' => 0
400  ]
401 ];
402 \$wgObjectCaches['db-replicated'] = [
403  'factory' => 'Wikimedia\ObjectFactory\ObjectFactory::getObjectFromSpec',
404  'args' => [ [ 'factory' => 'ObjectCache::getInstance', 'args' => [ CACHE_DB ] ] ]
405 ];
406 \$wgLocalisationCacheConf['storeServer'] = [
407  'type' => 'sqlite',
408  'dbname' => \"{\$wgDBname}_l10n_cache\",
409  'tablePrefix' => '',
410  'variables' => [ 'synchronous' => 'NORMAL' ],
411  'dbDirectory' => \$wgSQLiteDataDir,
412  'trxMode' => 'IMMEDIATE',
413  'flags' => 0
414 ];
415 \$wgJobTypeConf['default'] = [
416  'class' => 'JobQueueDB',
417  'claimTTL' => 3600,
418  'server' => [
419  'type' => 'sqlite',
420  'dbname' => \"{\$wgDBname}_jobqueue\",
421  'tablePrefix' => '',
422  'variables' => [ 'synchronous' => 'NORMAL' ],
423  'dbDirectory' => \$wgSQLiteDataDir,
424  'trxMode' => 'IMMEDIATE',
425  'flags' => 0
426  ]
427 ];
428 \$wgResourceLoaderUseObjectCacheForDeps = true;";
429  }
430 }
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.
Definition: Installer.php:749
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:45
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