123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729 |
- <?php
- // This file is part of GNU social - https://www.gnu.org/software/social
- //
- // GNU social is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // GNU social is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
- /**
- * Installation lib
- *
- * @package Installation
- * @author Adrian Lang <mail@adrianlang.de>
- * @author Brenda Wallace <shiny@cpan.org>
- * @author Brett Taylor <brett@webfroot.co.nz>
- * @author Brion Vibber <brion@pobox.com>
- * @author CiaranG <ciaran@ciarang.com>
- * @author Craig Andrews <candrews@integralblue.com>
- * @author Eric Helgeson <helfire@Erics-MBP.local>
- * @author Evan Prodromou <evan@status.net>
- * @author Mikael Nordfeldth <mmn@hethane.se>
- * @author Robin Millette <millette@controlyourself.ca>
- * @author Sarven Capadisli <csarven@status.net>
- * @author Tom Adams <tom@holizz.com>
- * @author Zach Copley <zach@status.net>
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
- * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
- */
- abstract class Installer
- {
- /** Web site info */
- public $sitename;
- public $server;
- public $path;
- public $fancy;
- public $siteProfile;
- public $ssl;
- /** DB info */
- public $host;
- public $database;
- public $dbtype;
- public $username;
- public $password;
- public $db;
- /** Storage info */
- public $avatarDir;
- public $fileDir;
- /** Administrator info */
- public $adminNick;
- public $adminPass;
- public $adminEmail;
- /** Should we skip writing the configuration file? */
- public $skipConfig = false;
- public static $dbModules = [
- 'mysql' => [
- 'name' => 'MariaDB 10.3+',
- 'check_module' => 'mysqli',
- 'scheme' => 'mysqli', // DSN prefix for MDB2
- 'charset' => 'utf8mb4',
- ],
- 'pgsql' => [
- 'name' => 'PostgreSQL 11+',
- 'check_module' => 'pgsql',
- 'scheme' => 'pgsql', // DSN prefix for MDB2
- 'charset' => 'UTF8',
- ]
- ];
- /**
- * Attempt to include a PHP file and report if it worked, while
- * suppressing the annoying warning messages on failure.
- * @param string $filename
- * @return bool
- */
- private function haveIncludeFile(string $filename): bool
- {
- $old = error_reporting(error_reporting() & ~E_WARNING);
- $ok = include_once($filename);
- error_reporting($old);
- return $ok;
- }
- /**
- * Check if all is ready for installation
- *
- * @return bool
- */
- public function checkPrereqs(): bool
- {
- $pass = true;
- $config = INSTALLDIR . '/config.php';
- if (!$this->skipConfig && file_exists($config)) {
- if (!is_writable($config) || filesize($config) > 0) {
- if (filesize($config) == 0) {
- $this->warning('Config file "config.php" already exists and is empty, but is not writable.');
- } else {
- $this->warning('Config file "config.php" already exists.');
- }
- $pass = false;
- }
- }
- if (version_compare(PHP_VERSION, '7.3.0', '<')) {
- $this->warning('Require PHP version 7.3.0 or greater.');
- $pass = false;
- }
- $reqs = ['bcmath', 'curl', 'dom', 'gd', 'intl', 'json', 'mbstring', 'openssl', 'simplexml', 'xml', 'xmlwriter'];
- foreach ($reqs as $req) {
- // Checks if a php extension is both installed and loaded
- if (!extension_loaded($req)) {
- $this->warning(sprintf('Cannot load required extension: <code>%s</code>', $req));
- $pass = false;
- }
- }
- // Make sure we have at least one database module available
- $missingExtensions = [];
- foreach (self::$dbModules as $type => $info) {
- if (!extension_loaded($info['check_module'])) {
- $missingExtensions[] = $info['check_module'];
- }
- }
- if (count($missingExtensions) == count(self::$dbModules)) {
- $req = implode(', ', $missingExtensions);
- $this->warning(sprintf('Cannot find a database extension. You need at least one of %s.', $req));
- $pass = false;
- }
- // @fixme this check seems to be insufficient with Windows ACLs
- if (!$this->skipConfig && !is_writable(INSTALLDIR)) {
- $this->warning(
- sprintf('Cannot write config file to: <code>%s</code></p>', INSTALLDIR),
- sprintf('On your server, try this command: <code>chmod a+w %s</code>', INSTALLDIR)
- );
- $pass = false;
- }
- // Check the subdirs used for file uploads
- // TODO get another flag for this --skipFileSubdirCreation
- if (!$this->skipConfig) {
- define('GNUSOCIAL', true);
- define('STATUSNET', true);
- require_once INSTALLDIR . '/lib/util/language.php';
- $_server = $this->server;
- $_path = $this->path; // We won't be using those so it's safe to do this small hack
- require_once INSTALLDIR . '/lib/util/util.php';
- require_once INSTALLDIR . '/lib/util/default.php';
- $fileSubdirs = [
- empty($this->avatarDir) ? $default['avatar']['dir'] : $this->avatarDir,
- empty($this->fileDir) ? $default['attachments']['dir'] : $this->fileDir
- ];
- unset($default);
- foreach ($fileSubdirs as $fileFullPath) {
- if (!file_exists($fileFullPath)) {
- $this->warning(
- sprintf('GNU social was unable to create a directory on this path: %s', $fileFullPath),
- 'Either create that directory with the right permissions so that GNU social can use it or '.
- 'set the necessary permissions and it will be created.'
- );
- $pass = $pass && mkdir($fileFullPath);
- } elseif (!is_dir($fileFullPath)) {
- $this->warning(
- sprintf('GNU social expected a directory but found something else on this path: %s', $fileFullPath),
- 'Either make sure it goes to a directory or remove it and a directory will be created.'
- );
- $pass = false;
- } elseif (!is_writable($fileFullPath)) {
- $this->warning(
- sprintf('Cannot write to directory: <code>%s</code>', $fileFullPath),
- sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath)
- );
- $pass = false;
- }
- }
- }
- return $pass;
- }
- /**
- * Basic validation on the database parameters
- * Side effects: error output if not valid
- *
- * @return bool success
- */
- public function validateDb(): bool
- {
- $fail = false;
- if (empty($this->host)) {
- $this->updateStatus("No hostname specified.", true);
- $fail = true;
- }
- if (empty($this->database)) {
- $this->updateStatus("No database specified.", true);
- $fail = true;
- }
- if (empty($this->username)) {
- $this->updateStatus("No username specified.", true);
- $fail = true;
- }
- if (empty($this->sitename)) {
- $this->updateStatus("No sitename specified.", true);
- $fail = true;
- }
- return !$fail;
- }
- /**
- * Basic validation on the administrator user parameters
- * Side effects: error output if not valid
- *
- * @return bool success
- */
- public function validateAdmin(): bool
- {
- $fail = false;
- if (empty($this->adminNick)) {
- $this->updateStatus("No initial user nickname specified.", true);
- $fail = true;
- }
- if ($this->adminNick && !preg_match('/^[0-9a-z]{1,64}$/', $this->adminNick)) {
- $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
- '" is invalid; should be plain letters and numbers no longer than 64 characters.', true);
- $fail = true;
- }
- // @fixme hardcoded list; should use Nickname::isValid()
- // if/when it's safe to have loaded the infrastructure here
- $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'];
- if (in_array($this->adminNick, $blacklist)) {
- $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
- '" is reserved.', true);
- $fail = true;
- }
- if (empty($this->adminPass)) {
- $this->updateStatus("No initial user password specified.", true);
- $fail = true;
- }
- return !$fail;
- }
- /**
- * Make sure a site profile was selected
- *
- * @return bool success
- */
- public function validateSiteProfile(): bool
- {
- if (empty($this->siteProfile)) {
- $this->updateStatus("No site profile selected.", true);
- return false;
- }
- return true;
- }
- /**
- * Set up the database with the appropriate function for the selected type...
- * Saves database info into $this->db.
- *
- * @fixme escape things in the connection string in case we have a funny pass etc
- * @return mixed array of database connection params on success, false on failure
- * @throws Exception
- */
- public function setupDatabase()
- {
- if (!empty($this->db)) {
- throw new Exception('Bad order of operations: DB already set up.');
- }
- $this->updateStatus('Starting installation...');
- $auth = '';
- if (!empty($this->password)) {
- $auth .= ":{$this->password}";
- }
- $scheme = self::$dbModules[$this->dbtype]['scheme'];
- $dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}";
- $this->updateStatus('Checking database...');
- $charset = self::$dbModules[$this->dbtype]['charset'];
- $conn = $this->connectDatabase($dsn, $charset);
- $server_charset = $this->getDatabaseCharset($conn, $this->dbtype);
- // Ensure the database server character set is UTF-8.
- if ($server_charset !== $charset) {
- $this->updateStatus(
- 'GNU social requires the "' . $charset . '" character set. '
- . 'Yours is ' . htmlentities($server_charset)
- );
- return false;
- }
- // Ensure the timezone is UTC.
- if ($this->dbtype !== 'mysql') {
- $conn->exec("SET TIME ZONE INTERVAL '+00:00' HOUR TO MINUTE");
- } else {
- $conn->exec("SET time_zone = '+0:00'");
- }
- $res = $this->updateStatus('Creating database tables...');
- if (!$this->createCoreTables($conn)) {
- $this->updateStatus('Error creating tables.', true);
- return false;
- }
- foreach ([
- 'sms_carrier' => 'SMS carrier',
- 'notice_source' => 'notice source',
- 'foreign_services' => 'foreign service',
- ] as $scr => $name) {
- $this->updateStatus(sprintf("Adding %s data to database...", $name));
- $res = $this->runDbScript($scr . '.sql', $conn);
- if ($res === false) {
- $this->updateStatus(sprintf("Can't run %s script.", $name), true);
- return false;
- }
- }
- $db = ['type' => $this->dbtype, 'database' => $dsn];
- return $db;
- }
- /**
- * Open a connection to the database.
- *
- * @param string $dsn
- * @param string $charset
- * @return MDB2_Driver_Common
- * @throws Exception
- */
- protected function connectDatabase(string $dsn, string $charset)
- {
- $dsn = MDB2::parseDSN($dsn);
- // Ensure the database client character set is UTF-8.
- $dsn['charset'] = $charset;
- $conn = MDB2::connect($dsn);
- if (MDB2::isError($conn)) {
- throw new Exception(
- 'Cannot connect to database: ' . $conn->getMessage()
- );
- }
- return $conn;
- }
- /**
- * Get the database server character set.
- *
- * @param MDB2_Driver_Common $conn
- * @param string $dbtype
- * @return string
- * @throws Exception
- */
- protected function getDatabaseCharset($conn, string $dbtype): string
- {
- $database = $conn->getDatabase();
- switch ($dbtype) {
- case 'pgsql':
- $res = $conn->query('SHOW server_encoding');
- break;
- case 'mysql':
- $stmt = $conn->prepare(
- <<<END
- SELECT DEFAULT_CHARACTER_SET_NAME
- FROM INFORMATION_SCHEMA.SCHEMATA
- WHERE SCHEMA_NAME = ?
- END,
- ['text'],
- MDB2_PREPARE_RESULT
- );
- if (MDB2::isError($stmt)) {
- return null;
- }
- $res = $stmt->execute([$database]);
- break;
- default:
- throw new Exception('Unknown DB type selected.');
- }
- if (MDB2::isError($res)) {
- throw new Exception($res->getMessage());
- }
- $ret = $res->fetchOne();
- if (MDB2::isError($ret)) {
- throw new Exception($ret->getMessage());
- }
- return $ret;
- }
- /**
- * Create core tables on the given database connection.
- *
- * @param MDB2_Driver_Common $conn
- * @return bool
- */
- public function createCoreTables($conn): bool
- {
- $schema = Schema::get($conn, $this->dbtype);
- $tableDefs = $this->getCoreSchema();
- foreach ($tableDefs as $name => $def) {
- if (defined('DEBUG_INSTALLER')) {
- echo " $name ";
- }
- $schema->ensureTable($name, $def);
- }
- return true;
- }
- /**
- * Fetch the core table schema definitions.
- *
- * @return array of table names => table def arrays
- */
- public function getCoreSchema(): array
- {
- $schema = [];
- include INSTALLDIR . '/db/core.php';
- return $schema;
- }
- /**
- * Return a parseable PHP literal for the given value.
- * This will include quotes for strings, etc.
- *
- * @param mixed $val
- * @return string
- */
- public function phpVal($val): string
- {
- return var_export($val, true);
- }
- /**
- * Return an array of parseable PHP literal for the given values.
- * These will include quotes for strings, etc.
- *
- * @param mixed $map
- * @return array
- */
- public function phpVals($map): array
- {
- return array_map([$this, 'phpVal'], $map);
- }
- /**
- * Write a stock configuration file.
- *
- * @return bool success
- *
- * @fixme escape variables in output in case we have funny chars, apostrophes etc
- */
- public function writeConf(): bool
- {
- $vals = $this->phpVals([
- 'sitename' => $this->sitename,
- 'server' => $this->server,
- 'path' => $this->path,
- 'ssl' => in_array($this->ssl, ['never', 'always'])
- ? $this->ssl
- : 'never',
- 'db_database' => $this->db['database'],
- 'db_type' => $this->db['type']
- ]);
- // assemble configuration file in a string
- $cfg = "<?php\n" .
- "if (!defined('GNUSOCIAL')) { exit(1); }\n\n" .
- // site name
- "\$config['site']['name'] = {$vals['sitename']};\n\n" .
- // site location
- "\$config['site']['server'] = {$vals['server']};\n" .
- "\$config['site']['path'] = {$vals['path']}; \n\n" .
- "\$config['site']['ssl'] = {$vals['ssl']}; \n\n" .
- ($this->ssl === 'proxy' ? "\$config['site']['sslproxy'] = true;\n\n" : '') .
- // checks if fancy URLs are enabled
- ($this->fancy ? "\$config['site']['fancy'] = true;\n\n" : '') .
- // database
- "\$config['db']['database'] = {$vals['db_database']};\n\n" .
- "\$config['db']['type'] = {$vals['db_type']};\n\n" .
- "// Uncomment below for better performance. Just remember you must run\n" .
- "// php scripts/checkschema.php whenever your enabled plugins change!\n" .
- "//\$config['db']['schemacheck'] = 'script';\n\n";
- // Normalize line endings for Windows servers
- $cfg = str_replace("\n", PHP_EOL, $cfg);
- // write configuration file out to install directory
- $res = file_put_contents(INSTALLDIR . DIRECTORY_SEPARATOR . 'config.php', $cfg);
- return $res;
- }
- /**
- * Write the site profile. We do this after creating the initial user
- * in case the site profile is set to single user. This gets around the
- * 'chicken-and-egg' problem of the system requiring a valid user for
- * single user mode, before the intial user is actually created. Yeah,
- * we should probably do this in smarter way.
- *
- * @return int res number of bytes written
- */
- public function writeSiteProfile(): int
- {
- $vals = $this->phpVals([
- 'site_profile' => $this->siteProfile,
- 'nickname' => $this->adminNick
- ]);
- $cfg =
- // site profile
- "\$config['site']['profile'] = {$vals['site_profile']};\n";
- if ($this->siteProfile == "singleuser") {
- $cfg .= "\$config['singleuser']['nickname'] = {$vals['nickname']};\n\n";
- } else {
- $cfg .= "\n";
- }
- // Normalize line endings for Windows servers
- $cfg = str_replace("\n", PHP_EOL, $cfg);
- // write configuration file out to install directory
- $res = file_put_contents(INSTALLDIR . '/config.php', $cfg, FILE_APPEND);
- return $res;
- }
- /**
- * Install schema into the database
- *
- * @param string $filename location of database schema file
- * @param MDB2_Driver_Common $conn connection to database
- *
- * @return bool - indicating success or failure
- */
- public function runDbScript(string $filename, $conn): bool
- {
- $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
- $stmts = explode(';', $sql);
- foreach ($stmts as $stmt) {
- $stmt = trim($stmt);
- if (!mb_strlen($stmt)) {
- continue;
- }
- try {
- $res = $conn->query($stmt);
- } catch (Exception $e) {
- $error = $e->getMessage();
- $this->updateStatus("ERROR ($error) for SQL '$stmt'");
- return false;
- }
- }
- return true;
- }
- /**
- * Create the initial admin user account.
- * Side effect: may load portions of GNU social framework.
- * Side effect: outputs program info
- */
- public function registerInitialUser(): bool
- {
- // initalize hostname from install arguments, so it can be used to find
- // the /etc config file from the commandline installer
- $server = $this->server;
- require_once INSTALLDIR . '/lib/util/common.php';
- $data = ['nickname' => $this->adminNick,
- 'password' => $this->adminPass,
- 'fullname' => $this->adminNick];
- if ($this->adminEmail) {
- $data['email'] = $this->adminEmail;
- }
- try {
- $user = User::register($data, true); // true to skip email sending verification
- } catch (Exception $e) {
- return false;
- }
- // give initial user carte blanche
- $user->grantRole('owner');
- $user->grantRole('moderator');
- $user->grantRole('administrator');
- return true;
- }
- /**
- * The beef of the installer!
- * Create database, config file, and admin user.
- *
- * Prerequisites: validation of input data.
- *
- * @return bool success
- */
- public function doInstall(): bool
- {
- global $config;
- $this->updateStatus("Initializing...");
- ini_set('display_errors', 1);
- error_reporting(E_ALL & ~E_STRICT & ~E_NOTICE);
- if (!defined('GNUSOCIAL')) {
- define('GNUSOCIAL', true);
- }
- if (!defined('STATUSNET')) {
- define('STATUSNET', true);
- }
- require_once INSTALLDIR . '/lib/util/framework.php';
- GNUsocial::initDefaults($this->server, $this->path);
- if ($this->siteProfile == "singleuser") {
- // Until we use ['site']['profile']==='singleuser' everywhere
- $config['singleuser']['enabled'] = true;
- }
- try {
- $this->db = $this->setupDatabase();
- if (!$this->db) {
- // database connection failed, do not move on to create config file.
- return false;
- }
- } catch (Exception $e) {
- // Lower-level DB error!
- $this->updateStatus("Database error: " . $e->getMessage(), true);
- return false;
- }
- if (!$this->skipConfig) {
- // Make sure we can write to the file twice
- $oldUmask = umask(000);
- $this->updateStatus("Writing config file...");
- $res = $this->writeConf();
- if (!$res) {
- $this->updateStatus("Can't write config file.", true);
- return false;
- }
- }
- if (!empty($this->adminNick)) {
- // Okay, cross fingers and try to register an initial user
- if ($this->registerInitialUser()) {
- $this->updateStatus(
- "An initial user with the administrator role has been created."
- );
- } else {
- $this->updateStatus(
- "Could not create initial user account.",
- true
- );
- return false;
- }
- }
- if (!$this->skipConfig) {
- $this->updateStatus("Setting site profile...");
- $res = $this->writeSiteProfile();
- if (!$res) {
- $this->updateStatus("Can't write to config file.", true);
- return false;
- }
- // Restore original umask
- umask($oldUmask);
- // Set permissions back to something decent
- chmod(INSTALLDIR . '/config.php', 0644);
- }
- $scheme = $this->ssl === 'always' ? 'https' : 'http';
- $link = "{$scheme}://{$this->server}/{$this->path}";
- $this->updateStatus("GNU social has been installed at $link");
- $this->updateStatus(
- '<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>.'
- );
- return true;
- }
- /**
- * Output a pre-install-time warning message
- * @param string $message HTML ok, but should be plaintext-able
- * @param string $submessage HTML ok, but should be plaintext-able
- */
- abstract public function warning(string $message, string $submessage = '');
- /**
- * Output an install-time progress message
- * @param string $status HTML ok, but should be plaintext-able
- * @param bool $error true if this should be marked as an error condition
- */
- abstract public function updateStatus(string $status, bool $error = false);
- }
|