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