Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.62% |
20 / 128 |
|
5.88% |
1 / 17 |
CRAP | |
0.00% |
0 / 1 |
SqliteInstaller | |
15.62% |
20 / 128 |
|
5.88% |
1 / 17 |
1050.74 | |
0.00% |
0 / 1 |
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCompiled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConnectForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSettingsForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkPrerequisites | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getGlobalDefaults | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
realpath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
checkDataDir | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
5.39 | |||
createDataDir | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
openConnection | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
needsUpgrade | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
setupDatabase | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
makeStubDBFile | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
createTables | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
createManualTables | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setupSearchIndex | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
getLocalSettings | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Sqlite-specific installer. |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @ingroup Installer |
23 | */ |
24 | |
25 | namespace MediaWiki\Installer; |
26 | |
27 | use MediaWiki\MediaWikiServices; |
28 | use MediaWiki\Status\Status; |
29 | use Wikimedia\AtEase\AtEase; |
30 | use Wikimedia\Rdbms\DatabaseFactory; |
31 | use Wikimedia\Rdbms\DatabaseSqlite; |
32 | use Wikimedia\Rdbms\DBConnectionError; |
33 | |
34 | /** |
35 | * Class for setting up the MediaWiki database using SQLLite. |
36 | * |
37 | * @ingroup Installer |
38 | * @since 1.17 |
39 | */ |
40 | class SqliteInstaller extends DatabaseInstaller { |
41 | |
42 | public static $minimumVersion = '3.8.0'; |
43 | protected static $notMinimumVersionMessage = 'config-outdated-sqlite'; |
44 | |
45 | /** |
46 | * @var DatabaseSqlite |
47 | */ |
48 | public $db; |
49 | |
50 | protected $globalNames = [ |
51 | 'wgDBname', |
52 | 'wgSQLiteDataDir', |
53 | ]; |
54 | |
55 | public function getName() { |
56 | return 'sqlite'; |
57 | } |
58 | |
59 | public function isCompiled() { |
60 | return self::checkExtension( 'pdo_sqlite' ); |
61 | } |
62 | |
63 | public function getConnectForm( WebInstaller $webInstaller ): DatabaseConnectForm { |
64 | return new SqliteConnectForm( $webInstaller, $this ); |
65 | } |
66 | |
67 | public function getSettingsForm( WebInstaller $webInstaller ): DatabaseSettingsForm { |
68 | return new DatabaseSettingsForm( $webInstaller, $this ); |
69 | } |
70 | |
71 | /** |
72 | * @return Status |
73 | */ |
74 | public function checkPrerequisites() { |
75 | // Bail out if SQLite is too old |
76 | $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); |
77 | $result = static::meetsMinimumRequirement( $db ); |
78 | // Check for FTS3 full-text search module |
79 | if ( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) { |
80 | $result->warning( 'config-no-fts3' ); |
81 | } |
82 | |
83 | return $result; |
84 | } |
85 | |
86 | public function getGlobalDefaults() { |
87 | global $IP; |
88 | $defaults = parent::getGlobalDefaults(); |
89 | if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
90 | $path = dirname( $_SERVER['DOCUMENT_ROOT'] ); |
91 | } else { |
92 | // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT'] |
93 | $path = $IP; |
94 | } |
95 | $defaults['wgSQLiteDataDir'] = str_replace( |
96 | [ '/', '\\' ], |
97 | DIRECTORY_SEPARATOR, |
98 | $path . '/data' |
99 | ); |
100 | return $defaults; |
101 | } |
102 | |
103 | /** |
104 | * Safe wrapper for PHP's realpath() that fails gracefully if it's unable to canonicalize the path. |
105 | * |
106 | * @param string $path |
107 | * |
108 | * @return string |
109 | */ |
110 | public static function realpath( $path ) { |
111 | return realpath( $path ) ?: $path; |
112 | } |
113 | |
114 | /** |
115 | * Check if the data directory is writable or can be created |
116 | * @param string $dir Path to the data directory |
117 | * @return Status Return fatal Status if $dir un-writable or no permission to create a directory |
118 | */ |
119 | public static function checkDataDir( $dir ): Status { |
120 | if ( is_dir( $dir ) ) { |
121 | if ( !is_readable( $dir ) ) { |
122 | return Status::newFatal( 'config-sqlite-dir-unwritable', $dir ); |
123 | } |
124 | } elseif ( !is_writable( dirname( $dir ) ) ) { |
125 | // Check the parent directory if $dir not exists |
126 | $webserverGroup = Installer::maybeGetWebserverPrimaryGroup(); |
127 | if ( $webserverGroup !== null ) { |
128 | return Status::newFatal( |
129 | 'config-sqlite-parent-unwritable-group', |
130 | $dir, dirname( $dir ), basename( $dir ), |
131 | $webserverGroup |
132 | ); |
133 | } |
134 | |
135 | return Status::newFatal( |
136 | 'config-sqlite-parent-unwritable-nogroup', |
137 | $dir, dirname( $dir ), basename( $dir ) |
138 | ); |
139 | } |
140 | return Status::newGood(); |
141 | } |
142 | |
143 | /** |
144 | * @param string $dir Path to the data directory |
145 | * @return Status Return good Status if without error |
146 | */ |
147 | private static function createDataDir( $dir ): Status { |
148 | if ( !is_dir( $dir ) ) { |
149 | AtEase::suppressWarnings(); |
150 | $ok = wfMkdirParents( $dir, 0700, __METHOD__ ); |
151 | AtEase::restoreWarnings(); |
152 | if ( !$ok ) { |
153 | return Status::newFatal( 'config-sqlite-mkdir-error', $dir ); |
154 | } |
155 | } |
156 | # Put a .htaccess file in case the user didn't take our advice |
157 | file_put_contents( "$dir/.htaccess", "Require all denied\n" ); |
158 | return Status::newGood(); |
159 | } |
160 | |
161 | /** |
162 | * @return ConnectionStatus |
163 | */ |
164 | public function openConnection() { |
165 | $status = new ConnectionStatus; |
166 | $dir = $this->getVar( 'wgSQLiteDataDir' ); |
167 | $dbName = $this->getVar( 'wgDBname' ); |
168 | try { |
169 | $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create( |
170 | 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ] |
171 | ); |
172 | $status->setDB( $db ); |
173 | } catch ( DBConnectionError $e ) { |
174 | $status->fatal( 'config-sqlite-connection-error', $e->getMessage() ); |
175 | } |
176 | |
177 | return $status; |
178 | } |
179 | |
180 | /** |
181 | * @return bool |
182 | */ |
183 | public function needsUpgrade() { |
184 | $dir = $this->getVar( 'wgSQLiteDataDir' ); |
185 | $dbName = $this->getVar( 'wgDBname' ); |
186 | // Don't create the data file yet |
187 | if ( !file_exists( DatabaseSqlite::generateFileName( $dir, $dbName ) ) ) { |
188 | return false; |
189 | } |
190 | |
191 | // If the data file exists, look inside it |
192 | return parent::needsUpgrade(); |
193 | } |
194 | |
195 | /** |
196 | * @return Status |
197 | */ |
198 | public function setupDatabase() { |
199 | $dir = $this->getVar( 'wgSQLiteDataDir' ); |
200 | |
201 | # Double check (Only available in web installation). We checked this before but maybe someone |
202 | # deleted the data dir between then and now |
203 | $dir_status = self::checkDataDir( $dir ); |
204 | if ( $dir_status->isGood() ) { |
205 | $res = self::createDataDir( $dir ); |
206 | if ( !$res->isGood() ) { |
207 | return $res; |
208 | } |
209 | } else { |
210 | return $dir_status; |
211 | } |
212 | |
213 | $db = $this->getVar( 'wgDBname' ); |
214 | |
215 | # Make the main and cache stub DB files |
216 | $status = Status::newGood(); |
217 | $status->merge( $this->makeStubDBFile( $dir, $db ) ); |
218 | $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) ); |
219 | $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) ); |
220 | $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) ); |
221 | if ( !$status->isOK() ) { |
222 | return $status; |
223 | } |
224 | |
225 | # Nuke the unused settings for clarity |
226 | $this->setVar( 'wgDBserver', '' ); |
227 | $this->setVar( 'wgDBuser', '' ); |
228 | $this->setVar( 'wgDBpassword', '' ); |
229 | $this->setupSchemaVars(); |
230 | |
231 | # Create the l10n cache DB |
232 | try { |
233 | $conn = ( new DatabaseFactory() )->create( |
234 | 'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] ); |
235 | # @todo: don't duplicate l10n_cache definition, though it's very simple |
236 | $sql = |
237 | <<<EOT |
238 | CREATE TABLE l10n_cache ( |
239 | lc_lang BLOB NOT NULL, |
240 | lc_key TEXT NOT NULL, |
241 | lc_value BLOB NOT NULL, |
242 | PRIMARY KEY (lc_lang, lc_key) |
243 | ); |
244 | EOT; |
245 | $conn->query( $sql, __METHOD__ ); |
246 | $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent |
247 | $conn->close( __METHOD__ ); |
248 | } catch ( DBConnectionError $e ) { |
249 | return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() ); |
250 | } |
251 | |
252 | # Create the job queue DB |
253 | try { |
254 | $conn = ( new DatabaseFactory() )->create( |
255 | 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] ); |
256 | # @todo: don't duplicate job definition, though it's very static |
257 | $sql = |
258 | <<<EOT |
259 | CREATE TABLE job ( |
260 | job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
261 | job_cmd BLOB NOT NULL default '', |
262 | job_namespace INTEGER NOT NULL, |
263 | job_title TEXT NOT NULL, |
264 | job_timestamp BLOB NULL default NULL, |
265 | job_params BLOB NOT NULL, |
266 | job_random integer NOT NULL default 0, |
267 | job_attempts integer NOT NULL default 0, |
268 | job_token BLOB NOT NULL default '', |
269 | job_token_timestamp BLOB NULL default NULL, |
270 | job_sha1 BLOB NOT NULL default '' |
271 | ); |
272 | CREATE INDEX job_sha1 ON job (job_sha1); |
273 | CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random); |
274 | CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id); |
275 | CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params); |
276 | CREATE INDEX job_timestamp ON job (job_timestamp); |
277 | EOT; |
278 | $conn->query( $sql, __METHOD__ ); |
279 | $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent |
280 | $conn->close( __METHOD__ ); |
281 | } catch ( DBConnectionError $e ) { |
282 | return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() ); |
283 | } |
284 | |
285 | # Open the main DB |
286 | $mainConnStatus = $this->getConnection(); |
287 | // Use WAL mode. This has better performance |
288 | // when the DB is being read and written concurrently. |
289 | // This causes the DB to be created in this mode |
290 | // so we only have to do this on creation. |
291 | $mainConnStatus->getDB()->query( "PRAGMA journal_mode=WAL", __METHOD__ ); |
292 | return $mainConnStatus; |
293 | } |
294 | |
295 | /** |
296 | * @param string $dir |
297 | * @param string $db |
298 | * @return Status |
299 | */ |
300 | protected function makeStubDBFile( $dir, $db ) { |
301 | $file = DatabaseSqlite::generateFileName( $dir, $db ); |
302 | |
303 | if ( file_exists( $file ) ) { |
304 | if ( !is_writable( $file ) ) { |
305 | return Status::newFatal( 'config-sqlite-readonly', $file ); |
306 | } |
307 | return Status::newGood(); |
308 | } |
309 | |
310 | $oldMask = umask( 0177 ); |
311 | if ( file_put_contents( $file, '' ) === false ) { |
312 | umask( $oldMask ); |
313 | return Status::newFatal( 'config-sqlite-cant-create-db', $file ); |
314 | } |
315 | umask( $oldMask ); |
316 | |
317 | return Status::newGood(); |
318 | } |
319 | |
320 | /** |
321 | * @return Status |
322 | */ |
323 | public function createTables() { |
324 | $status = parent::createTables(); |
325 | if ( $status->isGood() ) { |
326 | $status = parent::createManualTables(); |
327 | } |
328 | |
329 | return $this->setupSearchIndex( $status ); |
330 | } |
331 | |
332 | public function createManualTables() { |
333 | // Already handled above. Do nothing. |
334 | return Status::newGood(); |
335 | } |
336 | |
337 | /** |
338 | * @param Status &$status |
339 | * @return Status |
340 | */ |
341 | public function setupSearchIndex( &$status ) { |
342 | global $IP; |
343 | |
344 | $module = DatabaseSqlite::getFulltextSearchModule(); |
345 | $searchIndexSql = (string)$this->db->newSelectQueryBuilder() |
346 | ->select( 'sql' ) |
347 | ->from( $this->db->addIdentifierQuotes( 'sqlite_master' ) ) |
348 | ->where( [ 'tbl_name' => $this->db->tableName( 'searchindex', 'raw' ) ] ) |
349 | ->caller( __METHOD__ )->fetchField(); |
350 | $fts3tTable = ( stristr( $searchIndexSql, 'fts' ) !== false ); |
351 | |
352 | if ( $fts3tTable && !$module ) { |
353 | $status->warning( 'config-sqlite-fts3-downgrade' ); |
354 | $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" ); |
355 | } elseif ( !$fts3tTable && $module == 'FTS3' ) { |
356 | $this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" ); |
357 | } |
358 | |
359 | return $status; |
360 | } |
361 | |
362 | /** |
363 | * @return string |
364 | */ |
365 | public function getLocalSettings() { |
366 | $dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) ); |
367 | // These tables have frequent writes and are thus split off from the main one. |
368 | // Since the code using these tables only uses transactions for writes, then set |
369 | // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on the first write action. |
370 | return "# SQLite-specific settings |
371 | \$wgSQLiteDataDir = \"{$dir}\"; |
372 | \$wgObjectCaches[CACHE_DB] = [ |
373 | 'class' => SqlBagOStuff::class, |
374 | 'loggroup' => 'SQLBagOStuff', |
375 | 'server' => [ |
376 | 'type' => 'sqlite', |
377 | 'dbname' => 'wikicache', |
378 | 'tablePrefix' => '', |
379 | 'variables' => [ 'synchronous' => 'NORMAL' ], |
380 | 'dbDirectory' => \$wgSQLiteDataDir, |
381 | 'trxMode' => 'IMMEDIATE', |
382 | 'flags' => 0 |
383 | ] |
384 | ]; |
385 | \$wgLocalisationCacheConf['storeServer'] = [ |
386 | 'type' => 'sqlite', |
387 | 'dbname' => \"{\$wgDBname}_l10n_cache\", |
388 | 'tablePrefix' => '', |
389 | 'variables' => [ 'synchronous' => 'NORMAL' ], |
390 | 'dbDirectory' => \$wgSQLiteDataDir, |
391 | 'trxMode' => 'IMMEDIATE', |
392 | 'flags' => 0 |
393 | ]; |
394 | \$wgJobTypeConf['default'] = [ |
395 | 'class' => 'JobQueueDB', |
396 | 'claimTTL' => 3600, |
397 | 'server' => [ |
398 | 'type' => 'sqlite', |
399 | 'dbname' => \"{\$wgDBname}_jobqueue\", |
400 | 'tablePrefix' => '', |
401 | 'variables' => [ 'synchronous' => 'NORMAL' ], |
402 | 'dbDirectory' => \$wgSQLiteDataDir, |
403 | 'trxMode' => 'IMMEDIATE', |
404 | 'flags' => 0 |
405 | ] |
406 | ]; |
407 | \$wgResourceLoaderUseObjectCacheForDeps = true;"; |
408 | } |
409 | } |