We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

installer.php 20.5 KB
Newer Older
1 2 3 4
<?php

/**
 * StatusNet - the distributed open-source microblogging tool
5
 * Copyright (C) 2009-2010, StatusNet, Inc.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
 *
 * 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
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category Installation
 * @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   Robin Millette <millette@controlyourself.ca>
 * @author   Sarven Capadisli <csarven@status.net>
 * @author   Tom Adams <tom@holizz.com>
 * @author   Zach Copley <zach@status.net>
35
 * @copyright 2009-2010 StatusNet, Inc http://status.net
36
 * @copyright 2009 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 40 41 42 43 44
 * @link     http://status.net
 */

abstract class Installer
{
    /** Web site info */
Zach Copley's avatar
Zach Copley committed
45
    public $sitename, $server, $path, $fancy, $siteProfile;
46
    /** DB info */
47
    public $host, $database, $dbtype, $username, $password, $db;
48 49 50 51 52 53 54 55
    /** Administrator info */
    public $adminNick, $adminPass, $adminEmail, $adminUpdates;
    /** Should we skip writing the configuration file? */
    public $skipConfig = false;

    public static $dbModules = array(
        'mysql' => array(
            'name' => 'MySQL',
56
            'check_module' => 'mysqli',
57
            'scheme' => 'mysqli', // DSN prefix for PEAR::DB
58 59 60 61
        ),
        'pgsql' => array(
            'name' => 'PostgreSQL',
            'check_module' => 'pgsql',
62
            'scheme' => 'pgsql', // DSN prefix for PEAR::DB
63 64 65 66 67 68 69 70 71 72 73 74 75
        ),
    );

    /**
     * Attempt to include a PHP file and report if it worked, while
     * suppressing the annoying warning messages on failure.
     */
    private function haveIncludeFile($filename) {
        $old = error_reporting(error_reporting() & ~E_WARNING);
        $ok = include_once($filename);
        error_reporting($old);
        return $ok;
    }
Zach Copley's avatar
Zach Copley committed
76

77 78 79 80 81 82 83 84 85
    /**
     * Check if all is ready for installation
     *
     * @return void
     */
    function checkPrereqs()
    {
        $pass = true;

86 87 88
        $config = INSTALLDIR.'/config.php';
        if (file_exists($config)) {
            if (!is_writable($config) || filesize($config) > 0) {
89 90 91 92 93
                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.');
                }
94 95
                $pass = false;
            }
96 97 98
        }

        if (version_compare(PHP_VERSION, '5.2.3', '<')) {
99
            $this->warning('Require PHP version 5.2.3 or greater.');
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
            $pass = false;
        }

        // Look for known library bugs
        $str = "abcdefghijklmnopqrstuvwxyz";
        $replaced = preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
        if ($str != $replaced) {
            $this->warning('PHP is linked to a version of the PCRE library ' .
                           'that does not support Unicode properties. ' .
                           'If you are running Red Hat Enterprise Linux / ' .
                           'CentOS 5.4 or earlier, see <a href="' .
                           'http://status.net/wiki/Red_Hat_Enterprise_Linux#PCRE_library' .
                           '">our documentation page</a> on fixing this.');
            $pass = false;
        }

        $reqs = array('gd', 'curl',
                      'xmlwriter', 'mbstring', 'xml', 'dom', 'simplexml');

        foreach ($reqs as $req) {
            if (!$this->checkExtension($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 = array();
        foreach (self::$dbModules as $type => $info) {
            if (!$this->checkExtension($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;
        }

140
        // @fixme this check seems to be insufficient with Windows ACLs
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
        if (!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
        $fileSubdirs = array('avatar', 'background', 'file');
        foreach ($fileSubdirs as $fileSubdir) {
            $fileFullPath = INSTALLDIR."/$fileSubdir/";
            if (!is_writable($fileFullPath)) {
                $this->warning(sprintf('Cannot write to %s directory: <code>%s</code>', $fileSubdir, $fileFullPath),
                               sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath));
                $pass = false;
            }
        }

        return $pass;
    }

    /**
     * Checks if a php extension is both installed and loaded
     *
     * @param string $name of extension to check
     *
     * @return boolean whether extension is installed and loaded
     */
    function checkExtension($name)
    {
        if (extension_loaded($name)) {
            return true;
        } elseif (function_exists('dl') && ini_get('enable_dl') && !ini_get('safe_mode')) {
            // dl will throw a fatal error if it's disabled or we're in safe mode.
            // More fun, it may not even exist under some SAPIs in 5.3.0 or later...
            $soname = $name . '.' . PHP_SHLIB_SUFFIX;
            if (PHP_SHLIB_SUFFIX == 'dll') {
                $soname = "php_" . $soname;
            }
            return @dl($soname);
        } else {
            return false;
        }
    }

    /**
     * Basic validation on the database paramters
     * Side effects: error output if not valid
Zach Copley's avatar
Zach Copley committed
188
     *
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
     * @return boolean success
     */
    function validateDb()
    {
        $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 paramters
     * Side effects: error output if not valid
Zach Copley's avatar
Zach Copley committed
221
     *
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
     * @return boolean success
     */
    function validateAdmin()
    {
        $fail = false;

        if (empty($this->adminNick)) {
            $this->updateStatus("No initial StatusNet 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 User::allowed_nickname()
        // if/when it's safe to have loaded the infrastructure here
Evan Prodromou's avatar
Evan Prodromou committed
239
        $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');
240 241 242 243 244 245 246 247 248 249 250 251 252 253
        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 StatusNet user password specified.", true);
            $fail = true;
        }

        return !$fail;
    }

Zach Copley's avatar
Zach Copley committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    /**
     * Make sure a site profile was selected
     *
     * @return type boolean success
     */
    function validateSiteProfile()
    {
        $fail = false;

        $sprofile = $this->siteProfile;

        if (empty($sprofile))  {
            $this->updateStatus("No site profile selected.", true);
            $fail = true;
        }

        return !$fail;
    }

273 274 275
    /**
     * Set up the database with the appropriate function for the selected type...
     * Saves database info into $this->db.
Zach Copley's avatar
Zach Copley committed
276
     *
277
     * @fixme escape things in the connection string in case we have a funny pass etc
278 279 280 281 282 283 284 285 286
     * @return mixed array of database connection params on success, false on failure
     */
    function setupDatabase()
    {
        if ($this->db) {
            throw new Exception("Bad order of operations: DB already set up.");
        }
        $this->updateStatus("Starting installation...");

287
        if (empty($this->password)) {
288 289 290
            $auth = '';
        } else {
            $auth = ":$this->password";
291
        }
292
        $scheme = self::$dbModules[$this->dbtype]['scheme'];
293
        $dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}";
294

295 296
        $this->updateStatus("Checking database...");
        $conn = $this->connectDatabase($dsn);
297

298 299 300 301
        // ensure database encoding is UTF8
        if ($this->dbtype == 'mysql') {
            // @fixme utf8m4 support for mysql 5.5?
            // Force the comms charset to utf8 for sanity
302 303
            // This doesn't currently work. :P
            //$conn->executes('set names utf8');
304 305 306 307 308 309 310
        } 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;
            }
        }
311

312 313 314
        $res = $this->updateStatus("Creating database tables...");
        if (!$this->createCoreTables($conn)) {
            $this->updateStatus("Error creating tables.", true);
315 316
            return false;
        }
317

318 319 320 321 322
        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));
323
            $res = $this->runDbScript($scr.'.sql', $conn);
324
            if ($res === false) {
325
                $this->updateStatus(sprintf("Can't run %d script.", $name), true);
326 327 328 329
                return false;
            }
        }

330
        $db = array('type' => $this->dbtype, 'database' => $dsn);
331 332 333 334
        return $db;
    }

    /**
335 336 337
     * Open a connection to the database.
     *
     * @param <type> $dsn
Zach Copley's avatar
Zach Copley committed
338
     * @return <type>
339
     */
340
    function connectDatabase($dsn)
341
    {
342 343
        // @fixme move this someplace more sensible
        //set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
344 345 346
        require_once 'DB.php';
        return DB::connect($dsn);
    }
347

348 349 350 351 352 353 354 355 356 357
    /**
     * 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) {
358 359 360
            if (defined('DEBUG_INSTALLER')) {
                echo " $name ";
            }
361
            $schema->ensureTable($name, $def);
362
        }
Brion Vibber's avatar
Brion Vibber committed
363
        return true;
364
    }
365

366 367 368 369 370 371 372 373 374 375
    /**
     * 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;
376 377
    }

378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    /**
     * 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);
    }

402 403 404 405
    /**
     * Write a stock configuration file.
     *
     * @return boolean success
Zach Copley's avatar
Zach Copley committed
406
     *
407 408 409 410
     * @fixme escape variables in output in case we have funny chars, apostrophes etc
     */
    function writeConf()
    {
411 412 413 414 415 416
        $vals = $this->phpVals(array(
            'sitename' => $this->sitename,
            'server' => $this->server,
            'path' => $this->path,
            'db_database' => $this->db['database'],
            'db_type' => $this->db['type'],
417 418
            'site_profile' => $this->siteProfile,
            'nickname' => $this->adminNick
419 420
        ));

421 422 423 424 425
        // assemble configuration file in a string
        $cfg =  "<?php\n".
                "if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }\n\n".

                // site name
426
                "\$config['site']['name'] = {$vals['sitename']};\n\n".
427 428

                // site location
429 430
                "\$config['site']['server'] = {$vals['server']};\n".
                "\$config['site']['path'] = {$vals['path']}; \n\n".
431 432 433 434 435

                // checks if fancy URLs are enabled
                ($this->fancy ? "\$config['site']['fancy'] = true;\n\n":'').

                // database
436
                "\$config['db']['database'] = {$vals['db_database']};\n\n".
437
                ($this->db['type'] == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
Zach Copley's avatar
Zach Copley committed
438 439 440
                "\$config['db']['type'] = {$vals['db_type']};\n\n".

                // site profile
441 442 443 444 445 446 447
                "\$config['site']['profile'] = {$vals['site_profile']};\n";

        if ($this->siteProfile == "singleuser") {
            $cfg .= "\$config['singleuser']['nickname'] = {$vals['nickname']};\n\n";
        } else {
            $cfg .= "\n";
        }
448 449 450 451

        // Normalize line endings for Windows servers
        $cfg = str_replace("\n", PHP_EOL, $cfg);

452 453 454 455 456 457 458 459 460
        // write configuration file out to install directory
        $res = file_put_contents(INSTALLDIR.'/config.php', $cfg);

        return $res;
    }

    /**
     * Install schema into the database
     *
461 462
     * @param string    $filename location of database schema file
     * @param DB_common $conn     connection to database
463 464 465
     *
     * @return boolean - indicating success or failure
     */
466
    function runDbScript($filename, DB_common $conn)
467 468 469 470 471 472 473 474
    {
        $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
        $stmts = explode(';', $sql);
        foreach ($stmts as $stmt) {
            $stmt = trim($stmt);
            if (!mb_strlen($stmt)) {
                continue;
            }
Brion Vibber's avatar
Brion Vibber committed
475 476 477 478
            try {
                $res = $conn->simpleQuery($stmt);
            } catch (Exception $e) {
                $error = $e->getMessage();
479
                $this->updateStatus("ERROR ($error) for SQL '$stmt'");
Brion Vibber's avatar
Brion Vibber committed
480
                return false;
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
            }
        }
        return true;
    }

    /**
     * Create the initial admin user account.
     * Side effect: may load portions of StatusNet framework.
     * Side effect: outputs program info
     */
    function registerInitialUser()
    {
        require_once INSTALLDIR . '/lib/common.php';

        $data = array('nickname' => $this->adminNick,
                      'password' => $this->adminPass,
                      'fullname' => $this->adminNick);
        if ($this->adminEmail) {
            $data['email'] = $this->adminEmail;
        }
        $user = User::register($data);

        if (empty($user)) {
            return false;
        }

        // give initial user carte blanche

        $user->grantRole('owner');
        $user->grantRole('moderator');
        $user->grantRole('administrator');
Zach Copley's avatar
Zach Copley committed
512

513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
        // Attempt to do a remote subscribe to update@status.net
        // Will fail if instance is on a private network.

        if ($this->adminUpdates && class_exists('Ostatus_profile')) {
            try {
                $oprofile = Ostatus_profile::ensureProfileURL('http://update.status.net/');
                Subscription::start($user->getProfile(), $oprofile->localProfile());
                $this->updateStatus("Set up subscription to <a href='http://update.status.net/'>update@status.net</a>.");
            } catch (Exception $e) {
                $this->updateStatus("Could not set up subscription to <a href='http://update.status.net/'>update@status.net</a>.", true);
            }
        }

        return true;
    }

    /**
     * The beef of the installer!
     * Create database, config file, and admin user.
Zach Copley's avatar
Zach Copley committed
532
     *
533
     * Prerequisites: validation of input data.
Zach Copley's avatar
Zach Copley committed
534
     *
535 536 537 538
     * @return boolean success
     */
    function doInstall()
    {
539 540 541
        $this->updateStatus("Initializing...");
        ini_set('display_errors', 1);
        error_reporting(E_ALL);
542 543 544
        if (!defined('STATUSNET')) {
            define('STATUSNET', 1);
        }
545 546 547 548 549 550 551 552 553 554 555 556
        require_once INSTALLDIR . '/lib/framework.php';
        StatusNet::initDefaults($this->server, $this->path);

        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);
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
            return false;
        }

        if (!$this->skipConfig) {
            $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 StatusNet user (administrator).",
                    true
                );
                return false;
            }
        }

        /*
            TODO https needs to be considered
        */
        $link = "http://".$this->server.'/'.$this->path;

        $this->updateStatus("StatusNet has been installed at $link");
        $this->updateStatus(
            "<strong>DONE!</strong> You can visit your <a href='$link'>new StatusNet site</a> (login as '$this->adminNick'). If this is your first StatusNet install, you may want to poke around our <a href='http://status.net/wiki/Getting_started'>Getting Started guide</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 function warning($message, $submessage='');

    /**
     * Output an install-time progress message
     * @param string $message HTML ok, but should be plaintext-able
     * @param boolean $error true if this should be marked as an error condition
     */
    abstract function updateStatus($status, $error=false);

}