allow sync force via userID
[tine20] / tine20 / Tinebase / Frontend / Cli.php
1 <?php
2 /**
3  * Tine 2.0
4  * @package     Tinebase
5  * @subpackage  Frontend
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Philipp Schüle <p.schuele@metaways.de>
8  * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  */
10
11 /**
12  * cli server
13  *
14  * This class handles all requests from cli scripts
15  *
16  * @package     Tinebase
17  * @subpackage  Frontend
18  */
19 class Tinebase_Frontend_Cli extends Tinebase_Frontend_Cli_Abstract
20 {
21     /**
22      * the internal name of the application
23      *
24      * @var string
25      */
26     protected $_applicationName = 'Tinebase';
27
28     /**
29      * needed by demo data fns
30      *
31      * @var array
32      */
33     protected $_applicationsToWorkOn = array();
34
35     /**
36     * @param Zend_Console_Getopt $_opts
37     * @return boolean success
38     */
39     public function rebuildPaths($opts)
40     {
41         if (! $this->_checkAdminRight()) {
42             return -1;
43         }
44
45         $applications = Tinebase_Application::getInstance()->getApplications();
46         foreach($applications as $application) {
47             try {
48                 $app = Tinebase_Core::getApplicationInstance($application, '', true);
49             } catch (Tinebase_Exception_NotFound $tenf) {
50                 continue;
51             }
52
53             if (! $app instanceof Tinebase_Controller_Abstract) {
54                 continue;
55             }
56
57             $pathModels = $app->getModelsUsingPaths();
58             if (!is_array($pathModels)) {
59                 $pathModels = array();
60             }
61             foreach($pathModels as $pathModel) {
62                 $controller = Tinebase_Core::getApplicationInstance($pathModel, '', true);
63
64                 $_filter = $pathModel . 'Filter';
65                 $_filter = new $_filter();
66
67                 $iterator = new Tinebase_Record_Iterator(array(
68                     'iteratable' => $this,
69                     'controller' => $controller,
70                     'filter' => $_filter,
71                     'options' => array('getRelations' => true),
72                     'function' => 'rebuildPathsIteration',
73                 ));
74                 $result = $iterator->iterate($controller);
75
76                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
77                     if (false === $result) {
78                         $result['totalcount'] = 0;
79                     }
80                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Build paths for ' . $result['totalcount'] . ' records of ' . $pathModel);
81                 }
82             }
83         }
84     }
85
86     /**
87      * rebuild paths for multiple records in an iteration
88      * @see Tinebase_Record_Iterator / self::rebuildPaths()
89      *
90      * @param Tinebase_Record_RecordSet $records
91      * @param Tinebase_Controller_Abstract $controller
92      */
93     public function rebuildPathsIteration(Tinebase_Record_RecordSet $records, Tinebase_Controller_Record_Abstract $controller)
94     {
95         foreach ($records as $record) {
96             try {
97                 $controller->buildPath($record, true);
98             } catch (Exception $e) {
99                 Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' record path building failed: '
100                     . $e->getMessage() . PHP_EOL
101                     . $e->getTraceAsString() . PHP_EOL
102                     . $record->toArray());
103             }
104         }
105     }
106
107     /**
108      * forces containers that support sync token to resync via WebDAV sync tokens
109      *
110      * this will cause 2 BadRequest responses to sync token requests
111      * the first one as soon as the client notices that something changed and sends a sync token request
112      * eventually the client receives a false sync token (as we increased content sequence, but we dont have a content history entry)
113      * eventually not (if something really changed in the calendar in the meantime)
114      *
115      * in case the client got a fake sync token, the clients next sync token request (once something really changed) will fail again
116      * after something really changed valid sync tokens will be handed out again
117      *
118      * @param Zend_Console_Getopt $_opts
119      */
120     public function forceSyncTokenResync($_opts)
121     {
122         $args = $this->_parseArgs($_opts, array());
123         $container = Tinebase_Container::getInstance();
124
125         if (isset($args['userIds'])) {
126             $args['userIds'] = !is_array($args['userIds']) ? array($args['userIds']) : $args['userIds'];
127             $args['containerIds'] = $container->search(new Tinebase_Model_ContainerFilter(array(
128                 array('field' => 'owner_id', 'operator' => 'in', 'value' => $args['userIds'])
129             )))->getId();
130         }
131
132         if (isset($args['containerIds'])) {
133             $resultStr = '';
134
135             if (!is_array($args['containerIds'])) {
136                 $args['containerIds'] = array($args['containerIds']);
137             }
138
139             $db = Tinebase_Core::getDb();
140
141
142             $contentBackend = $container->getContentBackend();
143             foreach($args['containerIds'] as $id) {
144                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
145
146                 $containerData = $container->get($id);
147                 $recordsBackend = Tinebase_Core::getApplicationInstance($containerData->model)->getBackend();
148                 if (method_exists($recordsBackend, 'increaseSeqsForContainerId')) {
149                     $recordsBackend->increaseSeqsForContainerId($id);
150
151                     $container->increaseContentSequence($id);
152                     $resultStr .= ($resultStr !== '' ? ', ' : '') . $id . '(' . $contentBackend->deleteByProperty($id,
153                             'container_id') . ')';
154                 }
155                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
156             }
157
158             echo "\nDeleted containers(num content history records): " . $resultStr . "\n";
159         }
160     }
161
162     /**
163      * clean timemachine_modlog for records that have been pruned (not deleted!)
164      */
165     public function cleanModlog()
166     {
167         if (! $this->_checkAdminRight()) {
168             return FALSE;
169         }
170
171         $deleted = Tinebase_Timemachine_ModificationLog::getInstance()->clean();
172
173         echo "\ndeleted $deleted modlogs records\n";
174     }
175
176     /**
177      * clean relations, set relation to deleted if at least one of the ends has been set to deleted or pruned
178      */
179     public function cleanRelations()
180     {
181         if (! $this->_checkAdminRight()) {
182             return FALSE;
183         }
184
185         $relations = Tinebase_Relations::getInstance();
186         $filter = new Tinebase_Model_Filter_FilterGroup();
187         $pagination = new Tinebase_Model_Pagination();
188         $pagination->limit = 10000;
189         $pagination->sort = 'id';
190
191         $totalCount = 0;
192         $date = Tinebase_DateTime::now()->subYear(1);
193
194         while ( ($recordSet = $relations->search($filter, $pagination)) && $recordSet->count() > 0 ) {
195             $filter = new Tinebase_Model_Filter_FilterGroup();
196             $pagination->start += $pagination->limit;
197             $models = array();
198
199             foreach($recordSet as $relation) {
200                 $models[$relation->own_model][$relation->own_id][] = $relation->id;
201                 $models[$relation->related_model][$relation->related_id][] = $relation->id;
202             }
203             foreach ($models as $model => &$ids) {
204                 $doAll = false;
205
206                 try {
207                     $app = Tinebase_Core::getApplicationInstance($model, '', true);
208                 } catch (Tinebase_Exception_NotFound $tenf) {
209                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
210                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' no application found for it');
211                     $doAll = true;
212                 }
213                 if (!$doAll) {
214                     if ($app instanceof Tinebase_Container)
215                     {
216                         $backend = $app;
217                     } else {
218                         if (!$app instanceof Tinebase_Controller_Record_Abstract) {
219                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
220                                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' controller: ' . get_class($app) . ' not an instance of Tinebase_Controller_Record_Abstract');
221                             continue;
222                         }
223
224                         $backend = $app->getBackend();
225                     }
226                     if (!$backend instanceof Tinebase_Backend_Interface) {
227                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
228                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' backend: ' . get_class($backend) . ' not an instance of Tinebase_Backend_Interface');
229                         continue;
230                     }
231                     $record = new $model(null, true);
232
233                     $modelFilter = $model . 'Filter';
234                     $idFilter = new $modelFilter(array(), '', array('ignoreAcl' => true));
235                     $idFilter->addFilter(new Tinebase_Model_Filter_Id(array(
236                         'field' => $record->getIdProperty(), 'operator' => 'in', 'value' => array_keys($ids)
237                     )));
238
239
240                     $existingIds = $backend->search($idFilter, null, true);
241
242                     if (!is_array($existingIds)) {
243                         throw new Exception('search for model: ' . $model . ' returned not an array!');
244                     }
245                     foreach ($existingIds as $id) {
246                         unset($ids[$id]);
247                     }
248                 }
249
250                 if ( count($ids) > 0 ) {
251                     $toDelete = array();
252                     foreach ($ids as $idArrays) {
253                         foreach ($idArrays as $id) {
254                             $toDelete[$id] = true;
255                         }
256                     }
257
258                     $toDelete = array_keys($toDelete);
259
260                     foreach($toDelete as $id) {
261                         if ( $recordSet->getById($id)->creation_time && $recordSet->getById($id)->creation_time->isLater($date) ) {
262                             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' relation is about to get deleted that is younger than 1 year: ' . print_r($recordSet->getById($id)->toArray(false), true));
263                         }
264                     }
265
266                     $relations->delete($toDelete);
267                     $totalCount += count($toDelete);
268                 }
269             }
270         }
271
272         $message = 'Deleted ' . $totalCount . ' relations in total';
273         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
274             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $message);
275         echo $message . "\n";
276     }
277
278     /**
279      * authentication
280      *
281      * @param string $_username
282      * @param string $_password
283      */
284     public function authenticate($_username, $_password)
285     {
286         $authResult = Tinebase_Auth::getInstance()->authenticate($_username, $_password);
287         
288         if ($authResult->isValid()) {
289             $accountsController = Tinebase_User::getInstance();
290             try {
291                 $account = $accountsController->getFullUserByLoginName($authResult->getIdentity());
292             } catch (Tinebase_Exception_NotFound $e) {
293                 echo 'account ' . $authResult->getIdentity() . ' not found in account storage'."\n";
294                 exit();
295             }
296             
297             Tinebase_Core::set('currentAccount', $account);
298
299             $ipAddress = '127.0.0.1';
300             $account->setLoginTime($ipAddress);
301
302             Tinebase_AccessLog::getInstance()->create(new Tinebase_Model_AccessLog(array(
303                 'sessionid'     => 'cli call',
304                 'login_name'    => $authResult->getIdentity(),
305                 'ip'            => $ipAddress,
306                 'li'            => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
307                 'lo'            => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
308                 'result'        => $authResult->getCode(),
309                 'account_id'    => Tinebase_Core::getUser()->getId(),
310                 'clienttype'    => 'TineCli',
311             )));
312             
313         } else {
314             echo "Wrong username and/or password.\n";
315             exit();
316         }
317     }
318     
319     /**
320      * handle request (call -ApplicationName-_Cli.-MethodName- or -ApplicationName-_Cli.getHelp)
321      *
322      * @param Zend_Console_Getopt $_opts
323      * @return boolean success
324      */
325     public function handle($_opts)
326     {
327         list($application, $method) = explode('.', $_opts->method);
328         $class = $application . '_Frontend_Cli';
329         
330         if (@class_exists($class)) {
331             $object = new $class;
332             if ($_opts->info) {
333                 $result = $object->getHelp();
334             } else if (method_exists($object, $method)) {
335                 $result = call_user_func(array($object, $method), $_opts);
336             } else {
337                 $result = FALSE;
338                 echo "Method $method not found.\n";
339             }
340         } else {
341             echo "Class $class does not exist.\n";
342             $result = FALSE;
343         }
344         
345         return $result;
346     }
347
348     /**
349      * trigger async events (for example via cronjob)
350      *
351      * @param Zend_Console_Getopt $_opts
352      * @return boolean success
353      */
354     public function triggerAsyncEvents($_opts)
355     {
356         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
357             . ' Triggering async events from CLI.');
358
359         $freeLock = $this->_aquireMultiServerLock(__CLASS__ . '::' . __FUNCTION__);
360         if (! $freeLock) {
361             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
362                 .' Job already running.');
363             return false;
364         }
365         
366         $userController = Tinebase_User::getInstance();
367
368         // deactivate user plugins (like postfix/dovecot email backends) for async job user
369         $userController->unregisterAllPlugins();
370
371         try {
372             $cronuser = $userController->getFullUserByLoginName($_opts->username);
373         } catch (Tinebase_Exception_NotFound $tenf) {
374             $cronuser = $this->_getCronuserFromConfigOrCreateOnTheFly();
375         }
376         Tinebase_Core::set(Tinebase_Core::USER, $cronuser);
377         
378         $scheduler = Tinebase_Core::getScheduler();
379         $responses = $scheduler->run();
380         
381         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' ' . print_r(array_keys($responses), TRUE));
382         
383         $responseString = ($responses) ? implode(',', array_keys($responses)) : 'NULL';
384         echo "Tine 2.0 scheduler run (" . $responseString . ") complete.\n";
385         
386         return true;
387     }
388
389     /**
390      * process given queue job
391      *  --message json encoded task
392      *
393      * @TODO rework user management, jobs should be executed as the right user in future
394      * 
395      * @param Zend_Console_Getopt $_opts
396      * @return boolean success
397      */
398     public function executeQueueJob($_opts)
399     {
400         try {
401             $cronuser = Tinebase_User::getInstance()->getFullUserByLoginName($_opts->username);
402         } catch (Tinebase_Exception_NotFound $tenf) {
403             $cronuser = $this->_getCronuserFromConfigOrCreateOnTheFly();
404         }
405         
406         Tinebase_Core::set(Tinebase_Core::USER, $cronuser);
407         
408         $args = $_opts->getRemainingArgs();
409         $message = preg_replace('/^message=/', '', $args[0]);
410         
411         if (! $message) {
412             throw new Tinebase_Exception_InvalidArgument('mandatory parameter "message" is missing');
413         }
414         
415         Tinebase_ActionQueue::getInstance()->executeAction($message);
416         
417         return TRUE;
418     }
419     
420     /**
421      * clear table as defined in arguments
422      * can clear the following tables:
423      * - credential_cache
424      * - access_log
425      * - async_job
426      * - temp_files
427      * 
428      * if param date is given (date=2010-09-17), all records before this date are deleted (if the table has a date field)
429      * 
430      * @param $_opts
431      * @return boolean success
432      */
433     public function clearTable(Zend_Console_Getopt $_opts)
434     {
435         if (! $this->_checkAdminRight()) {
436             return FALSE;
437         }
438         
439         $args = $this->_parseArgs($_opts, array('tables'), 'tables');
440         $dateString = (isset($args['date']) || array_key_exists('date', $args)) ? $args['date'] : NULL;
441
442         $db = Tinebase_Core::getDb();
443         foreach ($args['tables'] as $table) {
444             switch ($table) {
445                 case 'access_log':
446                     $date = ($dateString) ? new Tinebase_DateTime($dateString) : NULL;
447                     Tinebase_AccessLog::getInstance()->clearTable($date);
448                     break;
449                 case 'async_job':
450                     $where = ($dateString) ? array(
451                         $db->quoteInto($db->quoteIdentifier('end_time') . ' < ?', $dateString)
452                     ) : array();
453                     $where[] = $db->quoteInto($db->quoteIdentifier('status') . ' < ?', 'success');
454                     
455                     echo "\nRemoving all successful async_job entries " . ($dateString ? "before $dateString " : "") . "...";
456                     $deleteCount = $db->delete(SQL_TABLE_PREFIX . $table, $where);
457                     echo "\nRemoved $deleteCount records.";
458                     break;
459                 case 'credential_cache':
460                     Tinebase_Auth_CredentialCache::getInstance()->clearCacheTable($dateString);
461                     break;
462                 case 'temp_files':
463                     Tinebase_TempFile::getInstance()->clearTableAndTempdir($dateString);
464                     break;
465                 default:
466                     echo 'Table ' . $table . " not supported or argument missing.\n";
467             }
468             echo "\nCleared table $table.";
469         }
470         echo "\n\n";
471         
472         return TRUE;
473     }
474     
475     /**
476      * purge deleted records
477      * 
478      * if param date is given (for example: date=2010-09-17), all records before this date are deleted (if the table has a date field)
479      * if table names are given, purge only records from this tables
480      * 
481      * @param $_opts
482      * @return boolean success
483      *
484      * TODO move purge logic to applications, purge Tinebase tables at the end
485      */
486     public function purgeDeletedRecords(Zend_Console_Getopt $_opts)
487     {
488         if (! $this->_checkAdminRight()) {
489             return FALSE;
490         }
491
492         $args = $this->_parseArgs($_opts, array(), 'tables');
493         $doEverything = false;
494
495         if (! (isset($args['tables']) || array_key_exists('tables', $args)) || empty($args['tables'])) {
496             echo "No tables given.\nPurging records from all tables!\n";
497             $args['tables'] = $this->_getAllApplicationTables();
498             $doEverything = true;
499         }
500         
501         $db = Tinebase_Core::getDb();
502         
503         if ((isset($args['date']) || array_key_exists('date', $args))) {
504             echo "\nRemoving all deleted entries before {$args['date']} ...";
505             $where = array(
506                 $db->quoteInto($db->quoteIdentifier('deleted_time') . ' < ?', $args['date'])
507             );
508         } else {
509             echo "\nRemoving all deleted entries ...";
510             $where = array();
511         }
512         $where[] = $db->quoteInto($db->quoteIdentifier('is_deleted') . ' = ?', 1);
513
514         $orderedTables = $this->_orderTables($args['tables']);
515         $this->_purgeTables($orderedTables, $where);
516
517         if ($doEverything) {
518             echo "\nCleaning relations...";
519             $this->cleanRelations();
520
521             echo "\nCleaning modlog...";
522             $this->cleanModlog();
523
524             echo "\nCleaning customfields...";
525             $this->cleanCustomfields();
526
527             echo "\nCleaning notes...";
528             $this->cleanNotes();
529         }
530
531         echo "\n\n";
532         
533         return TRUE;
534     }
535
536     /**
537      * cleanNotes: removes notes of records that have been deleted
538      */
539     public function cleanNotes()
540     {
541         if (! $this->_checkAdminRight()) {
542             return FALSE;
543         }
544
545         $notesController = Tinebase_Notes::getInstance();
546         $notes = $notesController->getAllNotes();
547         $controllers = array();
548         $models = array();
549         $deleteIds = array();
550
551         /** @var Tinebase_Model_Note $note */
552         foreach ($notes as $note) {
553             if (!isset($controllers[$note->record_model])) {
554                 if (strpos($note->record_model, 'Tinebase') === 0) {
555                     continue;
556                 }
557                 try {
558                     $controllers[$note->record_model] = Tinebase_Core::getApplicationInstance($note->record_model);
559                 } catch(Tinebase_Exception_AccessDenied $e) {
560                     // TODO log
561                     continue;
562                 } catch(Tinebase_Exception_NotFound $tenf) {
563                     $deleteIds[] = $note->getId();
564                     continue;
565                 }
566                 $oldACLCheckValue = $controllers[$note->record_model]->doContainerACLChecks(false);
567                 $models[$note->record_model] = array(
568                     0 => new $note->record_model(),
569                     1 => ($note->record_model !== 'Filemanager_Model_Node' ? class_exists($note->record_model . 'Filter') : false),
570                     2 => $note->record_model . 'Filter',
571                     3 => $oldACLCheckValue
572                 );
573             }
574             $controller = $controllers[$note->record_model];
575             $model = $models[$note->record_model];
576
577             if ($model[1]) {
578                 $filter = new $model[2](array(
579                     array('field' => $model[0]->getIdProperty(), 'operator' => 'equals', 'value' => $note->record_id)
580                 ));
581                 if ($model[0]->has('is_deleted')) {
582                     $filter->addFilter(new Tinebase_Model_Filter_Int(array('field' => 'is_deleted', 'operator' => 'notnull', 'value' => NULL)));
583                 }
584                 $result = $controller->searchCount($filter);
585
586                 if (is_bool($result) || (is_string($result) && $result === ((string)intval($result)))) {
587                     $result = (int)$result;
588                 }
589
590                 if (!is_int($result)) {
591                     if (is_array($result) && isset($result['totalcount'])) {
592                         $result = (int)$result['totalcount'];
593                     } elseif(is_array($result) && isset($result['count'])) {
594                         $result = (int)$result['count'];
595                     } else {
596                         // todo log
597                         // dummy line, remove!
598                         $result = 1;
599                     }
600                 }
601
602                 if ($result === 0) {
603                     $deleteIds[] = $note->getId();
604                 }
605             } else {
606                 try {
607                     $controller->get($note->record_id, null, false, true);
608                 } catch(Tinebase_Exception_NotFound $tenf) {
609                     $deleteIds[] = $note->getId();
610                 }
611             }
612         }
613
614         if (count($deleteIds) > 0) {
615             $notesController->purgeNotes($deleteIds);
616         }
617
618         foreach($controllers as $model => $controller) {
619             $controller->doContainerACLChecks($models[$model][3]);
620         }
621
622         echo "\ndeleted " . count($deleteIds) . " notes\n";
623     }
624
625     /**
626      * cleanCustomfields
627      */
628     public function cleanCustomfields()
629     {
630         if (! $this->_checkAdminRight()) {
631             return FALSE;
632         }
633
634         $customFieldController = Tinebase_CustomField::getInstance();
635         $customFieldConfigs = $customFieldController->searchConfig();
636         $deleteCount = 0;
637
638         /** @var Tinebase_Model_CustomField_Config $customFieldConfig */
639         foreach($customFieldConfigs as $customFieldConfig) {
640             $deleteAll = false;
641             try {
642                 $controller = Tinebase_Core::getApplicationInstance($customFieldConfig->model);
643
644                 $oldACLCheckValue = $controller->doContainerACLChecks(false);
645                 if ($customFieldConfig->model !== 'Filemanager_Model_Node') {
646                     $filterClass = $customFieldConfig->model . 'Filter';
647                 } else {
648                     $filterClass = 'ClassThatDoesNotExist';
649                 }
650             } catch(Tinebase_Exception_AccessDenied $e) {
651                 // TODO log
652                 continue;
653             } catch(Tinebase_Exception_NotFound $tenf) {
654                 $deleteAll = true;
655             }
656
657
658
659             $filter = new Tinebase_Model_CustomField_ValueFilter(array(
660                 array('field' => 'customfield_id', 'operator' => 'equals', 'value' => $customFieldConfig->id)
661             ));
662             $customFieldValues = $customFieldController->search($filter);
663             $deleteIds = array();
664
665             if (true === $deleteAll) {
666                 $deleteIds = $customFieldValues->getId();
667             } elseif (class_exists($filterClass)) {
668                 $model = new $customFieldConfig->model();
669                 /** @var Tinebase_Model_CustomField_Value $customFieldValue */
670                 foreach ($customFieldValues as $customFieldValue) {
671                     $filter = new $filterClass(array(
672                         array('field' => $model->getIdProperty(), 'operator' => 'equals', 'value' => $customFieldValue->record_id)
673                     ));
674                     if ($model->has('is_deleted')) {
675                         $filter->addFilter(new Tinebase_Model_Filter_Int(array('field' => 'is_deleted', 'operator' => 'notnull', 'value' => NULL)));
676                     }
677
678                     $result = $controller->searchCount($filter);
679
680                     if (is_bool($result) || (is_string($result) && $result === ((string)intval($result)))) {
681                         $result = (int)$result;
682                     }
683
684                     if (!is_int($result)) {
685                         if (is_array($result) && isset($result['totalcount'])) {
686                             $result = (int)$result['totalcount'];
687                         } elseif(is_array($result) && isset($result['count'])) {
688                             $result = (int)$result['count'];
689                         } else {
690                             // todo log
691                             // dummy line, remove!
692                             $result = 1;
693                         }
694                     }
695
696                     if ($result === 0) {
697                         $deleteIds[] = $customFieldValue->getId();
698                     }
699                 }
700             } else {
701                 /** @var Tinebase_Model_CustomField_Value $customFieldValue */
702                 foreach ($customFieldValues as $customFieldValue) {
703                     try {
704                         $controller->get($customFieldValue->record_id, null, false, true);
705                     } catch(Tinebase_Exception_NotFound $tenf) {
706                         $deleteIds[] = $customFieldValue->getId();
707                     }
708                 }
709             }
710
711             if (count($deleteIds) > 0) {
712                 $customFieldController->deleteCustomFieldValue($deleteIds);
713                 $deleteCount += count($deleteIds);
714             }
715
716             if (true !== $deleteAll) {
717                 $controller->doContainerACLChecks($oldACLCheckValue);
718             }
719         }
720
721         echo "\ndeleted " . $deleteCount . " customfield values\n";
722     }
723     
724     /**
725      * get all app tables
726      * 
727      * @return array
728      */
729     protected function _getAllApplicationTables()
730     {
731         $result = array();
732         
733         $enabledApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED);
734         foreach ($enabledApplications as $application) {
735             $result = array_merge($result, Tinebase_Application::getInstance()->getApplicationTables($application));
736         }
737         
738         return $result;
739     }
740
741     /**
742      * order tables for purging deleted records in a defined order
743      *
744      * @param array $tables
745      * @return array
746      *
747      * TODO could be improved by using usort
748      */
749     protected function _orderTables($tables)
750     {
751         // tags should be deleted first
752         // containers should be deleted last
753
754         $orderedTables = array();
755         $lastTables = array();
756         foreach($tables as $table) {
757             switch ($table) {
758                 case 'container':
759                     $lastTables[] = $table;
760                     break;
761                 case 'tags':
762                     array_unshift($orderedTables, $table);
763                     break;
764                 default:
765                     $orderedTables[] = $table;
766             }
767         }
768         $orderedTables = array_merge($orderedTables, $lastTables);
769
770         return $orderedTables;
771     }
772
773     /**
774      * purge tables
775      *
776      * @param $orderedTables
777      * @param $where
778      */
779     protected function _purgeTables($orderedTables, $where)
780     {
781         foreach ($orderedTables as $table) {
782             try {
783                 $schema = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $table);
784             } catch (Zend_Db_Statement_Exception $zdse) {
785                 echo "\nCould not get schema (" . $zdse->getMessage() . "). Skipping table $table";
786                 continue;
787             }
788             if (!(isset($schema['is_deleted']) || array_key_exists('is_deleted', $schema)) || !(isset($schema['deleted_time']) || array_key_exists('deleted_time', $schema))) {
789                 continue;
790             }
791
792             $deleteCount = 0;
793             try {
794                 $deleteCount = Tinebase_Core::getDb()->delete(SQL_TABLE_PREFIX . $table, $where);
795             } catch (Zend_Db_Statement_Exception $zdse) {
796                 echo "\nFailed to purge deleted records for table $table. " . $zdse->getMessage();
797             }
798             if ($deleteCount > 0) {
799                 echo "\nCleared table $table (deleted $deleteCount records).";
800             }
801             // TODO this should only be echoed with --verbose or written to the logs
802             else {
803                 echo "\nNothing to purge from $table";
804             }
805         }
806     }
807
808     /**
809      * add new customfield config
810      *
811      * needs args like this:
812      * application="Addressbook" name="datefield" label="Date" model="Addressbook_Model_Contact" type="datefield"
813      * @see Tinebase_Model_CustomField_Config for full list
814      *
815      * @param $_opts
816      * @return boolean success
817      */
818     public function addCustomfield(Zend_Console_Getopt $_opts)
819     {
820         if (! $this->_checkAdminRight()) {
821             return FALSE;
822         }
823         
824         // parse args
825         $args = $_opts->getRemainingArgs();
826         $data = array();
827         foreach ($args as $idx => $arg) {
828             list($key, $value) = explode('=', $arg);
829             if ($key == 'application') {
830                 $key = 'application_id';
831                 $value = Tinebase_Application::getInstance()->getApplicationByName($value)->getId();
832             }
833             $data[$key] = $value;
834         }
835         
836         $customfieldConfig = new Tinebase_Model_CustomField_Config($data);
837         $cf = Tinebase_CustomField::getInstance()->addCustomField($customfieldConfig);
838
839         echo "\nCreated customfield: ";
840         print_r($cf->toArray());
841         echo "\n";
842         
843         return TRUE;
844     }
845     
846     /**
847      * nagios monitoring for tine 2.0 database connection
848      * 
849      * @return integer
850      * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
851      */
852     public function monitoringCheckDB()
853     {
854         $message = 'DB CONNECTION FAIL';
855         try {
856             if (! Setup_Core::isRegistered(Setup_Core::CONFIG)) {
857                 Setup_Core::setupConfig();
858             }
859             if (! Setup_Core::isRegistered(Setup_Core::LOGGER)) {
860                 Setup_Core::setupLogger();
861             }
862             $time_start = microtime(true);
863             $dbcheck = Setup_Core::setupDatabaseConnection();
864             $time = (microtime(true) - $time_start) * 1000;
865         } catch (Exception $e) {
866             $message .= ': ' . $e->getMessage();
867             $dbcheck = FALSE;
868         }
869         
870         if ($dbcheck) {
871             echo "DB CONNECTION OK | connecttime={$time}ms;;;;\n";
872             return 0;
873         } 
874         
875         echo $message . "\n";
876         return 2;
877     }
878     
879     /**
880      * nagios monitoring for tine 2.0 config file
881      * 
882      * @return integer
883      * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
884      */
885     public function monitoringCheckConfig()
886     {
887         $message = 'CONFIG FAIL';
888         $configcheck = FALSE;
889         
890         $configfile = Setup_Core::getConfigFilePath();
891         if ($configfile) {
892             $configfile = escapeshellcmd($configfile);
893             if (preg_match('/^win/i', PHP_OS)) {
894                 exec("php -l $configfile 2> NUL", $error, $code);
895             } else {
896                 exec("php -l $configfile 2> /dev/null", $error, $code);
897             }
898             if ($code == 0) {
899                 $configcheck = TRUE;
900             } else {
901                 $message .= ': CONFIG FILE SYNTAX ERROR';
902             }
903         } else {
904             $message .= ': CONFIG FILE MISSING';
905         }
906         
907         if ($configcheck) {
908             echo "CONFIG FILE OK\n";
909             return 0;
910         } else {
911             echo $message . "\n";
912             return 2;
913         }
914     }
915     
916     /**
917     * nagios monitoring for tine 2.0 async cronjob run
918     *
919     * @return integer
920     * 
921     * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
922     * @see 0008038: monitoringCheckCron -> check if cron did run in the last hour
923     */
924     public function monitoringCheckCron()
925     {
926         $message = 'CRON FAIL';
927
928         try {
929             $lastJob = Tinebase_AsyncJob::getInstance()->getLastJob('Tinebase_Event_Async_Minutely');
930             
931             if ($lastJob === NULL) {
932                 $message .= ': NO LAST JOB FOUND';
933                 $result = 1;
934             } else {
935                 if ($lastJob->end_time instanceof Tinebase_DateTime) {
936                     $duration = $lastJob->end_time->getTimestamp() - $lastJob->start_time->getTimestamp();
937                     $valueString = ' | duration=' . $duration . 's;;;;';
938                     $valueString .= ' end=' . $lastJob->end_time->getIso() . ';;;;';
939                 } else {
940                     $valueString = '';
941                 }
942                 
943                 if ($lastJob->status === Tinebase_Model_AsyncJob::STATUS_RUNNING && Tinebase_DateTime::now()->isLater($lastJob->end_time)) {
944                     $message .= ': LAST JOB TOOK TOO LONG';
945                     $result = 1;
946                 } else if ($lastJob->status === Tinebase_Model_AsyncJob::STATUS_FAILURE) {
947                     $message .= ': LAST JOB FAILED';
948                     $result = 1;
949                 } else if (Tinebase_DateTime::now()->isLater($lastJob->start_time->addHour(1))) {
950                     $message .= ': NO JOB IN THE LAST HOUR';
951                     $result = 1;
952                 } else {
953                     $message = 'CRON OK';
954                     $result = 0;
955                 }
956                 $message .= $valueString;
957             }
958         } catch (Exception $e) {
959             $message .= ': ' . $e->getMessage();
960             $result = 2;
961         }
962         
963         echo $message . "\n";
964         return $result;
965     }
966     
967     /**
968      * nagios monitoring for tine 2.0 logins during the last 5 mins
969      * 
970      * @return number
971      * 
972      * @todo allow to configure timeslot
973      */
974     public function monitoringLoginNumber()
975     {
976         $message = 'LOGINS';
977         $result  = 0;
978         
979         try {
980             $filter = new Tinebase_Model_AccessLogFilter(array(
981                 array('field' => 'li', 'operator' => 'after', 'value' => Tinebase_DateTime::now()->subMinute(5))
982             ));
983             $accesslogs = Tinebase_AccessLog::getInstance()->search($filter, NULL, FALSE, TRUE);
984             $valueString = ' | count=' . count($accesslogs) . ';;;;';
985             $message .= ' OK' . $valueString;
986         } catch (Exception $e) {
987             $message .= ' FAIL: ' . $e->getMessage();
988             $result = 2;
989         }
990         
991         echo $message . "\n";
992         return $result;
993     }
994
995     /**
996      * nagios monitoring for tine 2.0 active users
997      *
998      * @return number
999      *
1000      * @todo allow to configure timeslot / currently the active users of the last month are returned
1001      */
1002     public function monitoringActiveUsers()
1003     {
1004         $message = 'ACTIVE USERS';
1005         $result  = 0;
1006
1007         try {
1008             $userCount = Tinebase_User::getInstance()->getActiveUserCount();
1009             $valueString = ' | count=' . $userCount . ';;;;';
1010             $message .= ' OK' . $valueString;
1011         } catch (Exception $e) {
1012             $message .= ' FAIL: ' . $e->getMessage();
1013             $result = 2;
1014         }
1015
1016         echo $message . "\n";
1017         return $result;
1018     }
1019
1020     /**
1021      * undo changes to records defined by certain criteria (user, date, fields, ...)
1022      * 
1023      * example: $ php tine20.php --username pschuele --method Tinebase.undo -d 
1024      *   -- record_type=Addressbook_Model_Contact modification_time=2013-05-08 modification_account=3263
1025      * 
1026      * @param Zend_Console_Getopt $opts
1027      */
1028     public function undo(Zend_Console_Getopt $opts)
1029     {
1030         if (! $this->_checkAdminRight()) {
1031             return FALSE;
1032         }
1033         
1034         $data = $this->_parseArgs($opts, array('modification_time'));
1035         
1036         // build filter from params
1037         $filterData = array();
1038         $allowedFilters = array(
1039             'record_type',
1040             'modification_time',
1041             'modification_account',
1042             'record_id',
1043             'modified_attribute'
1044         );
1045         foreach ($data as $key => $value) {
1046             if (in_array($key, $allowedFilters)) {
1047                 $operator = ($key === 'modification_time') ? 'within' : 'equals';
1048                 $filterData[] = array('field' => $key, 'operator' => $operator, 'value' => $value);
1049             }
1050         }
1051         $filter = new Tinebase_Model_ModificationLogFilter($filterData);
1052         
1053         $dryrun = $opts->d;
1054         $overwrite = (isset($data['overwrite']) && $data['overwrite']) ? TRUE : FALSE;
1055         $result = Tinebase_Timemachine_ModificationLog::getInstance()->undo($filter, $overwrite, $dryrun);
1056         
1057         if (! $dryrun) {
1058             echo 'Reverted ' . $result['totalcount'] . " change(s)\n";
1059         } else {
1060             echo "Dry run\n";
1061             echo 'Would revert ' . $result['totalcount'] . " change(s):\n";
1062             foreach ($result['undoneModlogs'] as $modlog) {
1063                 echo 'id ' . $modlog->record_id . ' [' . $modlog->modified_attribute . ']: ' . $modlog->new_value . ' -> ' . $modlog->old_value . "\n";
1064             }
1065         }
1066         echo 'Failcount: ' . $result['failcount'] . "\n";
1067         return 0;
1068     }
1069     
1070     /**
1071      * creates demo data for all applications
1072      * accepts same arguments as Tinebase_Frontend_Cli_Abstract::createDemoData
1073      * and the additional argument "skipAdmin" to force no user/group/role creation
1074      * 
1075      * @param Zend_Console_Getopt $_opts
1076      */
1077     public function createAllDemoData($_opts)
1078     {
1079         if (! $this->_checkAdminRight()) {
1080             return FALSE;
1081         }
1082         
1083         // fetch all applications and check if required are installed, otherwise remove app from array
1084         $applications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
1085         foreach($applications as $appName) {
1086             echo 'Searching for DemoData in application "' . $appName . '"...' . PHP_EOL;
1087             $className = $appName.'_Setup_DemoData';
1088             if (class_exists($className)) {
1089                 echo 'DemoData in application "' . $appName . '" found!' . PHP_EOL;
1090                 $required = $className::getRequiredApplications();
1091                 foreach($required as $requiredApplication) {
1092                     if (! Tinebase_Helper::in_array_case($applications, $requiredApplication)) {
1093                         echo 'Creating DemoData for Application ' . $appName . ' is impossible, because application "' . $requiredApplication . '" is not installed.' . PHP_EOL;
1094                         continue 2;
1095                     }
1096                 }
1097                 $this->_applicationsToWorkOn[$appName] = array('appName' => $appName, 'required' => $required);
1098             } else {
1099                 echo 'DemoData in application "' . $appName . '" not found.' . PHP_EOL . PHP_EOL;
1100             }
1101         }
1102         unset($applications);
1103         
1104         foreach($this->_applicationsToWorkOn as $app => $cfg) {
1105             $this->_createDemoDataRecursive($app, $cfg, $_opts);
1106         }
1107
1108         return 0;
1109     }
1110     
1111     /**
1112      * creates demo data and calls itself if there are required apps
1113      * 
1114      * @param string $app
1115      * @param array $cfg
1116      * @param Zend_Console_Getopt $opts
1117      */
1118     protected function _createDemoDataRecursive($app, $cfg, $opts)
1119     {
1120         if (isset($cfg['required']) && is_array($cfg['required'])) {
1121             foreach($cfg['required'] as $requiredApp) {
1122                 $this->_createDemoDataRecursive($requiredApp, $this->_applicationsToWorkOn[$requiredApp], $opts);
1123             }
1124         }
1125         
1126         $className = $app . '_Frontend_Cli';
1127         
1128         $classNameDD = $app . '_Setup_DemoData';
1129         
1130         if (class_exists($className)) {
1131             if (! $classNameDD::hasBeenRun()) {
1132                 echo 'Creating DemoData in application "' . $app . '"...' . PHP_EOL;
1133                 $class = new $className();
1134                 $class->createDemoData($opts, FALSE);
1135             } else {
1136                 echo 'DemoData for ' . $app . ' has been run already, skipping...' . PHP_EOL;
1137             }
1138         } else {
1139             echo 'Could not found ' . $className . ', so DemoData for application "' . $app . '" could not be created!';
1140         }
1141     }
1142     
1143     /**
1144      * clears deleted files from filesystem + database
1145      * @return boolean
1146      */
1147     public function clearDeletedFiles()
1148     {
1149         if (! $this->_checkAdminRight()) {
1150             return FALSE;
1151         }
1152         
1153         $this->_addOutputLogWriter();
1154         
1155         Tinebase_FileSystem::getInstance()->clearDeletedFiles();
1156
1157         return 0;
1158     }
1159
1160     /**
1161      * repair a table
1162      * 
1163      * @param Zend_Console_Getopt $opts
1164      * 
1165      * @todo add more tables
1166      */
1167     public function repairTable($opts)
1168     {
1169         if (! $this->_checkAdminRight()) {
1170             return FALSE;
1171         }
1172         
1173         $this->_addOutputLogWriter();
1174         
1175         $data = $this->_parseArgs($opts, array('table'));
1176         
1177         switch ($data['table']) {
1178             case 'importexport_definition':
1179                 Tinebase_ImportExportDefinition::getInstance()->repairTable();
1180                 $result = 0;
1181                 break;
1182             default:
1183                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1184                     . ' No repair script found for ' . $data['table']);
1185                 $result = 1;
1186         }
1187         
1188         exit($result);
1189     }
1190     
1191     /**
1192      * repairs container names
1193      * 
1194      * @param Zend_Console_Getopt $opts
1195      */
1196     public function repairContainerName($opts)
1197     {
1198         if (! $this->_checkAdminRight()) {
1199             return FALSE;
1200         }
1201         $dryrun = $opts->d;
1202         
1203         $this->_addOutputLogWriter();
1204         $args = $this->_parseArgs($opts);
1205         
1206         $containersWithBadNames = Tinebase_Container::getInstance()->getContainersWithBadNames();
1207         
1208         $locale = Tinebase_Translation::getLocale((isset($args['locale']) ?$args['locale'] : 'auto'));
1209
1210         if ($dryrun) {
1211             print_r($containersWithBadNames->toArray());
1212             echo "Using Locale " . $locale . "\n";
1213         }
1214         
1215         $appContainerNames = array(
1216             'Calendar' => 'calendar',
1217             'Tasks'    => 'tasks',
1218             'Addressbook'    => 'addressbook',
1219         );
1220         
1221         foreach ($containersWithBadNames as $container) {
1222             if (empty($container->owner_id)) {
1223                 if ($dryrun) {
1224                     echo "Don't rename shared container " . $container->id . "\n";
1225                 }
1226                 continue;
1227             }
1228             $app = Tinebase_Application::getInstance()->getApplicationById($container->application_id);
1229             $appContainerName = isset($appContainerNames[$app->name]) ? $appContainerNames[$app->name] : "container";
1230             $translation = Tinebase_Translation::getTranslation($app->name, $locale);
1231             $account = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $container->owner_id);
1232             $newName = $newBaseName = sprintf($translation->_("%s's personal " . $appContainerName), $account->accountFullName);
1233             
1234             $count = 1;
1235             do {
1236                 try {
1237                     Tinebase_Container::getInstance()->getContainerByName($app->name, $newName, Tinebase_Model_Container::TYPE_PERSONAL, $container->owner_id);
1238                     $found = true;
1239                     $newName = $newBaseName . ' ' . ++$count;
1240                 } catch (Tinebase_Exception_NotFound $tenf) {
1241                     $found = false;
1242                 }
1243                 
1244             } while ($found);
1245             if ($dryrun) {
1246                 echo "Rename container id " . $container->id . ' to ' . $newName . "\n";
1247             } else {
1248                 
1249                 $container->name = $newName;
1250                 Tinebase_Container::getInstance()->update($container);
1251             }
1252         }
1253         
1254         $result = 0;
1255         exit($result);
1256     }
1257     
1258     /**
1259      * transfer relations
1260      * 
1261      * @param Zend_Console_Getopt $opts
1262      */
1263     public function transferRelations($opts)
1264     {
1265         if (! $this->_checkAdminRight()) {
1266             return FALSE;
1267         }
1268         
1269         $this->_addOutputLogWriter();
1270         
1271         try {
1272             $args = $this->_parseArgs($opts, array('oldId', 'newId', 'model'));
1273         } catch (Tinebase_Exception_InvalidArgument $e) {
1274             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1275                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Parameters "oldId", "newId" and "model" are required!');
1276             }
1277             exit(1);
1278         }
1279         
1280         $skippedEntries = Tinebase_Relations::getInstance()->transferRelations($args['oldId'], $args['newId'], $args['model']);
1281
1282         if (! empty($skippedEntries) && Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1283             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . count($skippedEntries) . ' entries has been skipped:');
1284         }
1285         
1286         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1287             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' The operation has been terminated successfully.');
1288         }
1289
1290         return 0;
1291     }
1292
1293     /**
1294      * repair function for persistent filters (favorites) without grants: this adds default grants for those filters.
1295      *
1296      * @return int
1297      */
1298     public function setDefaultGrantsOfPersistentFilters()
1299     {
1300         if (! $this->_checkAdminRight()) {
1301             return -1;
1302         }
1303
1304         $this->_addOutputLogWriter(6);
1305
1306         // get all persistent filters without grants
1307         // TODO this could be enhanced by allowing to set default grants for other filters, too
1308         Tinebase_PersistentFilter::getInstance()->doContainerACLChecks(false);
1309         $filters = Tinebase_PersistentFilter::getInstance()->search(new Tinebase_Model_PersistentFilterFilter(array(),'', array('ignoreAcl' => true)));
1310         $filtersWithoutGrants = 0;
1311
1312         foreach ($filters as $filter) {
1313             if (count($filter->grants) == 0) {
1314                 // update to set default grants
1315                 $filter = Tinebase_PersistentFilter::getInstance()->update($filter);
1316                 $filtersWithoutGrants++;
1317
1318                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1319                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1320                         . ' Updated filter: ' . print_r($filter->toArray(), true));
1321                 }
1322             }
1323         }
1324
1325         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1326             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1327                 . ' Set default grants for ' . $filtersWithoutGrants . ' filters'
1328                 . ' (checked ' . count($filters) . ' in total).');
1329         }
1330
1331         return 0;
1332     }
1333
1334     /**
1335      *
1336      *
1337      * @return int
1338      */
1339     public function repairContainerOwner()
1340     {
1341         if (! $this->_checkAdminRight()) {
1342             return 2;
1343         }
1344
1345         $this->_addOutputLogWriter(6);
1346         Tinebase_Container::getInstance()->setContainerOwners();
1347
1348         return 0;
1349     }
1350
1351     /**
1352      * show user report (number of enabled, disabled, ... users)
1353      *
1354      * TODO add system user count
1355      * TODO use twig?
1356      */
1357     public function userReport()
1358     {
1359         if (! $this->_checkAdminRight()) {
1360             return 2;
1361         }
1362
1363         $translation = Tinebase_Translation::getTranslation('Tinebase');
1364
1365         $userStatus = array(
1366             'total' => array(),
1367             Tinebase_Model_User::ACCOUNT_STATUS_ENABLED => array(/* 'showUserNames' => true, 'showClients' => true */),
1368             Tinebase_Model_User::ACCOUNT_STATUS_DISABLED => array(),
1369             Tinebase_Model_User::ACCOUNT_STATUS_BLOCKED => array(),
1370             Tinebase_Model_User::ACCOUNT_STATUS_EXPIRED => array(),
1371             //'system' => array(),
1372             'lastmonth' => array('lastMonths' => 1, 'showUserNames' => true, 'showClients' => true),
1373             'last 3 months' => array('lastMonths' => 3),
1374         );
1375
1376         foreach ($userStatus as $status => $options) {
1377             switch ($status) {
1378                 case 'lastmonth':
1379                 case 'last 3 months':
1380                     $userCount = Tinebase_User::getInstance()->getActiveUserCount($options['lastMonths']);
1381                     $text = $translation->_("Number of distinct users") . " (" . $status . "): " . $userCount . "\n";
1382                     break;
1383                 case 'system':
1384                     $text = "TODO add me\n";
1385                     break;
1386                 default:
1387                     $userCount = Tinebase_User::getInstance()->getUserCount($status);
1388                     $text = $translation->_("Number of users") . " (" . $status . "): " . $userCount . "\n";
1389             }
1390             echo $text;
1391
1392             if (isset($options['showUserNames']) && $options['showUserNames']
1393                 && in_array($status, array('lastmonth', 'last 3 months'))
1394                 && isset($options['lastMonths'])
1395             ) {
1396                 // TODO allow this for other status
1397                 echo $translation->_("  User Accounts:\n");
1398                 $userIds = Tinebase_User::getInstance()->getActiveUserIds($options['lastMonths']);
1399                 foreach ($userIds as $userId) {
1400                     $user = Tinebase_User::getInstance()->getUserByProperty('accountId', $userId, 'Tinebase_Model_FullUser');
1401                     echo "  * " . $user->accountLoginName . ' / ' . $user->accountDisplayName . "\n";
1402                     if (isset($options['showClients']) && $options['showClients']) {
1403                         $userClients = Tinebase_AccessLog::getInstance()->getUserClients($user, $options['lastMonths']);
1404                         echo "    Clients: \n";
1405                         foreach ($userClients as $client) {
1406                             echo "     - $client\n";
1407                         }
1408                         echo "\n";
1409                     }
1410                 }
1411             }
1412             echo "\n";
1413         }
1414
1415         return 0;
1416     }
1417 }