allow empty admin password passend to setup.php
[tine20] / tine20 / Setup / Frontend / Cli.php
1 <?php
2 /**
3  * Tine 2.0
4  * @package     Tinebase
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Philipp Schüle <p.schuele@metaways.de>
7  * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8  * 
9  * @todo        add ext check again
10  */
11
12 /**
13  * cli server
14  *
15  * This class handles all requests from cli scripts
16  *
17  * @package     Tinebase
18  */
19 class Setup_Frontend_Cli
20 {
21     /**
22      * the internal name of the application
23      *
24      * @var string
25      */
26     protected $_appname = 'Setup';
27
28     /**
29      * authentication
30      *
31      * @param string $_username
32      * @param string $_password
33      * 
34      * @return boolean
35      */
36     public function authenticate($_username, $_password)
37     {
38         return false;
39     }
40     
41     /**
42      * handle request (call -ApplicationName-_Cli.-MethodName- or -ApplicationName-_Cli.getHelp)
43      *
44      * @param Zend_Console_Getopt $_opts
45      * @param boolean $exitAfterHandle
46      * @return void
47      */
48     public function handle(Zend_Console_Getopt $_opts, $exitAfterHandle = true)
49     {
50         Setup_Core::set(Setup_Core::USER, 'setupuser');
51         
52         $result = 0;
53         if (isset($_opts->install)) {
54             $this->_install($_opts);
55         } elseif(isset($_opts->update)) {
56             $result = $this->_update($_opts);
57         } elseif(isset($_opts->uninstall)) {
58             $this->_uninstall($_opts);
59         } elseif(isset($_opts->list)) {
60             $result = $this->_listInstalled();
61         } elseif(isset($_opts->sync_accounts_from_ldap)) {
62             $this->_importAccounts($_opts);
63         } elseif(isset($_opts->sync_passwords_from_ldap)) {
64             $this->_syncPasswords($_opts);
65         } elseif(isset($_opts->egw14import)) {
66             $this->_egw14Import($_opts);
67         } elseif(isset($_opts->check_requirements)) {
68             $this->_checkRequirements($_opts);
69         } elseif(isset($_opts->setconfig)) {
70             $this->_setConfig($_opts);
71         } elseif(isset($_opts->create_admin)) {
72             $this->_createAdminUser($_opts);
73         } elseif(isset($_opts->getconfig)) {
74             $this->_getConfig($_opts);
75         }
76         
77         if ($exitAfterHandle) {
78             exit($result);
79         }
80     }
81     
82     /**
83      * install new applications
84      *
85      * @param Zend_Console_Getopt $_opts
86      */
87     protected function _install(Zend_Console_Getopt $_opts)
88     {
89         $controller = Setup_Controller::getInstance();
90         
91         if($_opts->install === true) {
92             $applications = $controller->getInstallableApplications();
93             $applications = array_keys($applications);
94         } else {
95             $applications = array();
96             $applicationNames = explode(',', $_opts->install);
97             foreach($applicationNames as $applicationName) {
98                 $applicationName = ucfirst(trim($applicationName));
99                 try {
100                     $controller->getSetupXml($applicationName);
101                     $applications[] = $applicationName;
102                 } catch (Setup_Exception_NotFound $e) {
103                     echo "Application $applicationName not found! Skipped...\n";
104                 }
105             }
106         }
107         
108         $options = $this->_parseRemainingArgs($_opts->getRemainingArgs());
109         $this->_promptRemainingOptions($applications, $options);
110         
111         $controller->installApplications($applications, $options);
112         
113         if ((isset($options['acceptedTermsVersion']) || array_key_exists('acceptedTermsVersion', $options))) {
114             Setup_Controller::getInstance()->saveAcceptedTerms($options['acceptedTermsVersion']);
115         }
116         
117         echo "Successfully installed " . count($applications) . " applications.\n";
118     }
119
120     /**
121      * prompt remaining options
122      * 
123      * @param array $_applications
124      * @param array $_options
125      * @return void
126      * 
127      * @todo add required version server side
128      */
129     protected function _promptRemainingOptions($_applications, &$_options) {
130         if (in_array('Tinebase', $_applications)) {
131             
132             if (! isset($_options['acceptedTermsVersion'])) {
133                 fwrite(STDOUT, PHP_EOL . file_get_contents(dirname(dirname(dirname(__FILE__))) . '/LICENSE' ));
134                 $licenseAnswer = Tinebase_Server_Cli::promptInput('I have read the license agreement and accept it (type "yes" to accept)');
135                 
136                 
137                 fwrite(STDOUT, PHP_EOL . file_get_contents(dirname(dirname(dirname(__FILE__))) . '/PRIVACY' ));
138                 $privacyAnswer = Tinebase_Server_Cli::promptInput('I have read the privacy agreement and accept it (type "yes" to accept)');
139             
140                 if (! (strtoupper($licenseAnswer) == 'YES' && strtoupper($privacyAnswer) == 'YES')) {
141                     echo "error: you need to accept the terms! exiting \n";
142                     exit (1);
143                 }
144                 
145                 $_options['acceptedTermsVersion'] = 1;
146             }
147             
148             
149             // initial username
150             if (! isset($_options['adminLoginName'])) {
151                 $_options['adminLoginName'] = Tinebase_Server_Cli::promptInput('Inital Admin Users Username');
152                 if (! $_options['adminLoginName']) {
153                     echo "error: username must be given! exiting \n";
154                     exit (1);
155                 }
156             }
157             
158             // initial password / can be empty => will trigger password change dialogue
159             if (! array_key_exists('adminPassword', $_options)) {
160                 $_options['adminPassword'] = $this->_promptPassword();
161             }
162         }
163     }
164     
165     /**
166      * prompt password
167      * 
168      * @return string
169      */
170     protected function _promptPassword()
171     {
172         $password1 = Tinebase_Server_Cli::promptInput('Admin user password', TRUE);
173         if (! $password1) {
174             echo "Error: Password must not be empty! Exiting ... \n";
175             exit (1);
176         }
177         $password2 = Tinebase_Server_Cli::promptInput('Confirm password', TRUE);
178         if ($password1 !== $password2) {
179             echo "Error: Passwords do not match! Exiting ... \n";
180             exit (1);
181         }
182         
183         return $password1;
184     }
185     
186     /**
187      * update existing applications
188      *
189      * @param Zend_Console_Getopt $_opts
190      * @return integer
191      */
192     protected function _update(Zend_Console_Getopt $_opts)
193     {
194         $maxLoops = 50;
195         do {
196             $result = $this->_updateApplications();
197             if ($_opts->v && ! empty($result['messages'])) {
198                 echo "Messages:\n";
199                 foreach ($result['messages'] as $message) {
200                     echo "  " . $message . "\n";
201                 }
202             }
203             $maxLoops--;
204         } while ($result['updated'] > 0 && $maxLoops > 0);
205         
206         return ($maxLoops > 0) ? 0 : 1;
207     }
208     
209     /**
210      * update all applications
211      * 
212      * @return array
213      */
214     protected function _updateApplications()
215     {
216         $controller = Setup_Controller::getInstance();
217         $applications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
218         
219         foreach ($applications as $key => &$application) {
220             try {
221                 if (! $controller->updateNeeded($application)) {
222                     unset($applications[$key]);
223                 }
224             } catch (Setup_Exception_NotFound $e) {
225                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
226                     . ' Failed to check if an application needs an update:' . $e->getMessage());
227                 unset($applications[$key]);
228             }
229         }
230
231         $result = array();
232         if (count($applications) > 0) {
233             $result = $controller->updateApplications($applications);
234             echo "Updated " . $result['updated'] . " application(s).\n";
235         } else {
236             $result['updated'] = 0;
237         }
238         
239         return $result;
240     }
241
242     /**
243      * uninstall applications
244      *
245      * @param Zend_Console_Getopt $_opts
246      */
247     protected function _uninstall(Zend_Console_Getopt $_opts)
248     {
249         $controller = Setup_Controller::getInstance();
250         
251         if($_opts->uninstall === true) {
252             $applications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
253         } else {
254             $applications = new Tinebase_Record_RecordSet('Tinebase_Model_Application');
255             $applicationNames = explode(',', $_opts->uninstall);
256             foreach($applicationNames as $applicationName) {
257                 $applicationName = ucfirst(trim($applicationName));
258                 try {
259                     $application = Tinebase_Application::getInstance()->getApplicationByName($applicationName);
260                     $applications->addRecord($application);
261                 } catch (Tinebase_Exception_NotFound $e) {
262                 }
263             }
264         }
265         
266         $controller->uninstallApplications($applications->name);
267         
268         echo "Successfully uninstalled " . count($applications) . " applications.\n";
269     }
270
271     /**
272      * list installed apps
273      */
274     protected function _listInstalled()
275     {
276         try {
277             $applications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
278         } catch (Zend_Db_Statement_Exception $e) {
279             echo "No applications installed\n";
280             return 1;
281         }
282         
283         echo "Currently installed applications:\n";
284         foreach($applications as $application) {
285             echo "* $application\n";
286         }
287         
288         return 0;
289     }
290     
291     /**
292      * import accounts from ldap
293      *
294      * @param Zend_Console_Getopt $_opts
295      */
296     protected function _importAccounts(Zend_Console_Getopt $_opts)
297     {
298         // disable timelimit during import of user accounts
299         Setup_Core::setExecutionLifeTime(0);
300         
301         // import groups
302         if (! $_opts->onlyusers) {
303             Tinebase_Group::syncGroups();
304         }
305         
306         // import users
307         $options = array('syncContactData' => TRUE);
308         if ($_opts->dbmailldap) {
309             $options['ldapplugins'] = array(
310                 new Tinebase_EmailUser_Imap_LdapDbmailSchema(),
311                 new Tinebase_EmailUser_Smtp_LdapDbmailSchema()
312             );
313         }
314         Tinebase_User::syncUsers($options);
315     }
316     
317     /**
318      * sync ldap passwords
319      * 
320      * @param Zend_Console_Getopt $_opts
321      */
322     protected function _syncPasswords(Zend_Console_Getopt $_opts)
323     {
324         Tinebase_User::syncLdapPasswords();
325     }
326     
327     /**
328      * import from egw14
329      * 
330      * @param Zend_Console_Getopt $_opts
331      */
332     protected function _egw14Import(Zend_Console_Getopt $_opts)
333     {
334         $args = $_opts->getRemainingArgs();
335         
336         if (count($args) < 1 || ! is_readable($args[0])) {
337             echo "can not open config file \n";
338             echo "see tine20.org/wiki/EGW_Migration_Howto for details \n\n";
339             echo "usage: ./setup.php --egw14import /path/to/config.ini (see Tinebase/Setup/Import/Egw14/config.ini)\n\n";
340             exit(1);
341         }
342         
343         try {
344             $config = new Zend_Config(array(), TRUE);
345             $config->merge(new Zend_Config_Ini($args[0]));
346             $config = $config->merge($config->all);
347         } catch (Zend_Config_Exception $e) {
348             fwrite(STDERR, "Error while parsing config file($args[0]) " .  $e->getMessage() . PHP_EOL);
349             exit(1);
350         }
351         
352         $writer = new Zend_Log_Writer_Stream('php://output');
353         $logger = new Zend_Log($writer);
354         
355         $filter = new Zend_Log_Filter_Priority((int) $config->loglevel);
356         $logger->addFilter($filter);
357         
358         $importer = new Tinebase_Setup_Import_Egw14($config, $logger);
359         $importer->import();
360     }
361     
362     /**
363      * do the environment check
364      *
365      * @return array
366      */
367     protected function _checkRequirements(Zend_Console_Getopt $_opts)
368     {
369         $results = Setup_Controller::getInstance()->checkRequirements();
370         if ($results['success']) {
371           echo "OK - All requirements are met\n";
372         } else {
373           echo "ERRORS - The following requirements are not met: \n";
374           foreach ($results['results'] as $result) {
375             if (!empty($result['message'])) {
376               echo "- " . strip_tags($result['message']) . "\n";
377             }
378           }
379         }
380     }
381     
382     /**
383      * set config
384      *
385      * @return array
386      */
387     protected function _setConfig(Zend_Console_Getopt $_opts)
388     {
389         $options = $this->_parseRemainingArgs($_opts->getRemainingArgs());
390         $errors = array();
391         if (empty($options['configkey'])) {
392             $errors[] = 'Missing argument: configkey';
393         }
394         if (! isset($options['configvalue'])) {
395             $errors[] = 'Missing argument: configvalue';
396         }
397         $configKey = (string)$options['configkey'];
398         $configValue = self::parseConfigValue($options['configvalue']);
399         $applicationName = (isset($options['app'])) ? $options['app'] : 'Tinebase';
400         
401         if (empty($errors)) {
402            Setup_Controller::getInstance()->setConfigOption($configKey, $configValue, $applicationName);
403            echo "OK - Updated configuration option $configKey for application $applicationName\n";
404         } else {
405             echo "ERRORS - The following errors occured: \n";
406             foreach ($errors as $error) {
407                 echo "- " . $error . "\n";
408             }
409         }
410     }
411     
412     /**
413      * get config
414      *
415      */
416     protected function _getConfig(Zend_Console_Getopt $_opts)
417     {
418         $options = $this->_parseRemainingArgs($_opts->getRemainingArgs());
419         $applicationName = (isset($options['app'])) ? $options['app'] : 'Tinebase';
420         $config = Tinebase_Config_Abstract::factory($applicationName);
421         
422         $errors = array();
423         if (empty($options['configkey'])) {
424             $errors[] = 'Missing argument: configkey';
425             $errors[] = 'Available config settings:';
426             $errors[] = print_r($config::getProperties(), true);
427         }
428         $configKey = (string)$options['configkey'];
429         
430         if (empty($errors)) {
431             $value = $config->get($configKey);
432             $value = is_string($value) ? $value : Zend_Json::encode($value);
433             echo $value . " \n";
434         } else {
435             echo "ERRORS - The following errors occured: \n";
436             foreach ($errors as $error) {
437                 echo "- " . $error . "\n";
438             }
439         }
440     }
441     
442     /**
443      * create admin user / activate existing user / allow to reset password
444      * 
445      * @param Zend_Console_Getopt $_opts
446      * 
447      * @todo check role by rights and not by name
448      * @todo replace echos with stdout logger
449      */
450     protected function _createAdminUser(Zend_Console_Getopt $_opts)
451     {
452         if (! Setup_Controller::getInstance()->isInstalled('Tinebase')) {
453             die('Install Tinebase first.');
454         }
455         
456         echo "Please enter a username. If the user already exists, he is reactivated and you can reset the password.\n";
457         $username = Tinebase_Server_Cli::promptInput('Username');
458         $tomorrow = Tinebase_DateTime::now()->addDay(1);
459         
460         try {
461             $user = Tinebase_User::getInstance()->getFullUserByLoginName($username);
462             echo "User $username already exists.\n";
463             Tinebase_User::getInstance()->setStatus($user->getId(), Tinebase_Model_User::ACCOUNT_STATUS_ENABLED);
464             echo "Activated admin user '$username'.\n";
465             
466             $expire = Tinebase_Server_Cli::promptInput('Should the admin user expire tomorrow (default: "no", "y" or "yes" for expiry)?');
467             if ($expire === 'y' or $expire === 'yes') {
468                 Tinebase_User::getInstance()->setExpiryDate($user->getId(), $tomorrow);
469                 echo "User expires tomorrow at $tomorrow.\n";
470             }
471             
472             $resetPw = Tinebase_Server_Cli::promptInput('Do you want to reset the password (default: "no", "y" or "yes" for reset)?');
473             if ($resetPw === 'y' or $resetPw === 'yes') {
474                 $password = $this->_promptPassword();
475                 Tinebase_User::getInstance()->setPassword($user, $password);
476                 echo "User password has been reset.\n";
477             }
478             
479             $this->_checkAdminGroupMembership($user);
480             $this->_checkAdminRole($user);
481             
482         } catch (Tinebase_Exception_NotFound $tenf) {
483             // create new admin user that expires tomorrow
484             $password = $this->_promptPassword();
485             Tinebase_User::createInitialAccounts(array(
486                 'adminLoginName' => $username,
487                 'adminPassword'  => $password,
488                 'expires'        => $tomorrow,
489             ));
490             echo "Created new admin user '$username' that expires tomorrow.\n";
491         }
492     }
493
494     /**
495      * check admin group membership
496      * 
497      * @param Tinebase_Model_FullUser $user
498      */
499     protected function _checkAdminGroupMembership($user)
500     {
501         $adminGroup = Tinebase_Group::getInstance()->getDefaultAdminGroup();
502         $memberships = Tinebase_Group::getInstance()->getGroupMemberships($user);
503         if (! in_array($adminGroup->getId(), $memberships)) {
504             try {
505                 Tinebase_Group::getInstance()->addGroupMember($adminGroup, $user);
506                 echo "Added user to default admin group\n";
507                 // @todo clear roles/groups cache
508             } catch (Exception $e) {
509                 Tinebase_Exception::log($e);
510                 echo "Could not add user to default admin group: " . $e->getMessage();
511             }
512         }
513     }
514     
515     /**
516      * check admin role membership
517      * 
518      * @param Tinebase_Model_FullUser $user
519      */
520     protected function _checkAdminRole($user)
521     {
522         $roleMemberships = Tinebase_Acl_Roles::getInstance()->getRoleMemberships($user->getId());
523         $adminRoleFound = FALSE;
524         foreach ($roleMemberships as $roleId) {
525             $role = Tinebase_Acl_Roles::getInstance()->getRoleById($roleId);
526             if ($role->name === 'admin role') {
527                 $adminRoleFound = TRUE;
528                 //print_r(Tinebase_Acl_Roles::getInstance()->getRoleRights($role->getId()));
529                 break;
530             }
531         }
532         
533         if (! $adminRoleFound || ! Tinebase_Acl_Roles::getInstance()->hasRight('Admin', $user->getId(), Tinebase_Acl_Rights::ADMIN)) {
534             echo "Admin role not found for user " . $user->accountLoginName . ".\n";
535             $adminRole = new Tinebase_Model_Role(array(
536                 'name'                  => 'admin role',
537                 'description'           => 'admin role for tine. this role has all rights per default.',
538             ));
539             $adminRole = Tinebase_Acl_Roles::getInstance()->createRole($adminRole);
540             Tinebase_Acl_Roles::getInstance()->setRoleMembers($adminRole->getId(), array(
541                 array(
542                     'id'    => $user->getId(),
543                     'type'  => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER, 
544                 )
545             ));
546             
547             // add all rights for all apps
548             $enabledApps = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED);
549             $roleRights = array();
550             foreach ($enabledApps as $application) {
551                 $allRights = Tinebase_Application::getInstance()->getAllRights($application->getId());
552                 foreach ($allRights as $right) {
553                     $roleRights[] = array(
554                         'application_id' => $application->getId(),
555                         'right'          => $right,
556                     );
557                 }
558             }
559             Tinebase_Acl_Roles::getInstance()->setRoleRights($adminRole->getId(), $roleRights);
560             
561             echo "Created admin role for user " . $user->accountLoginName . ".\n";
562             // @todo clear roles/groups cache
563         }
564     }
565     
566     /**
567      * parse options
568      * 
569      * @param string $_value
570      * @return array|string
571      */
572     public static function parseConfigValue($_value)
573     {
574         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_value, TRUE));
575         
576         // check value is json encoded
577         if (Tinebase_Helper::is_json($_value)) {
578             return Zend_Json::decode($_value); 
579         }
580         
581         $result = array(
582             'active' => 1
583         );
584
585         // keep spaces, \: and \,
586         $_value = preg_replace(array('/ /', '/\\\:/', '/\\\,/', '/\s*/'), array('§', '@', ';', ''), $_value);
587         
588         $parts = explode(',', $_value);
589         
590         foreach ($parts as $part) {
591             $part = str_replace(';', ',', $part);
592             $part = str_replace('§', ' ', $part);
593             $part = str_replace('@', ':', $part);
594             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $part);
595             if (strpos($part, '_') !== FALSE) {
596                 list($key, $sub) = preg_split('/_/', $part, 2);
597                 if (preg_match('/:/', $sub)) {
598                     list($subKey, $value) = explode(':', $sub);
599                     $result[$key][$subKey] = $value;
600                 } else {
601                     // might be a '_' in the value
602                     if (preg_match('/:/', $part)) {
603                         $exploded = explode(':', $part);
604                         $key = array_shift($exploded);
605                         $result[$key] = implode(':', $exploded);
606                     } else {
607                         throw new Timetracker_Exception_UnexpectedValue('You have an error in the config syntax (":" expected): ' . $part);
608                     }
609                 }
610             } else {
611                 if (strpos($part, ':') !== FALSE) {
612                     list($key, $value) = preg_split('/:/', $part, 2);
613                     $result[$key] = $value;
614                 } else {
615                     $result = $part;
616                 }
617             }
618         }
619
620         return $result;
621     }
622     
623     /**
624      * parse remaining args
625      * 
626      * @param string $_args
627      * @return array
628      */
629     protected function _parseRemainingArgs($_args)
630     {
631         $options = array();
632         foreach ($_args as $arg) {
633             if (strpos($arg, '=') !== FALSE) {
634                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $arg);
635                 list($key, $value) = preg_split('/=/', $arg, 2);
636                 $options[$key] = $value;
637             }
638         }
639         
640         return $options;
641     }
642 }