/**
* StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
+ * Copyright (C) 2009-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* @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>
+ * @copyright 2009-2010 StatusNet, Inc http://status.net
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license GNU Affero General Public License http://www.gnu.org/licenses/
- * @version 0.9.x
+ * @version 1.0.x
* @link http://status.net
*/
abstract class Installer
{
/** Web site info */
- public $sitename, $server, $path, $fancy;
+ public $sitename, $server, $path, $fancy, $siteProfile, $ssl;
/** DB info */
- public $host, $dbname, $dbtype, $username, $password, $db;
+ public $host, $database, $dbtype, $username, $password, $db;
/** Administrator info */
public $adminNick, $adminPass, $adminEmail, $adminUpdates;
/** Should we skip writing the configuration file? */
'mysql' => array(
'name' => 'MySQL',
'check_module' => 'mysqli',
- 'installer' => 'mysql_db_installer',
+ 'scheme' => 'mysqli', // DSN prefix for PEAR::DB
),
'pgsql' => array(
'name' => 'PostgreSQL',
'check_module' => 'pgsql',
- 'installer' => 'pgsql_db_installer',
+ 'scheme' => 'pgsql', // DSN prefix for PEAR::DB
),
);
error_reporting($old);
return $ok;
}
-
+
/**
* Check if all is ready for installation
*
$config = INSTALLDIR.'/config.php';
if (file_exists($config)) {
if (!is_writable($config) || filesize($config) > 0) {
- $this->warning('Config file "config.php" already exists.');
+ 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;
}
}
$pass = false;
}
- $reqs = array('gd', 'curl',
+ $reqs = array('gd', 'curl', 'json',
'xmlwriter', 'mbstring', 'xml', 'dom', 'simplexml');
foreach ($reqs as $req) {
/**
* Basic validation on the database paramters
* Side effects: error output if not valid
- *
+ *
* @return boolean success
*/
function validateDb()
/**
* Basic validation on the administrator user paramters
* Side effects: error output if not valid
- *
+ *
* @return boolean success
*/
function validateAdmin()
'" is invalid; should be plain letters and numbers no longer than 64 characters.', true);
$fail = true;
}
- // @fixme hardcoded list; should use User::allowed_nickname()
+ // @fixme hardcoded list; should use Nickname::isValid()
// if/when it's safe to have loaded the infrastructure here
- $blacklist = array('main', 'admin', 'twitter', 'settings', 'rsd.xml', 'favorited', 'featured', 'favoritedrss', 'featuredrss', 'rss', 'getfile', 'api', 'groups', 'group', 'peopletag', 'tag', 'user', 'message', 'conversation', 'bookmarklet', 'notice', 'attachment', 'search', 'index.php', 'doc', 'opensearch', 'robots.txt', 'xd_receiver.html', 'facebook');
+ $blacklist = array('main', 'panel', 'twitter', 'settings', 'rsd.xml', 'favorited', 'featured', 'favoritedrss', 'featuredrss', 'rss', 'getfile', 'api', 'groups', 'group', 'peopletag', 'tag', 'user', 'message', 'conversation', 'bookmarklet', 'notice', 'attachment', 'search', 'index.php', 'doc', 'opensearch', 'robots.txt', 'xd_receiver.html', 'facebook');
if (in_array($this->adminNick, $blacklist)) {
$this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
'" is reserved.', true);
}
/**
- * Set up the database with the appropriate function for the selected type...
- * Saves database info into $this->db.
- *
- * @return mixed array of database connection params on success, false on failure
+ * Make sure a site profile was selected
+ *
+ * @return type boolean success
*/
- function setupDatabase()
+ function validateSiteProfile()
{
- if ($this->db) {
- throw new Exception("Bad order of operations: DB already set up.");
+ if (empty($this->siteProfile)) {
+ $this->updateStatus("No site profile selected.", true);
+ return false;
}
- $method = self::$dbModules[$this->dbtype]['installer'];
- $db = call_user_func(array($this, $method),
- $this->host,
- $this->database,
- $this->username,
- $this->password);
- $this->db = $db;
- return $this->db;
+
+ return true;
}
/**
- * Set up a database on PostgreSQL.
- * Will output status updates during the operation.
- *
- * @param string $host
- * @param string $database
- * @param string $username
- * @param string $password
- * @return mixed array of database connection params on success, false on failure
- *
+ * 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
*/
- function Pgsql_Db_installer($host, $database, $username, $password)
+ function setupDatabase()
{
- $connstring = "dbname=$database host=$host user=$username";
-
- //No password would mean trust authentication used.
- if (!empty($password)) {
- $connstring .= " password=$password";
+ if ($this->db) {
+ throw new Exception("Bad order of operations: DB already set up.");
}
$this->updateStatus("Starting installation...");
- $this->updateStatus("Checking database...");
- $conn = pg_connect($connstring);
- if ($conn ===false) {
- $this->updateStatus("Failed to connect to database: $connstring");
- return false;
+ if (empty($this->password)) {
+ $auth = '';
+ } else {
+ $auth = ":$this->password";
}
+ $scheme = self::$dbModules[$this->dbtype]['scheme'];
+ $dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}";
- //ensure database encoding is UTF8
- $record = pg_fetch_object(pg_query($conn, 'SHOW server_encoding'));
- if ($record->server_encoding != 'UTF8') {
- $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
- return false;
+ $this->updateStatus("Checking database...");
+ $conn = $this->connectDatabase($dsn);
+
+ // ensure database encoding is UTF8
+ if ($this->dbtype == 'mysql') {
+ // @fixme utf8m4 support for mysql 5.5?
+ // Force the comms charset to utf8 for sanity
+ // This doesn't currently work. :P
+ //$conn->executes('set names utf8');
+ } else if ($this->dbtype == 'pgsql') {
+ $record = $conn->getRow('SHOW server_encoding');
+ if ($record->server_encoding != 'UTF8') {
+ $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
+ return false;
+ }
}
- $this->updateStatus("Running database script...");
- //wrap in transaction;
- pg_query($conn, 'BEGIN');
- $res = $this->runDbScript('statusnet_pg.sql', $conn, 'pgsql');
-
- if ($res === false) {
- $this->updateStatus("Can't run database script.", true);
+ $res = $this->updateStatus("Creating database tables...");
+ if (!$this->createCoreTables($conn)) {
+ $this->updateStatus("Error creating tables.", true);
return false;
}
+
foreach (array('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, 'pgsql');
+ $res = $this->runDbScript($scr.'.sql', $conn);
if ($res === false) {
$this->updateStatus(sprintf("Can't run %s script.", $name), true);
return false;
}
}
- pg_query($conn, 'COMMIT');
-
- if (empty($password)) {
- $sqlUrl = "pgsql://$username@$host/$database";
- } else {
- $sqlUrl = "pgsql://$username:$password@$host/$database";
- }
-
- $db = array('type' => 'pgsql', 'database' => $sqlUrl);
+ $db = array('type' => $this->dbtype, 'database' => $dsn);
return $db;
}
/**
- * Set up a database on MySQL.
- * Will output status updates during the operation.
- *
- * @param string $host
- * @param string $database
- * @param string $username
- * @param string $password
- * @return mixed array of database connection params on success, false on failure
- *
- * @fixme escape things in the connection string in case we have a funny pass etc
+ * Open a connection to the database.
+ *
+ * @param <type> $dsn
+ * @return <type>
*/
- function Mysql_Db_installer($host, $database, $username, $password)
+ function connectDatabase($dsn)
{
- $this->updateStatus("Starting installation...");
- $this->updateStatus("Checking database...");
-
- $conn = mysqli_init();
- if (!$conn->real_connect($host, $username, $password)) {
- $this->updateStatus("Can't connect to server '$host' as '$username'.", true);
- return false;
- }
- $this->updateStatus("Changing to database...");
- if (!$conn->select_db($database)) {
- $this->updateStatus("Can't change to database.", true);
- return false;
- }
+ global $_DB;
+ return $_DB->connect($dsn);
+ }
- $this->updateStatus("Running database script...");
- $res = $this->runDbScript('statusnet.sql', $conn);
- if ($res === false) {
- $this->updateStatus("Can't run database script.", true);
- return false;
- }
- foreach (array('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 %d script.", $name), true);
- return false;
+ /**
+ * Create core tables on the given database connection.
+ *
+ * @param DB_common $conn
+ */
+ function createCoreTables(DB_common $conn)
+ {
+ $schema = Schema::get($conn);
+ $tableDefs = $this->getCoreSchema();
+ foreach ($tableDefs as $name => $def) {
+ if (defined('DEBUG_INSTALLER')) {
+ echo " $name ";
}
+ $schema->ensureTable($name, $def);
}
+ return true;
+ }
- $sqlUrl = "mysqli://$username:$password@$host/$database";
- $db = array('type' => 'mysql', 'database' => $sqlUrl);
- return $db;
+ /**
+ * Fetch the core table schema definitions.
+ *
+ * @return array of table names => table def arrays
+ */
+ function getCoreSchema()
+ {
+ $schema = array();
+ 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
+ */
+ function phpVal($val)
+ {
+ 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 $val
+ * @return array
+ */
+ function phpVals($map)
+ {
+ return array_map(array($this, 'phpVal'), $map);
}
/**
* Write a stock configuration file.
*
* @return boolean success
- *
+ *
* @fixme escape variables in output in case we have funny chars, apostrophes etc
*/
function writeConf()
{
+ $vals = $this->phpVals(array(
+ 'sitename' => $this->sitename,
+ 'server' => $this->server,
+ 'path' => $this->path,
+ 'ssl' => in_array($this->ssl, array('never', 'sometimes', '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('STATUSNET') && !defined('LACONICA')) { exit(1); }\n\n".
+ "if (!defined('GNUSOCIAL')) { exit(1); }\n\n".
// site name
- "\$config['site']['name'] = '{$this->sitename}';\n\n".
+ "\$config['site']['name'] = {$vals['sitename']};\n\n".
// site location
- "\$config['site']['server'] = '{$this->server}';\n".
- "\$config['site']['path'] = '{$this->path}'; \n\n".
+ "\$config['site']['server'] = {$vals['server']};\n".
+ "\$config['site']['path'] = {$vals['path']}; \n\n".
+ "\$config['site']['ssl'] = {$vals['ssl']}; \n\n".
// checks if fancy URLs are enabled
($this->fancy ? "\$config['site']['fancy'] = true;\n\n":'').
// database
- "\$config['db']['database'] = '{$this->db['database']}';\n\n".
+ "\$config['db']['database'] = {$vals['db_database']};\n\n".
($this->db['type'] == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
- "\$config['db']['type'] = '{$this->db['type']}';\n\n";
+ "\$config['db']['type'] = {$vals['db_type']};\n\n";
// Normalize line endings for Windows servers
$cfg = str_replace("\n", PHP_EOL, $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
+ */
+ function writeSiteProfile()
+ {
+ $vals = $this->phpVals(array(
+ '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 dbconn $conn connection to database
- * @param string $type type of database, currently mysql or pgsql
+ * @param string $filename location of database schema file
+ * @param DB_common $conn connection to database
*
* @return boolean - indicating success or failure
*/
- function runDbScript($filename, $conn, $type = 'mysqli')
+ function runDbScript($filename, DB_common $conn)
{
$sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
$stmts = explode(';', $sql);
if (!mb_strlen($stmt)) {
continue;
}
- // FIXME: use PEAR::DB or PDO instead of our own switch
- switch ($type) {
- case 'mysqli':
- $res = $conn->query($stmt);
- if ($res === false) {
- $error = $conn->error;
- }
- break;
- case 'pgsql':
- $res = pg_query($conn, $stmt);
- if ($res === false) {
- $error = pg_last_error();
- }
- break;
- default:
- $this->updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
- }
- if ($res === false) {
+ try {
+ $res = $conn->simpleQuery($stmt);
+ } catch (Exception $e) {
+ $error = $e->getMessage();
$this->updateStatus("ERROR ($error) for SQL '$stmt'");
- return $res;
+ return false;
}
}
return true;
*/
function registerInitialUser()
{
- define('STATUSNET', true);
- define('LACONICA', true); // compatibility
-
require_once INSTALLDIR . '/lib/common.php';
$data = array('nickname' => $this->adminNick,
$user->grantRole('owner');
$user->grantRole('moderator');
$user->grantRole('administrator');
-
+
// Attempt to do a remote subscribe to update@status.net
// Will fail if instance is on a private network.
/**
* The beef of the installer!
* Create database, config file, and admin user.
- *
+ *
* Prerequisites: validation of input data.
- *
+ *
* @return boolean success
*/
function doInstall()
{
- $this->db = $this->setupDatabase();
+ global $config;
+
+ $this->updateStatus("Initializing...");
+ ini_set('display_errors', 1);
+ error_reporting(E_ALL);
+ if (!defined('GNUSOCIAL')) {
+ define('GNUSOCIAL', true);
+ }
+ if (!defined('STATUSNET')) {
+ define('STATUSNET', true);
+ }
+
+ require_once INSTALLDIR . '/lib/framework.php';
+ StatusNet::initDefaults($this->server, $this->path);
+
+ if ($this->siteProfile == "singleuser") {
+ // Until we use ['site']['profile']==='singleuser' everywhere
+ $config['singleuser']['enabled'] = true;
+ }
- if (!$this->db) {
- // database connection failed, do not move on to create config file.
+ 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;
}
+ // Make sure we can write to the file twice
+ $oldUmask = umask(000);
+
if (!$this->skipConfig) {
$this->updateStatus("Writing config file...");
$res = $this->writeConf();
);
} else {
$this->updateStatus(
- "Could not create initial StatusNet user (administrator).",
+ "Could not create initial GNU social user.",
true
);
return false;
}
}
- /*
- TODO https needs to be considered
- */
- $link = "http://".$this->server.'/'.$this->path;
+ 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("StatusNet has been installed at $link");
$this->updateStatus(