MediaWiki  master
SqliteInstaller.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\AtEase\AtEase;
30 
38 
39  public static $minimumVersion = '3.8.0';
40  protected static $notMinimumVersionMessage = 'config-outdated-sqlite';
41 
45  public $db;
46 
47  protected $globalNames = [
48  'wgDBname',
49  'wgSQLiteDataDir',
50  ];
51 
52  public function getName() {
53  return 'sqlite';
54  }
55 
56  public function isCompiled() {
57  return self::checkExtension( 'pdo_sqlite' );
58  }
59 
63  public function checkPrerequisites() {
64  // Bail out if SQLite is too old
65  $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
66  $result = static::meetsMinimumRequirement( $db );
67  // Check for FTS3 full-text search module
68  if ( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) {
69  $result->warning( 'config-no-fts3' );
70  }
71 
72  return $result;
73  }
74 
75  public function getGlobalDefaults() {
76  global $IP;
77  $defaults = parent::getGlobalDefaults();
78  if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
79  $path = dirname( $_SERVER['DOCUMENT_ROOT'] );
80  } else {
81  // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT']
82  $path = $IP;
83  }
84  $defaults['wgSQLiteDataDir'] = str_replace(
85  [ '/', '\\' ],
86  DIRECTORY_SEPARATOR,
87  $path . '/data'
88  );
89  return $defaults;
90  }
91 
92  public function getConnectForm() {
93  return $this->getTextBox(
94  'wgSQLiteDataDir',
95  'config-sqlite-dir', [],
96  $this->parent->getHelpBox( 'config-sqlite-dir-help' )
97  ) .
98  $this->getTextBox(
99  'wgDBname',
100  'config-db-name',
101  [],
102  $this->parent->getHelpBox( 'config-sqlite-name-help' )
103  );
104  }
105 
113  private static function realpath( $path ) {
114  return realpath( $path ) ?: $path;
115  }
116 
120  public function submitConnectForm() {
121  $this->setVarsFromRequest( [ 'wgSQLiteDataDir', 'wgDBname' ] );
122 
123  # Try realpath() if the directory already exists
124  $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) );
125  $result = self::checkDataDir( $dir );
126  if ( $result->isOK() ) {
127  # Try expanding again in case we've just created it
128  $dir = self::realpath( $dir );
129  $this->setVar( 'wgSQLiteDataDir', $dir );
130  }
131  # Table prefix is not used on SQLite, keep it empty
132  $this->setVar( 'wgDBprefix', '' );
133 
134  return $result;
135  }
136 
142  private static function checkDataDir( $dir ): Status {
143  if ( is_dir( $dir ) ) {
144  if ( !is_readable( $dir ) ) {
145  return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
146  }
147  } else {
148  // Check the parent directory if $dir not exists
149  if ( !is_writable( dirname( $dir ) ) ) {
150  $webserverGroup = Installer::maybeGetWebserverPrimaryGroup();
151  if ( $webserverGroup !== null ) {
152  return Status::newFatal(
153  'config-sqlite-parent-unwritable-group',
154  $dir, dirname( $dir ), basename( $dir ),
155  $webserverGroup
156  );
157  } else {
158  return Status::newFatal(
159  'config-sqlite-parent-unwritable-nogroup',
160  $dir, dirname( $dir ), basename( $dir )
161  );
162  }
163  }
164  }
165  return Status::newGood();
166  }
167 
172  private static function createDataDir( $dir ): Status {
173  if ( !is_dir( $dir ) ) {
174  AtEase::suppressWarnings();
175  $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
176  AtEase::restoreWarnings();
177  if ( !$ok ) {
178  return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
179  }
180  }
181  # Put a .htaccess file in case the user didn't take our advice
182  file_put_contents( "$dir/.htaccess", "Deny from all\n" );
183  return Status::newGood();
184  }
185 
189  public function openConnection() {
190  $status = Status::newGood();
191  $dir = $this->getVar( 'wgSQLiteDataDir' );
192  $dbName = $this->getVar( 'wgDBname' );
193  try {
194  $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create(
195  'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ]
196  );
197  $status->value = $db;
198  } catch ( DBConnectionError $e ) {
199  $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
200  }
201 
202  return $status;
203  }
204 
208  public function needsUpgrade() {
209  $dir = $this->getVar( 'wgSQLiteDataDir' );
210  $dbName = $this->getVar( 'wgDBname' );
211  // Don't create the data file yet
212  if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) {
213  return false;
214  }
215 
216  // If the data file exists, look inside it
217  return parent::needsUpgrade();
218  }
219 
223  public function setupDatabase() {
224  $dir = $this->getVar( 'wgSQLiteDataDir' );
225 
226  # Double check (Only available in web installation). We checked this before but maybe someone
227  # deleted the data dir between then and now
228  $dir_status = self::checkDataDir( $dir );
229  if ( $dir_status->isGood() ) {
230  $res = self::createDataDir( $dir );
231  if ( !$res->isGood() ) {
232  return $res;
233  }
234  } else {
235  return $dir_status;
236  }
237 
238  $db = $this->getVar( 'wgDBname' );
239 
240  # Make the main and cache stub DB files
241  $status = Status::newGood();
242  $status->merge( $this->makeStubDBFile( $dir, $db ) );
243  $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
244  $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
245  $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
246  if ( !$status->isOK() ) {
247  return $status;
248  }
249 
250  # Nuke the unused settings for clarity
251  $this->setVar( 'wgDBserver', '' );
252  $this->setVar( 'wgDBuser', '' );
253  $this->setVar( 'wgDBpassword', '' );
254  $this->setupSchemaVars();
255 
256  # Create the l10n cache DB
257  try {
258  $conn = ( new DatabaseFactory() )->create(
259  'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
260  # @todo: don't duplicate l10n_cache definition, though it's very simple
261  $sql =
262 <<<EOT
263  CREATE TABLE l10n_cache (
264  lc_lang BLOB NOT NULL,
265  lc_key TEXT NOT NULL,
266  lc_value BLOB NOT NULL,
267  PRIMARY KEY (lc_lang, lc_key)
268  );
269 EOT;
270  $conn->query( $sql, __METHOD__ );
271  $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
272  $conn->close( __METHOD__ );
273  } catch ( DBConnectionError $e ) {
274  return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
275  }
276 
277  # Create the job queue DB
278  try {
279  $conn = ( new DatabaseFactory() )->create(
280  'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
281  # @todo: don't duplicate job definition, though it's very static
282  $sql =
283 <<<EOT
284  CREATE TABLE job (
285  job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
286  job_cmd BLOB NOT NULL default '',
287  job_namespace INTEGER NOT NULL,
288  job_title TEXT NOT NULL,
289  job_timestamp BLOB NULL default NULL,
290  job_params BLOB NOT NULL,
291  job_random integer NOT NULL default 0,
292  job_attempts integer NOT NULL default 0,
293  job_token BLOB NOT NULL default '',
294  job_token_timestamp BLOB NULL default NULL,
295  job_sha1 BLOB NOT NULL default ''
296  );
297  CREATE INDEX job_sha1 ON job (job_sha1);
298  CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
299  CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
300  CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
301  CREATE INDEX job_timestamp ON job (job_timestamp);
302 EOT;
303  $conn->query( $sql, __METHOD__ );
304  $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
305  $conn->close( __METHOD__ );
306  } catch ( DBConnectionError $e ) {
307  return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
308  }
309 
310  # Open the main DB
311  return $this->getConnection();
312  }
313 
319  protected function makeStubDBFile( $dir, $db ) {
320  $file = DatabaseSqlite::generateFileName( $dir, $db );
321 
322  if ( file_exists( $file ) ) {
323  if ( !is_writable( $file ) ) {
324  return Status::newFatal( 'config-sqlite-readonly', $file );
325  }
326  return Status::newGood();
327  }
328 
329  $oldMask = umask( 0177 );
330  if ( file_put_contents( $file, '' ) === false ) {
331  umask( $oldMask );
332  return Status::newFatal( 'config-sqlite-cant-create-db', $file );
333  }
334  umask( $oldMask );
335 
336  return Status::newGood();
337  }
338 
342  public function createTables() {
343  $status = parent::createTables();
344  if ( $status->isGood() ) {
345  $status = parent::createManualTables();
346  }
347 
348  return $this->setupSearchIndex( $status );
349  }
350 
351  public function createManualTables() {
352  // Already handled above. Do nothing.
353  return Status::newGood();
354  }
355 
360  public function setupSearchIndex( &$status ) {
361  global $IP;
362 
363  $module = DatabaseSqlite::getFulltextSearchModule();
364  $searchIndexSql = (string)$this->db->newSelectQueryBuilder()
365  ->select( 'sql' )
366  ->from( $this->db->addIdentifierQuotes( 'sqlite_master' ) )
367  ->where( [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ] )
368  ->caller( __METHOD__ )->fetchField();
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:96
if(!defined('MW_SETUP_CALLBACK'))
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:757
static escapePhpString( $string)
Returns the escaped version of a string of php code.
Service locator for MediaWiki core services.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
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.
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