Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.54% covered (danger)
11.54%
6 / 52
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqliteCreateDatabaseTask
11.54% covered (danger)
11.54%
6 / 52
16.67% covered (danger)
16.67%
1 / 6
193.22
0.00% covered (danger)
0.00%
0 / 1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAliases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 makeStubDBFile
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getSqliteUtils
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createDataDir
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Installer\Task;
4
5use MediaWiki\MainConfigNames;
6use MediaWiki\Status\Status;
7use Wikimedia\Rdbms\DatabaseFactory;
8use Wikimedia\Rdbms\DatabaseSqlite;
9use Wikimedia\Rdbms\DBConnectionError;
10
11/**
12 * Create the SQLite database files
13 *
14 * @internal For use by the installer
15 */
16class SqliteCreateDatabaseTask extends Task {
17    public function getName() {
18        return 'database';
19    }
20
21    public function getAliases() {
22        return 'schema';
23    }
24
25    public function execute(): Status {
26        $dir = $this->getConfigVar( MainConfigNames::SQLiteDataDir );
27
28        # Double check (Only available in web installation). We checked this before but maybe someone
29        # deleted the data dir between then and now
30        $dir_status = $this->getSqliteUtils()->checkDataDir( $dir );
31        if ( $dir_status->isGood() ) {
32            $res = $this->createDataDir( $dir );
33            if ( !$res->isGood() ) {
34                return $res;
35            }
36        } else {
37            return $dir_status;
38        }
39
40        $db = $this->getConfigVar( MainConfigNames::DBname );
41
42        # Make the main and cache stub DB files
43        $status = Status::newGood();
44        $status->merge( $this->makeStubDBFile( $dir, $db ) );
45        $status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
46        $status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
47        $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
48        if ( !$status->isOK() ) {
49            return $status;
50        }
51
52        # Create the l10n cache DB
53        try {
54            $conn = ( new DatabaseFactory() )->create(
55                'sqlite', [ 'dbname' => "{$db}_l10n_cache", 'dbDirectory' => $dir ] );
56            # @todo: don't duplicate l10n_cache definition, though it's very simple
57            $sql =
58                <<<EOT
59    CREATE TABLE l10n_cache (
60        lc_lang BLOB NOT NULL,
61        lc_key TEXT NOT NULL,
62        lc_value BLOB NOT NULL,
63        PRIMARY KEY (lc_lang, lc_key)
64    );
65EOT;
66            $conn->query( $sql, __METHOD__ );
67            $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
68            $conn->close( __METHOD__ );
69        } catch ( DBConnectionError $e ) {
70            return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
71        }
72
73        # Create the job queue DB
74        try {
75            $conn = ( new DatabaseFactory() )->create(
76                'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
77            # @todo: don't duplicate job definition, though it's very static
78            $sql =
79                <<<EOT
80    CREATE TABLE job (
81        job_id INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT,
82        job_cmd BLOB NOT NULL default '',
83        job_namespace INTEGER NOT NULL,
84        job_title TEXT  NOT NULL,
85        job_timestamp BLOB NULL default NULL,
86        job_params BLOB NOT NULL,
87        job_random integer  NOT NULL default 0,
88        job_attempts integer  NOT NULL default 0,
89        job_token BLOB NOT NULL default '',
90        job_token_timestamp BLOB NULL default NULL,
91        job_sha1 BLOB NOT NULL default ''
92    );
93    CREATE INDEX job_sha1 ON job (job_sha1);
94    CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
95    CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
96    CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
97    CREATE INDEX job_timestamp ON job (job_timestamp);
98EOT;
99            $conn->query( $sql, __METHOD__ );
100            $conn->query( "PRAGMA journal_mode=WAL", __METHOD__ ); // this is permanent
101            $conn->close( __METHOD__ );
102        } catch ( DBConnectionError $e ) {
103            return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
104        }
105
106        # Open the main DB
107        $mainConnStatus = $this->getConnection( ITaskContext::CONN_CREATE_TABLES );
108        // Use WAL mode. This has better performance
109        // when the DB is being read and written concurrently.
110        // This causes the DB to be created in this mode
111        // so we only have to do this on creation.
112        $mainConnStatus->getDB()->query( "PRAGMA journal_mode=WAL", __METHOD__ );
113        return $mainConnStatus;
114    }
115
116    /**
117     * @param string $dir
118     * @param string $db
119     * @return Status
120     */
121    protected function makeStubDBFile( $dir, $db ) {
122        $file = DatabaseSqlite::generateFileName( $dir, $db );
123
124        if ( file_exists( $file ) ) {
125            if ( !is_writable( $file ) ) {
126                return Status::newFatal( 'config-sqlite-readonly', $file );
127            }
128            return Status::newGood();
129        }
130
131        $oldMask = umask( 0177 );
132        if ( file_put_contents( $file, '' ) === false ) {
133            umask( $oldMask );
134            return Status::newFatal( 'config-sqlite-cant-create-db', $file );
135        }
136        umask( $oldMask );
137
138        return Status::newGood();
139    }
140
141    private function getSqliteUtils() {
142        return new SqliteUtils;
143    }
144
145    /**
146     * @param string $dir Path to the data directory
147     * @return Status Return good Status if without error
148     */
149    private function createDataDir( $dir ): Status {
150        if ( !is_dir( $dir ) ) {
151            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
152            $ok = @mkdir( $dir, 0700, true );
153            if ( !$ok ) {
154                return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
155            }
156        }
157        # Put a .htaccess file in case the user didn't take our advice
158        file_put_contents( "$dir/.htaccess", "Require all denied\n" );
159        return Status::newGood();
160    }
161
162}