]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/installer.php
Merge branch 'apinamespace' into 0.9.x
[quix0rs-gnu-social.git] / lib / installer.php
1 <?php
2
3 /**
4  * StatusNet - the distributed open-source microblogging tool
5  * Copyright (C) 2009, 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 Free Software Foundation, Inc http://www.fsf.org
36  * @license  GNU Affero General Public License http://www.gnu.org/licenses/
37  * @version  0.9.x
38  * @link     http://status.net
39  */
40
41 abstract class Installer
42 {
43     /** Web site info */
44     public $sitename, $server, $path, $fancy;
45     /** DB info */
46     public $host, $dbname, $dbtype, $username, $password, $db;
47     /** Administrator info */
48     public $adminNick, $adminPass, $adminEmail, $adminUpdates;
49     /** Should we skip writing the configuration file? */
50     public $skipConfig = false;
51
52     public static $dbModules = array(
53         'mysql' => array(
54             'name' => 'MySQL',
55             'check_module' => 'mysqli',
56             'installer' => 'mysql_db_installer',
57         ),
58         'pgsql' => array(
59             'name' => 'PostgreSQL',
60             'check_module' => 'pgsql',
61             'installer' => 'pgsql_db_installer',
62         ),
63     );
64
65     /**
66      * Attempt to include a PHP file and report if it worked, while
67      * suppressing the annoying warning messages on failure.
68      */
69     private function haveIncludeFile($filename) {
70         $old = error_reporting(error_reporting() & ~E_WARNING);
71         $ok = include_once($filename);
72         error_reporting($old);
73         return $ok;
74     }
75     
76     /**
77      * Check if all is ready for installation
78      *
79      * @return void
80      */
81     function checkPrereqs()
82     {
83         $pass = true;
84
85         $config = INSTALLDIR.'/config.php';
86         if (file_exists($config)) {
87             if (!is_writable($config) || filesize($config) > 0) {
88                 $this->warning('Config file "config.php" already exists.');
89                 $pass = false;
90             }
91         }
92
93         if (version_compare(PHP_VERSION, '5.2.3', '<')) {
94             $this->warning('Require PHP version 5.2.3 or greater.');
95             $pass = false;
96         }
97
98         // Look for known library bugs
99         $str = "abcdefghijklmnopqrstuvwxyz";
100         $replaced = preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
101         if ($str != $replaced) {
102             $this->warning('PHP is linked to a version of the PCRE library ' .
103                            'that does not support Unicode properties. ' .
104                            'If you are running Red Hat Enterprise Linux / ' .
105                            'CentOS 5.4 or earlier, see <a href="' .
106                            'http://status.net/wiki/Red_Hat_Enterprise_Linux#PCRE_library' .
107                            '">our documentation page</a> on fixing this.');
108             $pass = false;
109         }
110
111         $reqs = array('gd', 'curl',
112                       'xmlwriter', 'mbstring', 'xml', 'dom', 'simplexml');
113
114         foreach ($reqs as $req) {
115             if (!$this->checkExtension($req)) {
116                 $this->warning(sprintf('Cannot load required extension: <code>%s</code>', $req));
117                 $pass = false;
118             }
119         }
120
121         // Make sure we have at least one database module available
122         $missingExtensions = array();
123         foreach (self::$dbModules as $type => $info) {
124             if (!$this->checkExtension($info['check_module'])) {
125                 $missingExtensions[] = $info['check_module'];
126             }
127         }
128
129         if (count($missingExtensions) == count(self::$dbModules)) {
130             $req = implode(', ', $missingExtensions);
131             $this->warning(sprintf('Cannot find a database extension. You need at least one of %s.', $req));
132             $pass = false;
133         }
134
135         // @fixme this check seems to be insufficient with Windows ACLs
136         if (!is_writable(INSTALLDIR)) {
137             $this->warning(sprintf('Cannot write config file to: <code>%s</code></p>', INSTALLDIR),
138                            sprintf('On your server, try this command: <code>chmod a+w %s</code>', INSTALLDIR));
139             $pass = false;
140         }
141
142         // Check the subdirs used for file uploads
143         $fileSubdirs = array('avatar', 'background', 'file');
144         foreach ($fileSubdirs as $fileSubdir) {
145             $fileFullPath = INSTALLDIR."/$fileSubdir/";
146             if (!is_writable($fileFullPath)) {
147                 $this->warning(sprintf('Cannot write to %s directory: <code>%s</code>', $fileSubdir, $fileFullPath),
148                                sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath));
149                 $pass = false;
150             }
151         }
152
153         return $pass;
154     }
155
156     /**
157      * Checks if a php extension is both installed and loaded
158      *
159      * @param string $name of extension to check
160      *
161      * @return boolean whether extension is installed and loaded
162      */
163     function checkExtension($name)
164     {
165         if (extension_loaded($name)) {
166             return true;
167         } elseif (function_exists('dl') && ini_get('enable_dl') && !ini_get('safe_mode')) {
168             // dl will throw a fatal error if it's disabled or we're in safe mode.
169             // More fun, it may not even exist under some SAPIs in 5.3.0 or later...
170             $soname = $name . '.' . PHP_SHLIB_SUFFIX;
171             if (PHP_SHLIB_SUFFIX == 'dll') {
172                 $soname = "php_" . $soname;
173             }
174             return @dl($soname);
175         } else {
176             return false;
177         }
178     }
179
180     /**
181      * Basic validation on the database paramters
182      * Side effects: error output if not valid
183      * 
184      * @return boolean success
185      */
186     function validateDb()
187     {
188         $fail = false;
189
190         if (empty($this->host)) {
191             $this->updateStatus("No hostname specified.", true);
192             $fail = true;
193         }
194
195         if (empty($this->database)) {
196             $this->updateStatus("No database specified.", true);
197             $fail = true;
198         }
199
200         if (empty($this->username)) {
201             $this->updateStatus("No username specified.", true);
202             $fail = true;
203         }
204
205         if (empty($this->sitename)) {
206             $this->updateStatus("No sitename specified.", true);
207             $fail = true;
208         }
209
210         return !$fail;
211     }
212
213     /**
214      * Basic validation on the administrator user paramters
215      * Side effects: error output if not valid
216      * 
217      * @return boolean success
218      */
219     function validateAdmin()
220     {
221         $fail = false;
222
223         if (empty($this->adminNick)) {
224             $this->updateStatus("No initial StatusNet user nickname specified.", true);
225             $fail = true;
226         }
227         if ($this->adminNick && !preg_match('/^[0-9a-z]{1,64}$/', $this->adminNick)) {
228             $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
229                          '" is invalid; should be plain letters and numbers no longer than 64 characters.', true);
230             $fail = true;
231         }
232         // @fixme hardcoded list; should use User::allowed_nickname()
233         // if/when it's safe to have loaded the infrastructure here
234         $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');
235         if (in_array($this->adminNick, $blacklist)) {
236             $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
237                          '" is reserved.', true);
238             $fail = true;
239         }
240
241         if (empty($this->adminPass)) {
242             $this->updateStatus("No initial StatusNet user password specified.", true);
243             $fail = true;
244         }
245
246         return !$fail;
247     }
248
249     /**
250      * Set up the database with the appropriate function for the selected type...
251      * Saves database info into $this->db.
252      * 
253      * @return mixed array of database connection params on success, false on failure
254      */
255     function setupDatabase()
256     {
257         if ($this->db) {
258             throw new Exception("Bad order of operations: DB already set up.");
259         }
260         $method = self::$dbModules[$this->dbtype]['installer'];
261         $db = call_user_func(array($this, $method),
262                              $this->host,
263                              $this->database,
264                              $this->username,
265                              $this->password);
266         $this->db = $db;
267         return $this->db;
268     }
269
270     /**
271      * Set up a database on PostgreSQL.
272      * Will output status updates during the operation.
273      * 
274      * @param string $host
275      * @param string $database
276      * @param string $username
277      * @param string $password
278      * @return mixed array of database connection params on success, false on failure
279      * 
280      * @fixme escape things in the connection string in case we have a funny pass etc
281      */
282     function Pgsql_Db_installer($host, $database, $username, $password)
283     {
284         $connstring = "dbname=$database host=$host user=$username";
285
286         //No password would mean trust authentication used.
287         if (!empty($password)) {
288             $connstring .= " password=$password";
289         }
290         $this->updateStatus("Starting installation...");
291         $this->updateStatus("Checking database...");
292         $conn = pg_connect($connstring);
293
294         if ($conn ===false) {
295             $this->updateStatus("Failed to connect to database: $connstring");
296             return false;
297         }
298
299         //ensure database encoding is UTF8
300         $record = pg_fetch_object(pg_query($conn, 'SHOW server_encoding'));
301         if ($record->server_encoding != 'UTF8') {
302             $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
303             return false;
304         }
305
306         $this->updateStatus("Running database script...");
307         //wrap in transaction;
308         pg_query($conn, 'BEGIN');
309         $res = $this->runDbScript('statusnet_pg.sql', $conn, 'pgsql');
310
311         if ($res === false) {
312             $this->updateStatus("Can't run database script.", true);
313             return false;
314         }
315         foreach (array('sms_carrier' => 'SMS carrier',
316                     'notice_source' => 'notice source',
317                     'foreign_services' => 'foreign service')
318               as $scr => $name) {
319             $this->updateStatus(sprintf("Adding %s data to database...", $name));
320             $res = $this->runDbScript($scr.'.sql', $conn, 'pgsql');
321             if ($res === false) {
322                 $this->updateStatus(sprintf("Can't run %s script.", $name), true);
323                 return false;
324             }
325         }
326         pg_query($conn, 'COMMIT');
327
328         if (empty($password)) {
329             $sqlUrl = "pgsql://$username@$host/$database";
330         } else {
331             $sqlUrl = "pgsql://$username:$password@$host/$database";
332         }
333
334         $db = array('type' => 'pgsql', 'database' => $sqlUrl);
335
336         return $db;
337     }
338
339     /**
340      * Set up a database on MySQL.
341      * Will output status updates during the operation.
342      * 
343      * @param string $host
344      * @param string $database
345      * @param string $username
346      * @param string $password
347      * @return mixed array of database connection params on success, false on failure
348      * 
349      * @fixme escape things in the connection string in case we have a funny pass etc
350      */
351     function Mysql_Db_installer($host, $database, $username, $password)
352     {
353         $this->updateStatus("Starting installation...");
354         $this->updateStatus("Checking database...");
355
356         $conn = mysqli_init();
357         if (!$conn->real_connect($host, $username, $password)) {
358             $this->updateStatus("Can't connect to server '$host' as '$username'.", true);
359             return false;
360         }
361         $this->updateStatus("Changing to database...");
362         if (!$conn->select_db($database)) {
363             $this->updateStatus("Can't change to database.", true);
364             return false;
365         }
366
367         $this->updateStatus("Running database script...");
368         $res = $this->runDbScript('statusnet.sql', $conn);
369         if ($res === false) {
370             $this->updateStatus("Can't run database script.", true);
371             return false;
372         }
373         foreach (array('sms_carrier' => 'SMS carrier',
374                     'notice_source' => 'notice source',
375                     'foreign_services' => 'foreign service')
376               as $scr => $name) {
377             $this->updateStatus(sprintf("Adding %s data to database...", $name));
378             $res = $this->runDbScript($scr.'.sql', $conn);
379             if ($res === false) {
380                 $this->updateStatus(sprintf("Can't run %d script.", $name), true);
381                 return false;
382             }
383         }
384
385         $sqlUrl = "mysqli://$username:$password@$host/$database";
386         $db = array('type' => 'mysql', 'database' => $sqlUrl);
387         return $db;
388     }
389
390     /**
391      * Write a stock configuration file.
392      *
393      * @return boolean success
394      * 
395      * @fixme escape variables in output in case we have funny chars, apostrophes etc
396      */
397     function writeConf()
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'] = '{$this->sitename}';\n\n".
405
406                 // site location
407                 "\$config['site']['server'] = '{$this->server}';\n".
408                 "\$config['site']['path'] = '{$this->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'] = '{$this->db['database']}';\n\n".
415                 ($this->db['type'] == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
416                 "\$config['db']['type'] = '{$this->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 dbconn $conn     connection to database
432      * @param string $type     type of database, currently mysql or pgsql
433      *
434      * @return boolean - indicating success or failure
435      */
436     function runDbScript($filename, $conn, $type = 'mysqli')
437     {
438         $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
439         $stmts = explode(';', $sql);
440         foreach ($stmts as $stmt) {
441             $stmt = trim($stmt);
442             if (!mb_strlen($stmt)) {
443                 continue;
444             }
445             // FIXME: use PEAR::DB or PDO instead of our own switch
446             switch ($type) {
447             case 'mysqli':
448                 $res = $conn->query($stmt);
449                 if ($res === false) {
450                     $error = $conn->error;
451                 }
452                 break;
453             case 'pgsql':
454                 $res = pg_query($conn, $stmt);
455                 if ($res === false) {
456                     $error = pg_last_error();
457                 }
458                 break;
459             default:
460                 $this->updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
461             }
462             if ($res === false) {
463                 $this->updateStatus("ERROR ($error) for SQL '$stmt'");
464                 return $res;
465             }
466         }
467         return true;
468     }
469
470     /**
471      * Create the initial admin user account.
472      * Side effect: may load portions of StatusNet framework.
473      * Side effect: outputs program info
474      */
475     function registerInitialUser()
476     {
477         define('STATUSNET', true);
478         define('LACONICA', true); // compatibility
479
480         require_once INSTALLDIR . '/lib/common.php';
481
482         $data = array('nickname' => $this->adminNick,
483                       'password' => $this->adminPass,
484                       'fullname' => $this->adminNick);
485         if ($this->adminEmail) {
486             $data['email'] = $this->adminEmail;
487         }
488         $user = User::register($data);
489
490         if (empty($user)) {
491             return false;
492         }
493
494         // give initial user carte blanche
495
496         $user->grantRole('owner');
497         $user->grantRole('moderator');
498         $user->grantRole('administrator');
499         
500         // Attempt to do a remote subscribe to update@status.net
501         // Will fail if instance is on a private network.
502
503         if ($this->adminUpdates && class_exists('Ostatus_profile')) {
504             try {
505                 $oprofile = Ostatus_profile::ensureProfileURL('http://update.status.net/');
506                 Subscription::start($user->getProfile(), $oprofile->localProfile());
507                 $this->updateStatus("Set up subscription to <a href='http://update.status.net/'>update@status.net</a>.");
508             } catch (Exception $e) {
509                 $this->updateStatus("Could not set up subscription to <a href='http://update.status.net/'>update@status.net</a>.", true);
510             }
511         }
512
513         return true;
514     }
515
516     /**
517      * The beef of the installer!
518      * Create database, config file, and admin user.
519      * 
520      * Prerequisites: validation of input data.
521      * 
522      * @return boolean success
523      */
524     function doInstall()
525     {
526         $this->db = $this->setupDatabase();
527
528         if (!$this->db) {
529             // database connection failed, do not move on to create config file.
530             return false;
531         }
532
533         if (!$this->skipConfig) {
534             $this->updateStatus("Writing config file...");
535             $res = $this->writeConf();
536
537             if (!$res) {
538                 $this->updateStatus("Can't write config file.", true);
539                 return false;
540             }
541         }
542
543         if (!empty($this->adminNick)) {
544             // Okay, cross fingers and try to register an initial user
545             if ($this->registerInitialUser()) {
546                 $this->updateStatus(
547                     "An initial user with the administrator role has been created."
548                 );
549             } else {
550                 $this->updateStatus(
551                     "Could not create initial StatusNet user (administrator).",
552                     true
553                 );
554                 return false;
555             }
556         }
557
558         /*
559             TODO https needs to be considered
560         */
561         $link = "http://".$this->server.'/'.$this->path;
562
563         $this->updateStatus("StatusNet has been installed at $link");
564         $this->updateStatus(
565             "<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>."
566         );
567
568         return true;
569     }
570
571     /**
572      * Output a pre-install-time warning message
573      * @param string $message HTML ok, but should be plaintext-able
574      * @param string $submessage HTML ok, but should be plaintext-able
575      */
576     abstract function warning($message, $submessage='');
577
578     /**
579      * Output an install-time progress message
580      * @param string $message HTML ok, but should be plaintext-able
581      * @param boolean $error true if this should be marked as an error condition
582      */
583     abstract function updateStatus($status, $error=false);
584
585 }