installer.php 23 KB

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