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