installer.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Installation lib
  18. *
  19. * @package Installation
  20. * @author Adrian Lang <mail@adrianlang.de>
  21. * @author Brenda Wallace <shiny@cpan.org>
  22. * @author Brett Taylor <brett@webfroot.co.nz>
  23. * @author Brion Vibber <brion@pobox.com>
  24. * @author CiaranG <ciaran@ciarang.com>
  25. * @author Craig Andrews <candrews@integralblue.com>
  26. * @author Eric Helgeson <helfire@Erics-MBP.local>
  27. * @author Evan Prodromou <evan@status.net>
  28. * @author Mikael Nordfeldth <mmn@hethane.se>
  29. * @author Robin Millette <millette@controlyourself.ca>
  30. * @author Sarven Capadisli <csarven@status.net>
  31. * @author Tom Adams <tom@holizz.com>
  32. * @author Zach Copley <zach@status.net>
  33. * @author Diogo Cordeiro <diogo@fc.up.pt>
  34. * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
  35. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  36. */
  37. abstract class Installer
  38. {
  39. /** Web site info */
  40. public $sitename;
  41. public $server;
  42. public $path;
  43. public $fancy;
  44. public $siteProfile;
  45. public $ssl;
  46. /** DB info */
  47. public $host;
  48. public $database;
  49. public $dbtype;
  50. public $username;
  51. public $password;
  52. public $db;
  53. /** Storage info */
  54. public $avatarDir;
  55. public $fileDir;
  56. /** Administrator info */
  57. public $adminNick;
  58. public $adminPass;
  59. public $adminEmail;
  60. /** Should we skip writing the configuration file? */
  61. public $skipConfig = false;
  62. public static $dbModules = [
  63. 'mysql' => [
  64. 'name' => 'MariaDB 10.3+',
  65. 'check_module' => 'mysqli',
  66. 'scheme' => 'mysqli', // DSN prefix for MDB2
  67. 'charset' => 'utf8mb4',
  68. ],
  69. 'pgsql' => [
  70. 'name' => 'PostgreSQL 11+',
  71. 'check_module' => 'pgsql',
  72. 'scheme' => 'pgsql', // DSN prefix for MDB2
  73. 'charset' => 'UTF8',
  74. ]
  75. ];
  76. /**
  77. * Attempt to include a PHP file and report if it worked, while
  78. * suppressing the annoying warning messages on failure.
  79. * @param string $filename
  80. * @return bool
  81. */
  82. private function haveIncludeFile(string $filename): bool
  83. {
  84. $old = error_reporting(error_reporting() & ~E_WARNING);
  85. $ok = include_once($filename);
  86. error_reporting($old);
  87. return $ok;
  88. }
  89. /**
  90. * Check if all is ready for installation
  91. *
  92. * @return bool
  93. */
  94. public function checkPrereqs(): bool
  95. {
  96. $pass = true;
  97. $config = INSTALLDIR . '/config.php';
  98. if (!$this->skipConfig && file_exists($config)) {
  99. if (!is_writable($config) || filesize($config) > 0) {
  100. if (filesize($config) == 0) {
  101. $this->warning('Config file "config.php" already exists and is empty, but is not writable.');
  102. } else {
  103. $this->warning('Config file "config.php" already exists.');
  104. }
  105. $pass = false;
  106. }
  107. }
  108. if (version_compare(PHP_VERSION, '7.3.0', '<')) {
  109. $this->warning('Require PHP version 7.3.0 or greater.');
  110. $pass = false;
  111. }
  112. $reqs = ['bcmath', 'curl', 'dom', 'gd', 'intl', 'json', 'mbstring', 'openssl', 'simplexml', 'xml', 'xmlwriter'];
  113. foreach ($reqs as $req) {
  114. // Checks if a php extension is both installed and loaded
  115. if (!extension_loaded($req)) {
  116. $this->warning(sprintf('Cannot load required extension: <code>%s</code>', $req));
  117. $pass = false;
  118. }
  119. }
  120. // Make sure we have at least one database module available
  121. $missingExtensions = [];
  122. foreach (self::$dbModules as $type => $info) {
  123. if (!extension_loaded($info['check_module'])) {
  124. $missingExtensions[] = $info['check_module'];
  125. }
  126. }
  127. if (count($missingExtensions) == count(self::$dbModules)) {
  128. $req = implode(', ', $missingExtensions);
  129. $this->warning(sprintf('Cannot find a database extension. You need at least one of %s.', $req));
  130. $pass = false;
  131. }
  132. // @fixme this check seems to be insufficient with Windows ACLs
  133. if (!$this->skipConfig && !is_writable(INSTALLDIR)) {
  134. $this->warning(
  135. sprintf('Cannot write config file to: <code>%s</code></p>', INSTALLDIR),
  136. sprintf('On your server, try this command: <code>chmod a+w %s</code>', INSTALLDIR)
  137. );
  138. $pass = false;
  139. }
  140. // Check the subdirs used for file uploads
  141. // TODO get another flag for this --skipFileSubdirCreation
  142. if (!$this->skipConfig) {
  143. define('GNUSOCIAL', true);
  144. define('STATUSNET', true);
  145. require_once INSTALLDIR . '/lib/util/language.php';
  146. $_server = $this->server;
  147. $_path = $this->path; // We won't be using those so it's safe to do this small hack
  148. require_once INSTALLDIR . '/lib/util/util.php';
  149. require_once INSTALLDIR . '/lib/util/default.php';
  150. $fileSubdirs = [
  151. empty($this->avatarDir) ? $default['avatar']['dir'] : $this->avatarDir,
  152. empty($this->fileDir) ? $default['attachments']['dir'] : $this->fileDir
  153. ];
  154. unset($default);
  155. foreach ($fileSubdirs as $fileFullPath) {
  156. if (!file_exists($fileFullPath)) {
  157. $this->warning(
  158. sprintf('GNU social was unable to create a directory on this path: %s', $fileFullPath),
  159. 'Either create that directory with the right permissions so that GNU social can use it or '.
  160. 'set the necessary permissions and it will be created.'
  161. );
  162. $pass = $pass && mkdir($fileFullPath);
  163. } elseif (!is_dir($fileFullPath)) {
  164. $this->warning(
  165. sprintf('GNU social expected a directory but found something else on this path: %s', $fileFullPath),
  166. 'Either make sure it goes to a directory or remove it and a directory will be created.'
  167. );
  168. $pass = false;
  169. } elseif (!is_writable($fileFullPath)) {
  170. $this->warning(
  171. sprintf('Cannot write to directory: <code>%s</code>', $fileFullPath),
  172. sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath)
  173. );
  174. $pass = false;
  175. }
  176. }
  177. }
  178. return $pass;
  179. }
  180. /**
  181. * Basic validation on the database parameters
  182. * Side effects: error output if not valid
  183. *
  184. * @return bool success
  185. */
  186. public function validateDb(): bool
  187. {
  188. $fail = false;
  189. if (empty($this->host)) {
  190. $this->updateStatus("No hostname specified.", true);
  191. $fail = true;
  192. }
  193. if (empty($this->database)) {
  194. $this->updateStatus("No database specified.", true);
  195. $fail = true;
  196. }
  197. if (empty($this->username)) {
  198. $this->updateStatus("No username specified.", true);
  199. $fail = true;
  200. }
  201. if (empty($this->sitename)) {
  202. $this->updateStatus("No sitename specified.", true);
  203. $fail = true;
  204. }
  205. return !$fail;
  206. }
  207. /**
  208. * Basic validation on the administrator user parameters
  209. * Side effects: error output if not valid
  210. *
  211. * @return bool success
  212. */
  213. public function validateAdmin(): bool
  214. {
  215. $fail = false;
  216. if (empty($this->adminNick)) {
  217. $this->updateStatus("No initial user nickname specified.", true);
  218. $fail = true;
  219. }
  220. if ($this->adminNick && !preg_match('/^[0-9a-z]{1,64}$/', $this->adminNick)) {
  221. $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
  222. '" is invalid; should be plain letters and numbers no longer than 64 characters.', true);
  223. $fail = true;
  224. }
  225. // @fixme hardcoded list; should use Nickname::isValid()
  226. // if/when it's safe to have loaded the infrastructure here
  227. $blacklist = ['main', 'panel', 'twitter', 'settings', 'rsd.xml', 'favorited', 'featured', 'favoritedrss', 'featuredrss', 'rss', 'getfile', 'api', 'groups', 'group', 'peopletag', 'tag', 'user', 'message', 'conversation', 'notice', 'attachment', 'search', 'index.php', 'doc', 'opensearch', 'robots.txt', 'xd_receiver.html', 'facebook', 'activity'];
  228. if (in_array($this->adminNick, $blacklist)) {
  229. $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
  230. '" is reserved.', true);
  231. $fail = true;
  232. }
  233. if (empty($this->adminPass)) {
  234. $this->updateStatus("No initial user password specified.", true);
  235. $fail = true;
  236. }
  237. return !$fail;
  238. }
  239. /**
  240. * Make sure a site profile was selected
  241. *
  242. * @return bool success
  243. */
  244. public function validateSiteProfile(): bool
  245. {
  246. if (empty($this->siteProfile)) {
  247. $this->updateStatus("No site profile selected.", true);
  248. return false;
  249. }
  250. return true;
  251. }
  252. /**
  253. * Set up the database with the appropriate function for the selected type...
  254. * Saves database info into $this->db.
  255. *
  256. * @fixme escape things in the connection string in case we have a funny pass etc
  257. * @return mixed array of database connection params on success, false on failure
  258. * @throws Exception
  259. */
  260. public function setupDatabase()
  261. {
  262. if (!empty($this->db)) {
  263. throw new Exception('Bad order of operations: DB already set up.');
  264. }
  265. $this->updateStatus('Starting installation...');
  266. $auth = '';
  267. if (!empty($this->password)) {
  268. $auth .= ":{$this->password}";
  269. }
  270. $scheme = self::$dbModules[$this->dbtype]['scheme'];
  271. $dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}";
  272. $this->updateStatus('Checking database...');
  273. $charset = self::$dbModules[$this->dbtype]['charset'];
  274. $conn = $this->connectDatabase($dsn, $charset);
  275. $server_charset = $this->getDatabaseCharset($conn, $this->dbtype);
  276. // Ensure the database server character set is UTF-8.
  277. if ($server_charset !== $charset) {
  278. $this->updateStatus(
  279. 'GNU social requires the "' . $charset . '" character set. '
  280. . 'Yours is ' . htmlentities($server_charset)
  281. );
  282. return false;
  283. }
  284. // Ensure the timezone is UTC.
  285. if ($this->dbtype !== 'mysql') {
  286. $conn->exec("SET TIME ZONE INTERVAL '+00:00' HOUR TO MINUTE");
  287. } else {
  288. $conn->exec("SET time_zone = '+0:00'");
  289. }
  290. $res = $this->updateStatus('Creating database tables...');
  291. if (!$this->createCoreTables($conn)) {
  292. $this->updateStatus('Error creating tables.', true);
  293. return false;
  294. }
  295. foreach ([
  296. 'sms_carrier' => 'SMS carrier',
  297. 'notice_source' => 'notice source',
  298. 'foreign_services' => 'foreign service',
  299. ] as $scr => $name) {
  300. $this->updateStatus(sprintf("Adding %s data to database...", $name));
  301. $res = $this->runDbScript($scr . '.sql', $conn);
  302. if ($res === false) {
  303. $this->updateStatus(sprintf("Can't run %s script.", $name), true);
  304. return false;
  305. }
  306. }
  307. $db = ['type' => $this->dbtype, 'database' => $dsn];
  308. return $db;
  309. }
  310. /**
  311. * Open a connection to the database.
  312. *
  313. * @param string $dsn
  314. * @param string $charset
  315. * @return MDB2_Driver_Common
  316. * @throws Exception
  317. */
  318. protected function connectDatabase(string $dsn, string $charset)
  319. {
  320. $dsn = MDB2::parseDSN($dsn);
  321. // Ensure the database client character set is UTF-8.
  322. $dsn['charset'] = $charset;
  323. $conn = MDB2::connect($dsn);
  324. if (MDB2::isError($conn)) {
  325. throw new Exception(
  326. 'Cannot connect to database: ' . $conn->getMessage()
  327. );
  328. }
  329. return $conn;
  330. }
  331. /**
  332. * Get the database server character set.
  333. *
  334. * @param MDB2_Driver_Common $conn
  335. * @param string $dbtype
  336. * @return string
  337. * @throws Exception
  338. */
  339. protected function getDatabaseCharset($conn, string $dbtype): string
  340. {
  341. $database = $conn->getDatabase();
  342. switch ($dbtype) {
  343. case 'pgsql':
  344. $res = $conn->query('SHOW server_encoding');
  345. break;
  346. case 'mysql':
  347. $stmt = $conn->prepare(
  348. <<<END
  349. SELECT DEFAULT_CHARACTER_SET_NAME
  350. FROM INFORMATION_SCHEMA.SCHEMATA
  351. WHERE SCHEMA_NAME = ?
  352. END,
  353. ['text'],
  354. MDB2_PREPARE_RESULT
  355. );
  356. if (MDB2::isError($stmt)) {
  357. return null;
  358. }
  359. $res = $stmt->execute([$database]);
  360. break;
  361. default:
  362. throw new Exception('Unknown DB type selected.');
  363. }
  364. if (MDB2::isError($res)) {
  365. throw new Exception($res->getMessage());
  366. }
  367. $ret = $res->fetchOne();
  368. if (MDB2::isError($ret)) {
  369. throw new Exception($ret->getMessage());
  370. }
  371. return $ret;
  372. }
  373. /**
  374. * Create core tables on the given database connection.
  375. *
  376. * @param MDB2_Driver_Common $conn
  377. * @return bool
  378. */
  379. public function createCoreTables($conn): bool
  380. {
  381. $schema = Schema::get($conn, $this->dbtype);
  382. $tableDefs = $this->getCoreSchema();
  383. foreach ($tableDefs as $name => $def) {
  384. if (defined('DEBUG_INSTALLER')) {
  385. echo " $name ";
  386. }
  387. $schema->ensureTable($name, $def);
  388. }
  389. return true;
  390. }
  391. /**
  392. * Fetch the core table schema definitions.
  393. *
  394. * @return array of table names => table def arrays
  395. */
  396. public function getCoreSchema(): array
  397. {
  398. $schema = [];
  399. include INSTALLDIR . '/db/core.php';
  400. return $schema;
  401. }
  402. /**
  403. * Return a parseable PHP literal for the given value.
  404. * This will include quotes for strings, etc.
  405. *
  406. * @param mixed $val
  407. * @return string
  408. */
  409. public function phpVal($val): string
  410. {
  411. return var_export($val, true);
  412. }
  413. /**
  414. * Return an array of parseable PHP literal for the given values.
  415. * These will include quotes for strings, etc.
  416. *
  417. * @param mixed $map
  418. * @return array
  419. */
  420. public function phpVals($map): array
  421. {
  422. return array_map([$this, 'phpVal'], $map);
  423. }
  424. /**
  425. * Write a stock configuration file.
  426. *
  427. * @return bool success
  428. *
  429. * @fixme escape variables in output in case we have funny chars, apostrophes etc
  430. */
  431. public function writeConf(): bool
  432. {
  433. $vals = $this->phpVals([
  434. 'sitename' => $this->sitename,
  435. 'server' => $this->server,
  436. 'path' => $this->path,
  437. 'ssl' => in_array($this->ssl, ['never', 'always'])
  438. ? $this->ssl
  439. : 'never',
  440. 'db_database' => $this->db['database'],
  441. 'db_type' => $this->db['type']
  442. ]);
  443. // assemble configuration file in a string
  444. $cfg = "<?php\n" .
  445. "if (!defined('GNUSOCIAL')) { exit(1); }\n\n" .
  446. // site name
  447. "\$config['site']['name'] = {$vals['sitename']};\n\n" .
  448. // site location
  449. "\$config['site']['server'] = {$vals['server']};\n" .
  450. "\$config['site']['path'] = {$vals['path']}; \n\n" .
  451. "\$config['site']['ssl'] = {$vals['ssl']}; \n\n" .
  452. ($this->ssl === 'proxy' ? "\$config['site']['sslproxy'] = true;\n\n" : '') .
  453. // checks if fancy URLs are enabled
  454. ($this->fancy ? "\$config['site']['fancy'] = true;\n\n" : '') .
  455. // database
  456. "\$config['db']['database'] = {$vals['db_database']};\n\n" .
  457. "\$config['db']['type'] = {$vals['db_type']};\n\n" .
  458. "// Uncomment below for better performance. Just remember you must run\n" .
  459. "// php scripts/checkschema.php whenever your enabled plugins change!\n" .
  460. "//\$config['db']['schemacheck'] = 'script';\n\n";
  461. // Normalize line endings for Windows servers
  462. $cfg = str_replace("\n", PHP_EOL, $cfg);
  463. // write configuration file out to install directory
  464. $res = file_put_contents(INSTALLDIR . DIRECTORY_SEPARATOR . 'config.php', $cfg);
  465. return $res;
  466. }
  467. /**
  468. * Write the site profile. We do this after creating the initial user
  469. * in case the site profile is set to single user. This gets around the
  470. * 'chicken-and-egg' problem of the system requiring a valid user for
  471. * single user mode, before the intial user is actually created. Yeah,
  472. * we should probably do this in smarter way.
  473. *
  474. * @return int res number of bytes written
  475. */
  476. public function writeSiteProfile(): int
  477. {
  478. $vals = $this->phpVals([
  479. 'site_profile' => $this->siteProfile,
  480. 'nickname' => $this->adminNick
  481. ]);
  482. $cfg =
  483. // site profile
  484. "\$config['site']['profile'] = {$vals['site_profile']};\n";
  485. if ($this->siteProfile == "singleuser") {
  486. $cfg .= "\$config['singleuser']['nickname'] = {$vals['nickname']};\n\n";
  487. } else {
  488. $cfg .= "\n";
  489. }
  490. // Normalize line endings for Windows servers
  491. $cfg = str_replace("\n", PHP_EOL, $cfg);
  492. // write configuration file out to install directory
  493. $res = file_put_contents(INSTALLDIR . '/config.php', $cfg, FILE_APPEND);
  494. return $res;
  495. }
  496. /**
  497. * Install schema into the database
  498. *
  499. * @param string $filename location of database schema file
  500. * @param MDB2_Driver_Common $conn connection to database
  501. *
  502. * @return bool - indicating success or failure
  503. */
  504. public function runDbScript(string $filename, $conn): bool
  505. {
  506. $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
  507. $stmts = explode(';', $sql);
  508. foreach ($stmts as $stmt) {
  509. $stmt = trim($stmt);
  510. if (!mb_strlen($stmt)) {
  511. continue;
  512. }
  513. try {
  514. $res = $conn->query($stmt);
  515. } catch (Exception $e) {
  516. $error = $e->getMessage();
  517. $this->updateStatus("ERROR ($error) for SQL '$stmt'");
  518. return false;
  519. }
  520. }
  521. return true;
  522. }
  523. /**
  524. * Create the initial admin user account.
  525. * Side effect: may load portions of GNU social framework.
  526. * Side effect: outputs program info
  527. */
  528. public function registerInitialUser(): bool
  529. {
  530. // initalize hostname from install arguments, so it can be used to find
  531. // the /etc config file from the commandline installer
  532. $server = $this->server;
  533. require_once INSTALLDIR . '/lib/util/common.php';
  534. $data = ['nickname' => $this->adminNick,
  535. 'password' => $this->adminPass,
  536. 'fullname' => $this->adminNick];
  537. if ($this->adminEmail) {
  538. $data['email'] = $this->adminEmail;
  539. }
  540. try {
  541. $user = User::register($data, true); // true to skip email sending verification
  542. } catch (Exception $e) {
  543. return false;
  544. }
  545. // give initial user carte blanche
  546. $user->grantRole('owner');
  547. $user->grantRole('moderator');
  548. $user->grantRole('administrator');
  549. return true;
  550. }
  551. /**
  552. * The beef of the installer!
  553. * Create database, config file, and admin user.
  554. *
  555. * Prerequisites: validation of input data.
  556. *
  557. * @return bool success
  558. */
  559. public function doInstall(): bool
  560. {
  561. global $config;
  562. $this->updateStatus("Initializing...");
  563. ini_set('display_errors', 1);
  564. error_reporting(E_ALL & ~E_STRICT & ~E_NOTICE);
  565. if (!defined('GNUSOCIAL')) {
  566. define('GNUSOCIAL', true);
  567. }
  568. if (!defined('STATUSNET')) {
  569. define('STATUSNET', true);
  570. }
  571. require_once INSTALLDIR . '/lib/util/framework.php';
  572. GNUsocial::initDefaults($this->server, $this->path);
  573. if ($this->siteProfile == "singleuser") {
  574. // Until we use ['site']['profile']==='singleuser' everywhere
  575. $config['singleuser']['enabled'] = true;
  576. }
  577. try {
  578. $this->db = $this->setupDatabase();
  579. if (!$this->db) {
  580. // database connection failed, do not move on to create config file.
  581. return false;
  582. }
  583. } catch (Exception $e) {
  584. // Lower-level DB error!
  585. $this->updateStatus("Database error: " . $e->getMessage(), true);
  586. return false;
  587. }
  588. if (!$this->skipConfig) {
  589. // Make sure we can write to the file twice
  590. $oldUmask = umask(000);
  591. $this->updateStatus("Writing config file...");
  592. $res = $this->writeConf();
  593. if (!$res) {
  594. $this->updateStatus("Can't write config file.", true);
  595. return false;
  596. }
  597. }
  598. if (!empty($this->adminNick)) {
  599. // Okay, cross fingers and try to register an initial user
  600. if ($this->registerInitialUser()) {
  601. $this->updateStatus(
  602. "An initial user with the administrator role has been created."
  603. );
  604. } else {
  605. $this->updateStatus(
  606. "Could not create initial user account.",
  607. true
  608. );
  609. return false;
  610. }
  611. }
  612. if (!$this->skipConfig) {
  613. $this->updateStatus("Setting site profile...");
  614. $res = $this->writeSiteProfile();
  615. if (!$res) {
  616. $this->updateStatus("Can't write to config file.", true);
  617. return false;
  618. }
  619. // Restore original umask
  620. umask($oldUmask);
  621. // Set permissions back to something decent
  622. chmod(INSTALLDIR . '/config.php', 0644);
  623. }
  624. $scheme = $this->ssl === 'always' ? 'https' : 'http';
  625. $link = "{$scheme}://{$this->server}/{$this->path}";
  626. $this->updateStatus("GNU social has been installed at $link");
  627. $this->updateStatus(
  628. '<strong>DONE!</strong> You can visit your <a href="' . htmlspecialchars($link) . '">new GNU social site</a> (log in as "' . htmlspecialchars($this->adminNick) . '"). If this is your first GNU social install, make your experience the best possible by visiting our resource site to join the <a href="https://gnu.io/social/resources/">mailing list or IRC</a>. <a href="' . htmlspecialchars($link) . '/doc/faq">FAQ is found here</a>.'
  629. );
  630. return true;
  631. }
  632. /**
  633. * Output a pre-install-time warning message
  634. * @param string $message HTML ok, but should be plaintext-able
  635. * @param string $submessage HTML ok, but should be plaintext-able
  636. */
  637. abstract public function warning(string $message, string $submessage = '');
  638. /**
  639. * Output an install-time progress message
  640. * @param string $status HTML ok, but should be plaintext-able
  641. * @param bool $error true if this should be marked as an error condition
  642. */
  643. abstract public function updateStatus(string $status, bool $error = false);
  644. }