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