SqliteInstaller.php 11 KB

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