installer.php 22 KB

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