Merge branch '2016.11-develop' into 2017.02
[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-2017 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 increaseReplicationMasterId($opts)
40     {
41         if (!$this->_checkAdminRight()) {
42             return -1;
43         }
44
45         Tinebase_Timemachine_ModificationLog::getInstance()->increaseReplicationMasterId();
46
47         return true;
48     }
49
50     /**
51      * @param Zend_Console_Getopt $_opts
52      * @return boolean success
53      */
54     public function readModifictionLogFromMaster($opts)
55     {
56         if (!$this->_checkAdminRight()) {
57             return -1;
58         }
59
60         Tinebase_Timemachine_ModificationLog::getInstance()->readModificationLogFromMaster();
61
62         return true;
63     }
64
65     /**
66      * rebuildPaths
67      *
68     * @param Zend_Console_Getopt $_opts
69     * @return integer success
70     */
71     public function rebuildPaths($opts)
72     {
73         if (! $this->_checkAdminRight()) {
74             return -1;
75         }
76
77         $result = Tinebase_Controller::getInstance()->rebuildPaths();
78
79         return $result ? true : -1;
80     }
81
82     /**
83      * forces containers that support sync token to resync via WebDAV sync tokens
84      *
85      * this will cause 2 BadRequest responses to sync token requests
86      * the first one as soon as the client notices that something changed and sends a sync token request
87      * eventually the client receives a false sync token (as we increased content sequence, but we dont have a content history entry)
88      * eventually not (if something really changed in the calendar in the meantime)
89      *
90      * in case the client got a fake sync token, the clients next sync token request (once something really changed) will fail again
91      * after something really changed valid sync tokens will be handed out again
92      *
93      * @param Zend_Console_Getopt $_opts
94      */
95     public function forceSyncTokenResync($_opts)
96     {
97         $args = $this->_parseArgs($_opts, array());
98         $container = Tinebase_Container::getInstance();
99
100         if (isset($args['userIds'])) {
101             $args['userIds'] = !is_array($args['userIds']) ? array($args['userIds']) : $args['userIds'];
102             $args['containerIds'] = $container->search(new Tinebase_Model_ContainerFilter(array(
103                 array('field' => 'owner_id', 'operator' => 'in', 'value' => $args['userIds'])
104             )))->getId();
105         }
106
107         if (isset($args['containerIds'])) {
108             $resultStr = '';
109
110             if (!is_array($args['containerIds'])) {
111                 $args['containerIds'] = array($args['containerIds']);
112             }
113
114             $db = Tinebase_Core::getDb();
115
116
117             $contentBackend = $container->getContentBackend();
118             foreach($args['containerIds'] as $id) {
119                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
120
121                 $containerData = $container->get($id);
122                 $recordsBackend = Tinebase_Core::getApplicationInstance($containerData->model)->getBackend();
123                 if (method_exists($recordsBackend, 'increaseSeqsForContainerId')) {
124                     $recordsBackend->increaseSeqsForContainerId($id);
125
126                     $container->increaseContentSequence($id);
127                     $resultStr .= ($resultStr !== '' ? ', ' : '') . $id . '(' . $contentBackend->deleteByProperty($id,
128                             'container_id') . ')';
129                 }
130                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
131             }
132
133             echo "\nDeleted containers(num content history records): " . $resultStr . "\n";
134         }
135     }
136
137     /**
138      * clean timemachine_modlog for records that have been pruned (not deleted!)
139      */
140     public function cleanModlog()
141     {
142         if (! $this->_checkAdminRight()) {
143             return FALSE;
144         }
145
146         $deleted = Tinebase_Timemachine_ModificationLog::getInstance()->clean();
147
148         echo "\ndeleted $deleted modlogs records\n";
149     }
150
151     /**
152      * clean relations, set relation to deleted if at least one of the ends has been set to deleted or pruned
153      */
154     public function cleanRelations()
155     {
156         if (! $this->_checkAdminRight()) {
157             return FALSE;
158         }
159
160         $relations = Tinebase_Relations::getInstance();
161         $filter = new Tinebase_Model_Filter_FilterGroup();
162         $pagination = new Tinebase_Model_Pagination();
163         $pagination->limit = 10000;
164         $pagination->sort = 'id';
165
166         $totalCount = 0;
167         $date = Tinebase_DateTime::now()->subYear(1);
168
169         while ( ($recordSet = $relations->search($filter, $pagination)) && $recordSet->count() > 0 ) {
170             $filter = new Tinebase_Model_Filter_FilterGroup();
171             $pagination->start += $pagination->limit;
172             $models = array();
173
174             foreach($recordSet as $relation) {
175                 $models[$relation->own_model][$relation->own_id][] = $relation->id;
176                 $models[$relation->related_model][$relation->related_id][] = $relation->id;
177             }
178             foreach ($models as $model => &$ids) {
179                 $doAll = false;
180
181                 try {
182                     $app = Tinebase_Core::getApplicationInstance($model, '', true);
183                 } catch (Tinebase_Exception_NotFound $tenf) {
184                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
185                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' no application found for it');
186                     $doAll = true;
187                 }
188                 if (!$doAll) {
189                     if ($app instanceof Tinebase_Container)
190                     {
191                         $backend = $app;
192                     } else {
193                         if (!$app instanceof Tinebase_Controller_Record_Abstract) {
194                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
195                                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' controller: ' . get_class($app) . ' not an instance of Tinebase_Controller_Record_Abstract');
196                             continue;
197                         }
198
199                         $backend = $app->getBackend();
200                     }
201                     if (!$backend instanceof Tinebase_Backend_Interface) {
202                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
203                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' backend: ' . get_class($backend) . ' not an instance of Tinebase_Backend_Interface');
204                         continue;
205                     }
206                     $record = new $model(null, true);
207
208                     $modelFilter = $model . 'Filter';
209                     $idFilter = new $modelFilter(array(), '', array('ignoreAcl' => true));
210                     $idFilter->addFilter(new Tinebase_Model_Filter_Id(array(
211                         'field' => $record->getIdProperty(), 'operator' => 'in', 'value' => array_keys($ids)
212                     )));
213
214
215                     $existingIds = $backend->search($idFilter, null, true);
216
217                     if (!is_array($existingIds)) {
218                         throw new Exception('search for model: ' . $model . ' returned not an array!');
219                     }
220                     foreach ($existingIds as $id) {
221                         unset($ids[$id]);
222                     }
223                 }
224
225                 if ( count($ids) > 0 ) {
226                     $toDelete = array();
227                     foreach ($ids as $idArrays) {
228                         foreach ($idArrays as $id) {
229                             $toDelete[$id] = true;
230                         }
231                     }
232
233                     $toDelete = array_keys($toDelete);
234
235                     foreach($toDelete as $id) {
236                         if ( $recordSet->getById($id)->creation_time && $recordSet->getById($id)->creation_time->isLater($date) ) {
237                             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));
238                         }
239                     }
240
241                     $relations->delete($toDelete);
242                     $totalCount += count($toDelete);
243                 }
244             }
245         }
246
247         $message = 'Deleted ' . $totalCount . ' relations in total';
248         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
249             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $message);
250         echo $message . "\n";
251     }
252
253     /**
254      * authentication
255      *
256      * @param string $_username
257      * @param string $_password
258      */
259     public function authenticate($_username, $_password)
260     {
261         $authResult = Tinebase_Auth::getInstance()->authenticate($_username, $_password);
262         
263         if ($authResult->isValid()) {
264             $accountsController = Tinebase_User::getInstance();
265             try {
266                 $account = $accountsController->getFullUserByLoginName($authResult->getIdentity());
267             } catch (Tinebase_Exception_NotFound $e) {
268                 echo 'account ' . $authResult->getIdentity() . ' not found in account storage'."\n";
269                 exit();
270             }
271             
272             Tinebase_Core::set('currentAccount', $account);
273
274             $ipAddress = '127.0.0.1';
275             $account->setLoginTime($ipAddress);
276
277             Tinebase_AccessLog::getInstance()->create(new Tinebase_Model_AccessLog(array(
278                 'sessionid'     => 'cli call',
279                 'login_name'    => $authResult->getIdentity(),
280                 'ip'            => $ipAddress,
281                 'li'            => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
282                 'lo'            => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
283                 'result'        => $authResult->getCode(),
284                 'account_id'    => Tinebase_Core::getUser()->getId(),
285                 'clienttype'    => 'TineCli',
286             )));
287             
288         } else {
289             echo "Wrong username and/or password.\n";
290             exit();
291         }
292     }
293     
294     /**
295      * handle request (call -ApplicationName-_Cli.-MethodName- or -ApplicationName-_Cli.getHelp)
296      *
297      * @param Zend_Console_Getopt $_opts
298      * @return boolean success
299      */
300     public function handle($_opts)
301     {
302         list($application, $method) = explode('.', $_opts->method);
303         $class = $application . '_Frontend_Cli';
304         
305         if (@class_exists($class)) {
306             $object = new $class;
307             if ($_opts->info) {
308                 $result = $object->getHelp();
309             } else if (method_exists($object, $method)) {
310                 $result = call_user_func(array($object, $method), $_opts);
311             } else {
312                 $result = FALSE;
313                 echo "Method $method not found.\n";
314             }
315         } else {
316             echo "Class $class does not exist.\n";
317             $result = FALSE;
318         }
319         
320         return $result;
321     }
322
323     /**
324      * trigger async events (for example via cronjob)
325      *
326      * @param Zend_Console_Getopt $_opts
327      * @return boolean success
328      */
329     public function triggerAsyncEvents($_opts)
330     {
331         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
332             . ' Triggering async events from CLI.');
333
334         $freeLock = $this->_aquireMultiServerLock(__CLASS__ . '::' . __FUNCTION__);
335         if (! $freeLock) {
336             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
337                 .' Job already running.');
338             return false;
339         }
340         
341         $userController = Tinebase_User::getInstance();
342
343         try {
344             $cronuser = $userController->getFullUserByLoginName($_opts->username);
345         } catch (Tinebase_Exception_NotFound $tenf) {
346             $cronuser = $this->_getCronuserFromConfigOrCreateOnTheFly();
347         }
348         Tinebase_Core::set(Tinebase_Core::USER, $cronuser);
349         
350         $scheduler = Tinebase_Core::getScheduler();
351         $responses = $scheduler->run();
352         
353         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' ' . print_r(array_keys($responses), TRUE));
354         
355         $responseString = ($responses) ? implode(',', array_keys($responses)) : 'NULL';
356         echo "Tine 2.0 scheduler run (" . $responseString . ") complete.\n";
357         
358         return true;
359     }
360
361     /**
362      * process given queue job
363      *  --message json encoded task
364      *
365      * @param Zend_Console_Getopt $_opts
366      * @return boolean success
367      */
368     public function executeQueueJob($_opts)
369     {
370         try {
371             $cronuser = Tinebase_User::getInstance()->getFullUserByLoginName($_opts->username);
372         } catch (Tinebase_Exception_NotFound $tenf) {
373             $cronuser = $this->_getCronuserFromConfigOrCreateOnTheFly();
374         }
375         
376         Tinebase_Core::set(Tinebase_Core::USER, $cronuser);
377         
378         $args = $_opts->getRemainingArgs();
379         $message = preg_replace('/^message=/', '', $args[0]);
380         
381         if (! $message) {
382             throw new Tinebase_Exception_InvalidArgument('mandatory parameter "message" is missing');
383         }
384
385         if (null === ($job = json_decode($message, true))) {
386             throw new Tinebase_Exception_InvalidArgument('parameter "message" can not be json decoded');
387         }
388
389         if (isset($job['account_id'])) {
390             Tinebase_Core::set(Tinebase_Core::USER, Tinebase_User::getInstance()->getFullUserById($job['account_id']));
391         }
392
393         Tinebase_ActionQueue::getInstance()->executeAction($job);
394         
395         return TRUE;
396     }
397     
398     /**
399      * clear table as defined in arguments
400      * can clear the following tables:
401      * - credential_cache
402      * - access_log
403      * - async_job
404      * - temp_files
405      * 
406      * if param date is given (date=2010-09-17), all records before this date are deleted (if the table has a date field)
407      * 
408      * @param $_opts
409      * @return boolean success
410      */
411     public function clearTable(Zend_Console_Getopt $_opts)
412     {
413         if (! $this->_checkAdminRight()) {
414             return FALSE;
415         }
416         
417         $args = $this->_parseArgs($_opts, array('tables'), 'tables');
418         $dateString = (isset($args['date']) || array_key_exists('date', $args)) ? $args['date'] : NULL;
419
420         $db = Tinebase_Core::getDb();
421         foreach ($args['tables'] as $table) {
422             switch ($table) {
423                 case 'access_log':
424                     $date = ($dateString) ? new Tinebase_DateTime($dateString) : NULL;
425                     Tinebase_AccessLog::getInstance()->clearTable($date);
426                     break;
427                 case 'async_job':
428                     $where = ($dateString) ? array(
429                         $db->quoteInto($db->quoteIdentifier('end_time') . ' < ?', $dateString)
430                     ) : array();
431                     $where[] = $db->quoteInto($db->quoteIdentifier('status') . ' < ?', 'success');
432                     
433                     echo "\nRemoving all successful async_job entries " . ($dateString ? "before $dateString " : "") . "...";
434                     $deleteCount = $db->delete(SQL_TABLE_PREFIX . $table, $where);
435                     echo "\nRemoved $deleteCount records.";
436                     break;
437                 case 'credential_cache':
438                     Tinebase_Auth_CredentialCache::getInstance()->clearCacheTable($dateString);
439                     break;
440                 case 'temp_files':
441                     Tinebase_TempFile::getInstance()->clearTableAndTempdir($dateString);
442                     break;
443                 default:
444                     echo 'Table ' . $table . " not supported or argument missing.\n";
445             }
446             echo "\nCleared table $table.";
447         }
448         echo "\n\n";
449         
450         return TRUE;
451     }
452     
453     /**
454      * purge deleted records
455      * 
456      * 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)
457      * if table names are given, purge only records from this tables
458      * 
459      * @param $_opts
460      * @return boolean success
461      *
462      * TODO move purge logic to applications, purge Tinebase tables at the end
463      */
464     public function purgeDeletedRecords(Zend_Console_Getopt $_opts)
465     {
466         if (! $this->_checkAdminRight()) {
467             return FALSE;
468         }
469
470         $args = $this->_parseArgs($_opts, array(), 'tables');
471         $doEverything = false;
472
473         if (! (isset($args['tables']) || array_key_exists('tables', $args)) || empty($args['tables'])) {
474             echo "No tables given.\nPurging records from all tables!\n";
475             $args['tables'] = $this->_getAllApplicationTables();
476             $doEverything = true;
477         }
478         
479         $db = Tinebase_Core::getDb();
480         
481         if ((isset($args['date']) || array_key_exists('date', $args))) {
482             echo "\nRemoving all deleted entries before {$args['date']} ...";
483             $where = array(
484                 $db->quoteInto($db->quoteIdentifier('deleted_time') . ' < ?', $args['date'])
485             );
486         } else {
487             echo "\nRemoving all deleted entries ...";
488             $where = array();
489         }
490         $where[] = $db->quoteInto($db->quoteIdentifier('is_deleted') . ' = ?', 1);
491
492         $orderedTables = $this->_orderTables($args['tables']);
493         $this->_purgeTables($orderedTables, $where);
494
495         if ($doEverything) {
496             echo "\nCleaning relations...";
497             $this->cleanRelations();
498
499             echo "\nCleaning modlog...";
500             $this->cleanModlog();
501
502             echo "\nCleaning customfields...";
503             $this->cleanCustomfields();
504
505             echo "\nCleaning notes...";
506             $this->cleanNotes();
507         }
508
509         echo "\n\n";
510         
511         return TRUE;
512     }
513
514     /**
515      * cleanNotes: removes notes of records that have been deleted
516      */
517     public function cleanNotes()
518     {
519         if (! $this->_checkAdminRight()) {
520             return FALSE;
521         }
522
523         $notesController = Tinebase_Notes::getInstance();
524         $notes = $notesController->getAllNotes();
525         $controllers = array();
526         $models = array();
527         $deleteIds = array();
528
529         /** @var Tinebase_Model_Note $note */
530         foreach ($notes as $note) {
531             if (!isset($controllers[$note->record_model])) {
532                 if (strpos($note->record_model, 'Tinebase') === 0) {
533                     continue;
534                 }
535                 try {
536                     $controllers[$note->record_model] = Tinebase_Core::getApplicationInstance($note->record_model);
537                 } catch(Tinebase_Exception_AccessDenied $e) {
538                     // TODO log
539                     continue;
540                 } catch(Tinebase_Exception_NotFound $tenf) {
541                     $deleteIds[] = $note->getId();
542                     continue;
543                 }
544                 $oldACLCheckValue = $controllers[$note->record_model]->doContainerACLChecks(false);
545                 $models[$note->record_model] = array(
546                     0 => new $note->record_model(),
547                     1 => ($note->record_model !== 'Filemanager_Model_Node' ? class_exists($note->record_model . 'Filter') : false),
548                     2 => $note->record_model . 'Filter',
549                     3 => $oldACLCheckValue
550                 );
551             }
552             $controller = $controllers[$note->record_model];
553             $model = $models[$note->record_model];
554
555             if ($model[1]) {
556                 $filter = new $model[2](array(
557                     array('field' => $model[0]->getIdProperty(), 'operator' => 'equals', 'value' => $note->record_id)
558                 ));
559                 if ($model[0]->has('is_deleted')) {
560                     $filter->addFilter(new Tinebase_Model_Filter_Int(array('field' => 'is_deleted', 'operator' => 'notnull', 'value' => NULL)));
561                 }
562                 $result = $controller->searchCount($filter);
563
564                 if (is_bool($result) || (is_string($result) && $result === ((string)intval($result)))) {
565                     $result = (int)$result;
566                 }
567
568                 if (!is_int($result)) {
569                     if (is_array($result) && isset($result['totalcount'])) {
570                         $result = (int)$result['totalcount'];
571                     } elseif(is_array($result) && isset($result['count'])) {
572                         $result = (int)$result['count'];
573                     } else {
574                         // todo log
575                         // dummy line, remove!
576                         $result = 1;
577                     }
578                 }
579
580                 if ($result === 0) {
581                     $deleteIds[] = $note->getId();
582                 }
583             } else {
584                 try {
585                     $controller->get($note->record_id, null, false, true);
586                 } catch(Tinebase_Exception_NotFound $tenf) {
587                     $deleteIds[] = $note->getId();
588                 }
589             }
590         }
591
592         if (count($deleteIds) > 0) {
593             $notesController->purgeNotes($deleteIds);
594         }
595
596         foreach($controllers as $model => $controller) {
597             $controller->doContainerACLChecks($models[$model][3]);
598         }
599
600         echo "\ndeleted " . count($deleteIds) . " notes\n";
601     }
602
603     /**
604      * cleanCustomfields
605      */
606     public function cleanCustomfields()
607     {
608         if (! $this->_checkAdminRight()) {
609             return FALSE;
610         }
611
612         $customFieldController = Tinebase_CustomField::getInstance();
613         $customFieldConfigs = $customFieldController->searchConfig();
614         $deleteCount = 0;
615
616         /** @var Tinebase_Model_CustomField_Config $customFieldConfig */
617         foreach($customFieldConfigs as $customFieldConfig) {
618             $deleteAll = false;
619             try {
620                 $controller = Tinebase_Core::getApplicationInstance($customFieldConfig->model);
621
622                 $oldACLCheckValue = $controller->doContainerACLChecks(false);
623                 if ($customFieldConfig->model !== 'Filemanager_Model_Node') {
624                     $filterClass = $customFieldConfig->model . 'Filter';
625                 } else {
626                     $filterClass = 'ClassThatDoesNotExist';
627                 }
628             } catch(Tinebase_Exception_AccessDenied $e) {
629                 // TODO log
630                 continue;
631             } catch(Tinebase_Exception_NotFound $tenf) {
632                 $deleteAll = true;
633             }
634
635
636
637             $filter = new Tinebase_Model_CustomField_ValueFilter(array(
638                 array('field' => 'customfield_id', 'operator' => 'equals', 'value' => $customFieldConfig->id)
639             ));
640             $customFieldValues = $customFieldController->search($filter);
641             $deleteIds = array();
642
643             if (true === $deleteAll) {
644                 $deleteIds = $customFieldValues->getId();
645             } elseif (class_exists($filterClass)) {
646                 $model = new $customFieldConfig->model();
647                 /** @var Tinebase_Model_CustomField_Value $customFieldValue */
648                 foreach ($customFieldValues as $customFieldValue) {
649                     $filter = new $filterClass(array(
650                         array('field' => $model->getIdProperty(), 'operator' => 'equals', 'value' => $customFieldValue->record_id)
651                     ));
652                     if ($model->has('is_deleted')) {
653                         $filter->addFilter(new Tinebase_Model_Filter_Int(array('field' => 'is_deleted', 'operator' => 'notnull', 'value' => NULL)));
654                     }
655
656                     $result = $controller->searchCount($filter);
657
658                     if (is_bool($result) || (is_string($result) && $result === ((string)intval($result)))) {
659                         $result = (int)$result;
660                     }
661
662                     if (!is_int($result)) {
663                         if (is_array($result) && isset($result['totalcount'])) {
664                             $result = (int)$result['totalcount'];
665                         } elseif(is_array($result) && isset($result['count'])) {
666                             $result = (int)$result['count'];
667                         } else {
668                             // todo log
669                             // dummy line, remove!
670                             $result = 1;
671                         }
672                     }
673
674                     if ($result === 0) {
675                         $deleteIds[] = $customFieldValue->getId();
676                     }
677                 }
678             } else {
679                 /** @var Tinebase_Model_CustomField_Value $customFieldValue */
680                 foreach ($customFieldValues as $customFieldValue) {
681                     try {
682                         $controller->get($customFieldValue->record_id, null, false, true);
683                     } catch(Tinebase_Exception_NotFound $tenf) {
684                         $deleteIds[] = $customFieldValue->getId();
685                     }
686                 }
687             }
688
689             if (count($deleteIds) > 0) {
690                 $customFieldController->deleteCustomFieldValue($deleteIds);
691                 $deleteCount += count($deleteIds);
692             }
693
694             if (true !== $deleteAll) {
695                 $controller->doContainerACLChecks($oldACLCheckValue);
696             }
697         }
698
699         echo "\ndeleted " . $deleteCount . " customfield values\n";
700     }
701     
702     /**
703      * get all app tables
704      * 
705      * @return array
706      */
707     protected function _getAllApplicationTables()
708     {
709         $result = array();
710         
711         $enabledApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED);
712         foreach ($enabledApplications as $application) {
713             $result = array_merge($result, Tinebase_Application::getInstance()->getApplicationTables($application));
714         }
715         
716         return $result;
717     }
718
719     /**
720      * order tables for purging deleted records in a defined order
721      *
722      * @param array $tables
723      * @return array
724      *
725      * TODO could be improved by using usort
726      */
727     protected function _orderTables($tables)
728     {
729         // tags should be deleted first
730         // containers should be deleted last
731
732         $orderedTables = array();
733         $lastTables = array();
734         foreach($tables as $table) {
735             switch ($table) {
736                 case 'container':
737                     $lastTables[] = $table;
738                     break;
739                 case 'tags':
740                     array_unshift($orderedTables, $table);
741                     break;
742                 default:
743                     $orderedTables[] = $table;
744             }
745         }
746         $orderedTables = array_merge($orderedTables, $lastTables);
747
748         return $orderedTables;
749     }
750
751     /**
752      * purge tables
753      *
754      * @param $orderedTables
755      * @param $where
756      */
757     protected function _purgeTables($orderedTables, $where)
758     {
759         foreach ($orderedTables as $table) {
760             try {
761                 $schema = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $table);
762             } catch (Zend_Db_Statement_Exception $zdse) {
763                 echo "\nCould not get schema (" . $zdse->getMessage() . "). Skipping table $table";
764                 continue;
765             }
766             if (!(isset($schema['is_deleted']) || array_key_exists('is_deleted', $schema)) || !(isset($schema['deleted_time']) || array_key_exists('deleted_time', $schema))) {
767                 continue;
768             }
769
770             $deleteCount = 0;
771             try {
772                 $deleteCount = Tinebase_Core::getDb()->delete(SQL_TABLE_PREFIX . $table, $where);
773             } catch (Zend_Db_Statement_Exception $zdse) {
774                 echo "\nFailed to purge deleted records for table $table. " . $zdse->getMessage();
775             }
776             if ($deleteCount > 0) {
777                 echo "\nCleared table $table (deleted $deleteCount records).";
778             }
779             // TODO this should only be echoed with --verbose or written to the logs
780             else {
781                 echo "\nNothing to purge from $table";
782             }
783         }
784     }
785
786     /**
787      * add new customfield config
788      *
789      * example:
790      * $ php tine20.php --method=Tinebase.addCustomfield -- \
791          application="Addressbook" model="Addressbook_Model_Contact" name="datefield" \
792          definition='{"label":"Date","type":"datetime", "uiconfig": {"group":"Dates", "order": 30}}'
793      * @see Tinebase_Model_CustomField_Config for full list
794      *
795      * @param $_opts
796      * @return boolean success
797      */
798     public function addCustomfield(Zend_Console_Getopt $_opts)
799     {
800         if (! $this->_checkAdminRight()) {
801             return FALSE;
802         }
803         
804         // parse args
805         $args = $_opts->getRemainingArgs();
806         $data = array();
807         foreach ($args as $idx => $arg) {
808             list($key, $value) = explode('=', $arg);
809             if ($key == 'application') {
810                 $key = 'application_id';
811                 $value = Tinebase_Application::getInstance()->getApplicationByName($value)->getId();
812             }
813             $data[$key] = $value;
814         }
815         
816         $customfieldConfig = new Tinebase_Model_CustomField_Config($data);
817         $cf = Tinebase_CustomField::getInstance()->addCustomField($customfieldConfig);
818
819         echo "\nCreated customfield: ";
820         print_r($cf->toArray());
821         echo "\n";
822         
823         return TRUE;
824     }
825     
826     /**
827      * nagios monitoring for tine 2.0 database connection
828      * 
829      * @return integer
830      * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
831      */
832     public function monitoringCheckDB()
833     {
834         $message = 'DB CONNECTION FAIL';
835         try {
836             if (! Setup_Core::isRegistered(Setup_Core::CONFIG)) {
837                 Setup_Core::setupConfig();
838             }
839             if (! Setup_Core::isRegistered(Setup_Core::LOGGER)) {
840                 Setup_Core::setupLogger();
841             }
842             $time_start = microtime(true);
843             $dbcheck = Setup_Core::setupDatabaseConnection();
844             $time = (microtime(true) - $time_start) * 1000;
845         } catch (Exception $e) {
846             $message .= ': ' . $e->getMessage();
847             $dbcheck = FALSE;
848         }
849         
850         if ($dbcheck) {
851             echo "DB CONNECTION OK | connecttime={$time}ms;;;;\n";
852             return 0;
853         } 
854         
855         echo $message . "\n";
856         return 2;
857     }
858     
859     /**
860      * nagios monitoring for tine 2.0 config file
861      * 
862      * @return integer
863      * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
864      */
865     public function monitoringCheckConfig()
866     {
867         $message = 'CONFIG FAIL';
868         $configcheck = FALSE;
869         
870         $configfile = Setup_Core::getConfigFilePath();
871         if ($configfile) {
872             $configfile = escapeshellcmd($configfile);
873             if (preg_match('/^win/i', PHP_OS)) {
874                 exec("php -l $configfile 2> NUL", $error, $code);
875             } else {
876                 exec("php -l $configfile 2> /dev/null", $error, $code);
877             }
878             if ($code == 0) {
879                 $configcheck = TRUE;
880             } else {
881                 $message .= ': CONFIG FILE SYNTAX ERROR';
882             }
883         } else {
884             $message .= ': CONFIG FILE MISSING';
885         }
886         
887         if ($configcheck) {
888             echo "CONFIG FILE OK\n";
889             return 0;
890         } else {
891             echo $message . "\n";
892             return 2;
893         }
894     }
895     
896     /**
897     * nagios monitoring for tine 2.0 async cronjob run
898     *
899     * @return integer
900     * 
901     * @see http://nagiosplug.sourceforge.net/developer-guidelines.html#PLUGOUTPUT
902     * @see 0008038: monitoringCheckCron -> check if cron did run in the last hour
903     */
904     public function monitoringCheckCron()
905     {
906         $message = 'CRON FAIL';
907
908         try {
909             $lastJob = Tinebase_AsyncJob::getInstance()->getLastJob('Tinebase_Event_Async_Minutely');
910             
911             if ($lastJob === NULL) {
912                 $message .= ': NO LAST JOB FOUND';
913                 $result = 1;
914             } else {
915                 if ($lastJob->end_time instanceof Tinebase_DateTime) {
916                     $duration = $lastJob->end_time->getTimestamp() - $lastJob->start_time->getTimestamp();
917                     $valueString = ' | duration=' . $duration . 's;;;;';
918                     $valueString .= ' end=' . $lastJob->end_time->getIso() . ';;;;';
919                 } else {
920                     $valueString = '';
921                 }
922                 
923                 if ($lastJob->status === Tinebase_Model_AsyncJob::STATUS_RUNNING && Tinebase_DateTime::now()->isLater($lastJob->end_time)) {
924                     $message .= ': LAST JOB TOOK TOO LONG';
925                     $result = 1;
926                 } else if ($lastJob->status === Tinebase_Model_AsyncJob::STATUS_FAILURE) {
927                     $message .= ': LAST JOB FAILED';
928                     $result = 1;
929                 } else if (Tinebase_DateTime::now()->isLater($lastJob->start_time->addHour(1))) {
930                     $message .= ': NO JOB IN THE LAST HOUR';
931                     $result = 1;
932                 } else {
933                     $message = 'CRON OK';
934                     $result = 0;
935                 }
936                 $message .= $valueString;
937             }
938         } catch (Exception $e) {
939             $message .= ': ' . $e->getMessage();
940             $result = 2;
941         }
942         
943         echo $message . "\n";
944         return $result;
945     }
946     
947     /**
948      * nagios monitoring for tine 2.0 logins during the last 5 mins
949      * 
950      * @return number
951      * 
952      * @todo allow to configure timeslot
953      */
954     public function monitoringLoginNumber()
955     {
956         $message = 'LOGINS';
957         $result  = 0;
958         
959         try {
960             $filter = new Tinebase_Model_AccessLogFilter(array(
961                 array('field' => 'li', 'operator' => 'after', 'value' => Tinebase_DateTime::now()->subMinute(5))
962             ));
963             $accesslogs = Tinebase_AccessLog::getInstance()->search($filter, NULL, FALSE, TRUE);
964             $valueString = ' | count=' . count($accesslogs) . ';;;;';
965             $message .= ' OK' . $valueString;
966         } catch (Exception $e) {
967             $message .= ' FAIL: ' . $e->getMessage();
968             $result = 2;
969         }
970         
971         echo $message . "\n";
972         return $result;
973     }
974
975     /**
976      * nagios monitoring for tine 2.0 active users
977      *
978      * @return number
979      *
980      * @todo allow to configure timeslot / currently the active users of the last month are returned
981      */
982     public function monitoringActiveUsers()
983     {
984         $message = 'ACTIVE USERS';
985         $result  = 0;
986
987         try {
988             $userCount = Tinebase_User::getInstance()->getActiveUserCount();
989             $valueString = ' | count=' . $userCount . ';;;;';
990             $message .= ' OK' . $valueString;
991         } catch (Exception $e) {
992             $message .= ' FAIL: ' . $e->getMessage();
993             $result = 2;
994         }
995
996         echo $message . "\n";
997         return $result;
998     }
999
1000     /**
1001      * undo changes to records defined by certain criteria (user, date, fields, ...)
1002      * 
1003      * example: $ php tine20.php --username pschuele --method Tinebase.undo -d 
1004      *   -- record_type=Addressbook_Model_Contact modification_time=2013-05-08 modification_account=3263
1005      * 
1006      * @param Zend_Console_Getopt $opts
1007      */
1008     public function undo(Zend_Console_Getopt $opts)
1009     {
1010         if (! $this->_checkAdminRight()) {
1011             return FALSE;
1012         }
1013         
1014         $data = $this->_parseArgs($opts, array('modification_time'));
1015         
1016         // build filter from params
1017         $filterData = array();
1018         $allowedFilters = array(
1019             'record_type',
1020             'modification_time',
1021             'modification_account',
1022             'record_id'
1023         );
1024         foreach ($data as $key => $value) {
1025             if (in_array($key, $allowedFilters)) {
1026                 $operator = ($key === 'modification_time') ? 'within' : 'equals';
1027                 $filterData[] = array('field' => $key, 'operator' => $operator, 'value' => $value);
1028             }
1029         }
1030         $filter = new Tinebase_Model_ModificationLogFilter($filterData);
1031         
1032         $dryrun = $opts->d;
1033         $overwrite = (isset($data['overwrite']) && $data['overwrite']) ? TRUE : FALSE;
1034         $result = Tinebase_Timemachine_ModificationLog::getInstance()->undo($filter, $overwrite, $dryrun, (isset($data['modified_attribute'])?$data['modified_attribute']:null));
1035         
1036         if (! $dryrun) {
1037             echo 'Reverted ' . $result['totalcount'] . " change(s)\n";
1038         } else {
1039             echo "Dry run\n";
1040             echo 'Would revert ' . $result['totalcount'] . " change(s):\n";
1041             foreach ($result['undoneModlogs'] as $modlog) {
1042                 $modifiedAttribute = $modlog->modified_attribute;
1043                 if (!empty($modifiedAttribute)) {
1044                     echo 'id ' . $modlog->record_id . ' [' . $modifiedAttribute . ']: ' . $modlog->new_value . ' -> ' . $modlog->old_value . PHP_EOL;
1045                 } else {
1046                     if ($modlog->change_type === Tinebase_Timemachine_ModificationLog::CREATED) {
1047                         echo 'id ' . $modlog->record_id . ' DELETE' . PHP_EOL;
1048                     } elseif ($modlog->change_type === Tinebase_Timemachine_ModificationLog::DELETED) {
1049                         echo 'id ' . $modlog->record_id . ' UNDELETE' . PHP_EOL;
1050                     } else {
1051                         $diff = new Tinebase_Record_Diff(json_decode($modlog->new_value));
1052                         foreach($diff->diff as $key => $val) {
1053                             echo 'id ' . $modlog->record_id . ' [' . $key . ']: ' . $val . ' -> ' . $diff->oldData[$key] . PHP_EOL;
1054                         }
1055                     }
1056                 }
1057             }
1058         }
1059         echo 'Failcount: ' . $result['failcount'] . "\n";
1060         return 0;
1061     }
1062     
1063     /**
1064      * creates demo data for all applications
1065      * accepts same arguments as Tinebase_Frontend_Cli_Abstract::createDemoData
1066      * and the additional argument "skipAdmin" to force no user/group/role creation
1067      * 
1068      * @param Zend_Console_Getopt $_opts
1069      */
1070     public function createAllDemoData($_opts)
1071     {
1072         if (! $this->_checkAdminRight()) {
1073             return FALSE;
1074         }
1075         
1076         // fetch all applications and check if required are installed, otherwise remove app from array
1077         $applications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
1078         foreach($applications as $appName) {
1079             echo 'Searching for DemoData in application "' . $appName . '"...' . PHP_EOL;
1080             $className = $appName.'_Setup_DemoData';
1081             if (class_exists($className)) {
1082                 echo 'DemoData in application "' . $appName . '" found!' . PHP_EOL;
1083                 $required = $className::getRequiredApplications();
1084                 foreach($required as $requiredApplication) {
1085                     if (! Tinebase_Helper::in_array_case($applications, $requiredApplication)) {
1086                         echo 'Creating DemoData for Application ' . $appName . ' is impossible, because application "' . $requiredApplication . '" is not installed.' . PHP_EOL;
1087                         continue 2;
1088                     }
1089                 }
1090                 $this->_applicationsToWorkOn[$appName] = array('appName' => $appName, 'required' => $required);
1091             } else {
1092                 echo 'DemoData in application "' . $appName . '" not found.' . PHP_EOL . PHP_EOL;
1093             }
1094         }
1095         unset($applications);
1096         
1097         foreach($this->_applicationsToWorkOn as $app => $cfg) {
1098             $this->_createDemoDataRecursive($app, $cfg, $_opts);
1099         }
1100
1101         return 0;
1102     }
1103     
1104     /**
1105      * creates demo data and calls itself if there are required apps
1106      * 
1107      * @param string $app
1108      * @param array $cfg
1109      * @param Zend_Console_Getopt $opts
1110      */
1111     protected function _createDemoDataRecursive($app, $cfg, $opts)
1112     {
1113         if (isset($cfg['required']) && is_array($cfg['required'])) {
1114             foreach($cfg['required'] as $requiredApp) {
1115                 $this->_createDemoDataRecursive($requiredApp, $this->_applicationsToWorkOn[$requiredApp], $opts);
1116             }
1117         }
1118         
1119         $className = $app . '_Frontend_Cli';
1120         
1121         $classNameDD = $app . '_Setup_DemoData';
1122         
1123         if (class_exists($className)) {
1124             if (! $classNameDD::hasBeenRun()) {
1125                 echo 'Creating DemoData in application "' . $app . '"...' . PHP_EOL;
1126                 $class = new $className();
1127                 $class->createDemoData($opts, FALSE);
1128             } else {
1129                 echo 'DemoData for ' . $app . ' has been run already, skipping...' . PHP_EOL;
1130             }
1131         } else {
1132             echo 'Could not found ' . $className . ', so DemoData for application "' . $app . '" could not be created!';
1133         }
1134     }
1135     
1136     /**
1137      * clears deleted files from filesystem + database
1138      *
1139      * @return int
1140      */
1141     public function clearDeletedFiles()
1142     {
1143         if (! $this->_checkAdminRight()) {
1144             return -1;
1145         }
1146         
1147         $this->_addOutputLogWriter();
1148         
1149         Tinebase_FileSystem::getInstance()->clearDeletedFiles();
1150
1151         return 0;
1152     }
1153
1154     /**
1155      * recalculates the revision sizes and then the folder sizes
1156      *
1157      * @return int
1158      */
1159     public function fileSystemSizeRecalculation()
1160     {
1161         if (! $this->_checkAdminRight()) {
1162             return -1;
1163         }
1164
1165         Tinebase_FileSystem::getInstance()->recalculateRevisionSize();
1166
1167         Tinebase_FileSystem::getInstance()->recalculateFolderSize();
1168
1169         return 0;
1170     }
1171
1172     /**
1173      * checks if there are not yet indexed file objects and adds them to the index synchronously
1174      * that means this can be very time consuming
1175      *
1176      * @return int
1177      */
1178     public function fileSystemCheckIndexing()
1179     {
1180
1181         if (! $this->_checkAdminRight()) {
1182             return -1;
1183         }
1184
1185         Tinebase_FileSystem::getInstance()->checkIndexing();
1186
1187         return 0;
1188     }
1189
1190     /**
1191      * repair a table
1192      * 
1193      * @param Zend_Console_Getopt $opts
1194      * 
1195      * @todo add more tables
1196      */
1197     public function repairTable($opts)
1198     {
1199         if (! $this->_checkAdminRight()) {
1200             return FALSE;
1201         }
1202         
1203         $this->_addOutputLogWriter();
1204         
1205         $data = $this->_parseArgs($opts, array('table'));
1206         
1207         switch ($data['table']) {
1208             case 'importexport_definition':
1209                 Tinebase_ImportExportDefinition::getInstance()->repairTable();
1210                 $result = 0;
1211                 break;
1212             default:
1213                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1214                     . ' No repair script found for ' . $data['table']);
1215                 $result = 1;
1216         }
1217         
1218         exit($result);
1219     }
1220
1221     /**
1222      * transfer relations
1223      * 
1224      * @param Zend_Console_Getopt $opts
1225      */
1226     public function transferRelations($opts)
1227     {
1228         if (! $this->_checkAdminRight()) {
1229             return FALSE;
1230         }
1231         
1232         $this->_addOutputLogWriter();
1233         
1234         try {
1235             $args = $this->_parseArgs($opts, array('oldId', 'newId', 'model'));
1236         } catch (Tinebase_Exception_InvalidArgument $e) {
1237             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1238                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Parameters "oldId", "newId" and "model" are required!');
1239             }
1240             exit(1);
1241         }
1242         
1243         $skippedEntries = Tinebase_Relations::getInstance()->transferRelations($args['oldId'], $args['newId'], $args['model']);
1244
1245         if (! empty($skippedEntries) && Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1246             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . count($skippedEntries) . ' entries has been skipped:');
1247         }
1248         
1249         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1250             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' The operation has been terminated successfully.');
1251         }
1252
1253         return 0;
1254     }
1255
1256     /**
1257      * repair function for persistent filters (favorites) without grants: this adds default grants for those filters.
1258      *
1259      * @return int
1260      */
1261     public function setDefaultGrantsOfPersistentFilters()
1262     {
1263         if (! $this->_checkAdminRight()) {
1264             return -1;
1265         }
1266
1267         $this->_addOutputLogWriter(6);
1268
1269         // get all persistent filters without grants
1270         // TODO this could be enhanced by allowing to set default grants for other filters, too
1271         Tinebase_PersistentFilter::getInstance()->doContainerACLChecks(false);
1272         $filters = Tinebase_PersistentFilter::getInstance()->search(new Tinebase_Model_PersistentFilterFilter(array(),'', array('ignoreAcl' => true)));
1273         $filtersWithoutGrants = 0;
1274
1275         foreach ($filters as $filter) {
1276             if (count($filter->grants) == 0) {
1277                 // update to set default grants
1278                 $filter = Tinebase_PersistentFilter::getInstance()->update($filter);
1279                 $filtersWithoutGrants++;
1280
1281                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1282                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1283                         . ' Updated filter: ' . print_r($filter->toArray(), true));
1284                 }
1285             }
1286         }
1287
1288         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1289             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1290                 . ' Set default grants for ' . $filtersWithoutGrants . ' filters'
1291                 . ' (checked ' . count($filters) . ' in total).');
1292         }
1293
1294         return 0;
1295     }
1296
1297     /**
1298      *
1299      *
1300      * @return int
1301      */
1302     public function repairContainerOwner()
1303     {
1304         if (! $this->_checkAdminRight()) {
1305             return 2;
1306         }
1307
1308         $this->_addOutputLogWriter(6);
1309         Tinebase_Container::getInstance()->setContainerOwners();
1310
1311         return 0;
1312     }
1313
1314     /**
1315      * show user report (number of enabled, disabled, ... users)
1316      *
1317      * TODO add system user count
1318      * TODO use twig?
1319      */
1320     public function userReport()
1321     {
1322         if (! $this->_checkAdminRight()) {
1323             return 2;
1324         }
1325
1326         $translation = Tinebase_Translation::getTranslation('Tinebase');
1327
1328         $userStatus = array(
1329             'total' => array(),
1330             Tinebase_Model_User::ACCOUNT_STATUS_ENABLED => array(/* 'showUserNames' => true, 'showClients' => true */),
1331             Tinebase_Model_User::ACCOUNT_STATUS_DISABLED => array(),
1332             Tinebase_Model_User::ACCOUNT_STATUS_BLOCKED => array(),
1333             Tinebase_Model_User::ACCOUNT_STATUS_EXPIRED => array(),
1334             //'system' => array(),
1335             'lastmonth' => array('lastMonths' => 1, 'showUserNames' => true, 'showClients' => true),
1336             'last 3 months' => array('lastMonths' => 3),
1337         );
1338
1339         foreach ($userStatus as $status => $options) {
1340             switch ($status) {
1341                 case 'lastmonth':
1342                 case 'last 3 months':
1343                     $userCount = Tinebase_User::getInstance()->getActiveUserCount($options['lastMonths']);
1344                     $text = $translation->_("Number of distinct users") . " (" . $status . "): " . $userCount . "\n";
1345                     break;
1346                 case 'system':
1347                     $text = "TODO add me\n";
1348                     break;
1349                 default:
1350                     $userCount = Tinebase_User::getInstance()->getUserCount($status);
1351                     $text = $translation->_("Number of users") . " (" . $status . "): " . $userCount . "\n";
1352             }
1353             echo $text;
1354
1355             if (isset($options['showUserNames']) && $options['showUserNames']
1356                 && in_array($status, array('lastmonth', 'last 3 months'))
1357                 && isset($options['lastMonths'])
1358             ) {
1359                 // TODO allow this for other status
1360                 echo $translation->_("  User Accounts:\n");
1361                 $userIds = Tinebase_User::getInstance()->getActiveUserIds($options['lastMonths']);
1362                 foreach ($userIds as $userId) {
1363                     $user = Tinebase_User::getInstance()->getUserByProperty('accountId', $userId, 'Tinebase_Model_FullUser');
1364                     echo "  * " . $user->accountLoginName . ' / ' . $user->accountDisplayName . "\n";
1365                     if (isset($options['showClients']) && $options['showClients']) {
1366                         $userClients = Tinebase_AccessLog::getInstance()->getUserClients($user, $options['lastMonths']);
1367                         echo "    Clients: \n";
1368                         foreach ($userClients as $client) {
1369                             echo "     - $client\n";
1370                         }
1371                         echo "\n";
1372                     }
1373                 }
1374             }
1375             echo "\n";
1376         }
1377
1378         return 0;
1379     }
1380
1381     public function createFullTextIndex()
1382     {
1383         if (! $this->_checkAdminRight()) {
1384             return -1;
1385         }
1386
1387         $setupBackend = Setup_Backend_Factory::factory();
1388         if (!$setupBackend->supports('mysql >= 5.6.4')) {
1389             return -2;
1390         }
1391
1392         $failures = array();
1393         try {
1394             $declaration = new Setup_Backend_Schema_Index_Xml('
1395                 <index>
1396                     <name>note</name>
1397                     <fulltext>true</fulltext>
1398                     <field>
1399                         <name>note</name>
1400                     </field>
1401                 </index>
1402             ');
1403             $setupBackend->addIndex('addressbook', $declaration);
1404         } catch (Exception $e) {
1405             $failures[] = 'addressbook';
1406         }
1407
1408         try {
1409             $declaration = new Setup_Backend_Schema_Index_Xml('
1410                 <index>
1411                     <name>description</name>
1412                     <fulltext>true</fulltext>
1413                     <field>
1414                         <name>description</name>
1415                     </field>
1416                 </index>
1417             ');
1418             $setupBackend->addIndex('cal_events', $declaration);
1419         } catch (Exception $e) {
1420             $failures[] = 'cal_events';
1421         }
1422
1423         try {
1424             $declaration = new Setup_Backend_Schema_Index_Xml('
1425                 <index>
1426                     <name>description</name>
1427                     <fulltext>true</fulltext>
1428                     <field>
1429                         <name>description</name>
1430                     </field>
1431                 </index>
1432             ');
1433             $setupBackend->addIndex('metacrm_lead', $declaration);
1434         } catch (Exception $e) {
1435             $failures[] = 'metacrm_lead';
1436         }
1437
1438         try {
1439             $declaration = new Setup_Backend_Schema_Index_Xml('
1440                 <index>
1441                     <name>description</name>
1442                     <fulltext>true</fulltext>
1443                     <field>
1444                         <name>description</name>
1445                     </field>
1446                 </index>
1447             ');
1448             $setupBackend->addIndex('events_event', $declaration);
1449         } catch (Exception $e) {
1450             $failures[] = 'events_event';
1451         }
1452
1453         try {
1454             $declaration = new Setup_Backend_Schema_Index_Xml('
1455                 <index>
1456                     <name>description</name>
1457                     <fulltext>true</fulltext>
1458                     <field>
1459                         <name>description</name>
1460                     </field>
1461                 </index>
1462             ');
1463             $setupBackend->addIndex('projects_project', $declaration);
1464         } catch (Exception $e) {
1465             $failures[] = 'projects_project';
1466         }
1467
1468         try {
1469             $declaration = new Setup_Backend_Schema_Index_Xml('
1470                 <index>
1471                     <name>description</name>
1472                     <fulltext>true</fulltext>
1473                     <field>
1474                         <name>description</name>
1475                     </field>
1476                 </index>
1477             ');
1478             $setupBackend->addIndex('sales_contracts', $declaration);
1479         } catch (Exception $e) {
1480             $failures[] = 'sales_contracts';
1481         }
1482
1483         try {
1484             $declaration = new Setup_Backend_Schema_Index_Xml('
1485                 <index>
1486                     <name>description</name>
1487                     <fulltext>true</fulltext>
1488                     <field>
1489                         <name>description</name>
1490                     </field>
1491                 </index>
1492             ');
1493             $setupBackend->addIndex('sales_products', $declaration);
1494         } catch (Exception $e) {
1495             $failures[] = 'sales_products';
1496         }
1497
1498         try {
1499             $declaration = new Setup_Backend_Schema_Index_Xml('
1500                 <index>
1501                     <name>description</name>
1502                     <fulltext>true</fulltext>
1503                     <field>
1504                         <name>description</name>
1505                     </field>
1506                 </index>
1507             ');
1508             $setupBackend->addIndex('sales_customers', $declaration);
1509         } catch (Exception $e) {
1510             $failures[] = 'sales_customers';
1511         }
1512
1513         try {
1514             $declaration = new Setup_Backend_Schema_Index_Xml('
1515                 <index>
1516                     <name>description</name>
1517                     <fulltext>true</fulltext>
1518                     <field>
1519                         <name>description</name>
1520                     </field>
1521                 </index>
1522             ');
1523             $setupBackend->addIndex('sales_suppliers', $declaration);
1524         } catch (Exception $e) {
1525             $failures[] = 'sales_suppliers';
1526         }
1527
1528         try {
1529             $declaration = new Setup_Backend_Schema_Index_Xml('
1530                 <index>
1531                     <name>description</name>
1532                     <fulltext>true</fulltext>
1533                     <field>
1534                         <name>description</name>
1535                     </field>
1536                 </index>
1537             ');
1538             $setupBackend->addIndex('sales_purchase_invoices', $declaration);
1539         } catch (Exception $e) {
1540             $failures[] = 'sales_purchase_invoices';
1541         }
1542
1543         try {
1544             $declaration = new Setup_Backend_Schema_Index_Xml('
1545                 <index>
1546                     <name>description</name>
1547                     <fulltext>true</fulltext>
1548                     <field>
1549                         <name>description</name>
1550                     </field>
1551                 </index>
1552             ');
1553             $setupBackend->addIndex('sales_sales_invoices', $declaration);
1554         } catch (Exception $e) {
1555             $failures[] = 'sales_sales_invoices';
1556         }
1557
1558         try {
1559             $declaration = new Setup_Backend_Schema_Index_Xml('
1560                 <index>
1561                     <name>description</name>
1562                     <fulltext>true</fulltext>
1563                     <field>
1564                         <name>description</name>
1565                     </field>
1566                 </index>
1567             ');
1568             $setupBackend->addIndex('sales_offers', $declaration);
1569         } catch (Exception $e) {
1570             $failures[] = 'sales_offers';
1571         }
1572
1573         try {
1574             $declaration = new Setup_Backend_Schema_Index_Xml('
1575                 <index>
1576                     <name>description</name>
1577                     <fulltext>true</fulltext>
1578                     <field>
1579                         <name>description</name>
1580                     </field>
1581                 </index>
1582             ');
1583             $setupBackend->addIndex('tasks', $declaration);
1584         } catch (Exception $e) {
1585             $failures[] = 'tasks';
1586         }
1587
1588         try {
1589             $declaration = new Setup_Backend_Schema_Index_Xml('
1590                 <index>
1591                     <name>description</name>
1592                     <fulltext>true</fulltext>
1593                     <field>
1594                         <name>description</name>
1595                     </field>
1596                 </index>
1597             ');
1598             $setupBackend->addIndex('timetracker_timesheet', $declaration);
1599         } catch (Exception $e) {
1600             $failures[] = 'timetracker_timesheet';
1601         }
1602
1603         try {
1604             $declaration = new Setup_Backend_Schema_Index_Xml('
1605                 <index>
1606                     <name>description</name>
1607                     <fulltext>true</fulltext>
1608                     <field>
1609                         <name>description</name>
1610                     </field>
1611                 </index>
1612             ');
1613             $setupBackend->addIndex('timetracker_timeaccount', $declaration);
1614         } catch (Exception $e) {
1615             $failures[] = 'timetracker_timeaccount';
1616         }
1617
1618         try {
1619             if ($setupBackend->tableExists('path')) {
1620                 $declaration = new Setup_Backend_Schema_Index_Xml('
1621                     <index>
1622                         <name>path</name>
1623                         <fulltext>true</fulltext>
1624                         <field>
1625                             <name>path</name>
1626                         </field>
1627                     </index>
1628                 ');
1629                 $setupBackend->addIndex('path', $declaration);
1630             }
1631         } catch (Exception $e) {
1632             $failures[] = 'path';
1633         }
1634
1635         try {
1636             if ($setupBackend->tableExists('path')) {
1637                 $declaration = new Setup_Backend_Schema_Index_Xml('
1638                     <index>
1639                         <name>shadow_path</name>
1640                         <fulltext>true</fulltext>
1641                         <field>
1642                             <name>shadow_path</name>
1643                         </field>
1644                     </index>
1645                 ');
1646                 $setupBackend->addIndex('path', $declaration);
1647             }
1648         } catch (Exception $e) {
1649             $failures[] = 'shadow_path';
1650         }
1651
1652         try {
1653             if (!$setupBackend->tableExists('path')) {
1654                 $declaration = new Setup_Backend_Schema_Table_Xml('<table>
1655                     <name>path</name>
1656                     <version>2</version>
1657                     <requirements>
1658                         <required>mysql >= 5.6.4</required>
1659                     </requirements>
1660                     <declaration>
1661                         <field>
1662                             <name>id</name>
1663                             <type>text</type>
1664                             <length>40</length>
1665                             <notnull>true</notnull>
1666                         </field>
1667                         <field>
1668                             <name>path</name>
1669                             <type>text</type>
1670                             <length>65535</length>
1671                             <notnull>true</notnull>
1672                         </field>
1673                         <field>
1674                             <name>shadow_path</name>
1675                             <type>text</type>
1676                             <length>65535</length>
1677                             <notnull>true</notnull>
1678                         </field>
1679                         <field>
1680                             <name>creation_time</name>
1681                             <type>datetime</type>
1682                         </field>
1683                         <index>
1684                             <name>id</name>
1685                             <primary>true</primary>
1686                             <field>
1687                                 <name>id</name>
1688                             </field>
1689                         </index>
1690                         <index>
1691                         <name>path</name>
1692                             <fulltext>true</fulltext>
1693                             <field>
1694                                 <name>path</name>
1695                             </field>
1696                         </index>
1697                         <index>
1698                             <name>shadow_path</name>
1699                             <fulltext>true</fulltext>
1700                             <field>
1701                                 <name>shadow_path</name>
1702                             </field>
1703                         </index>
1704                     </declaration>
1705                 </table>');
1706
1707                 $tmp = new Setup_Update_Abstract($setupBackend);
1708                 $tmp->createTable('path', $declaration, 'Tinebase', 2);
1709
1710                 $setupUser = Setup_Update_Abstract::getSetupFromConfigOrCreateOnTheFly();
1711                 if ($setupUser) {
1712                     Tinebase_Core::set(Tinebase_Core::USER, $setupUser);
1713                     Tinebase_Controller::getInstance()->rebuildPaths();
1714                 } else {
1715                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
1716                         Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1717                             . ' Could not find valid setupuser. Skipping rebuildPaths: you might need to run this manually.');
1718                     }
1719                 }
1720             }
1721         } catch (Exception $e) {
1722             $failures[] = 'create path';
1723         }
1724
1725         try {
1726             $declaration = new Setup_Backend_Schema_Index_Xml('
1727                 <index>
1728                     <name>text_data</name>
1729                     <fulltext>true</fulltext>
1730                     <field>
1731                         <name>text_data</name>
1732                     </field>
1733                 </index>
1734             ');
1735             $setupBackend->addIndex('external_fulltext', $declaration);
1736         } catch (Exception $e) {
1737             $failures[] = 'external_fulltext';
1738         }
1739
1740         try {
1741             $declaration = new Setup_Backend_Schema_Index_Xml('
1742                 <index>
1743                     <name>description</name>
1744                     <fulltext>true</fulltext>
1745                     <field>
1746                         <name>description</name>
1747                     </field>
1748                 </index>
1749             ');
1750             $setupBackend->addIndex('tree_fileobjects', $declaration);
1751         } catch (Exception $e) {
1752             $failures[] = 'tree_fileobjects';
1753         }
1754
1755         try {
1756             try {
1757                 $setupBackend->dropIndex('tags', 'description');
1758             } catch (Exception $e) {
1759                 // Ignore, if there is no index, we can just go on and create one.
1760             }
1761             $declaration = new Setup_Backend_Schema_Index_Xml('
1762                 <index>
1763                     <name>description</name>
1764                     <fulltext>true</fulltext>
1765                     <field>
1766                         <name>description</name>
1767                     </field>
1768                 </index>
1769             ');
1770             $setupBackend->addIndex('tags', $declaration);
1771         } catch (Exception $e) {
1772             $failures[] = 'tags';
1773         }
1774
1775         if (count($failures) > 0) {
1776             echo PHP_EOL . 'failures: ' . join(' ', $failures);
1777         }
1778
1779         echo PHP_EOL . 'done' . PHP_EOL . PHP_EOL;
1780     }
1781 }