5c896f0544783b14433a4209b4e7d3c1930aaf9f
[tine20] / tine20 / Calendar / Frontend / Cli.php
1 <?php
2 /**
3  * Tine 2.0
4  * @package     Calendar
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Cornelius Weiss <c.weiss@metaways.de>
7  * @copyright   Copyright (c) 2009-2013 Metaways Infosystems GmbH (http://www.metaways.de)
8  */
9
10 /**
11  * Cli frontend for Calendar
12  *
13  * This class handles cli requests for the Calendar
14  *
15  * @package     Calendar
16  */
17 class Calendar_Frontend_Cli extends Tinebase_Frontend_Cli_Abstract
18 {
19     /**
20      * the internal name of the application
21      * 
22      * @var string
23      */
24     protected $_applicationName = 'Calendar';
25     
26     /**
27      * help array with function names and param descriptions
28      * 
29      * @return void
30      */
31     protected $_help = array(
32         'updateCalDavData' => array(
33             'description'    => 'update calendar/events from a CalDav source using etags',
34             'params'         => array(
35                 'url'        => 'CalDav source URL',
36                 'caldavuserfile' => 'CalDav user file containing utf8 username;pwd',
37              )
38         ),
39         'importCalDavData' => array(
40             'description'    => 'import calendar/events from a CalDav source',
41             'params'         => array(
42                 'url'        => 'CalDav source URL',
43                 'caldavuserfile' => 'CalDav user file containing utf8 username;pwd',
44              )
45         ),
46         'importCalDavCalendars' => array(
47             'description'    => 'import calendars without events from a CalDav source',
48             'params'         => array(
49                 'url'        => 'CalDav source URL',
50                 'caldavuserfile' => 'CalDav user file containing utf8 username;pwd',
51              )
52         ),
53         'importegw14' => array(
54             'description'    => 'imports calendars/events from egw 1.4',
55             'params'         => array(
56                 'host'       => 'dbhost',
57                 'username'   => 'username',
58                 'password'   => 'password',
59                 'dbname'     => 'dbname'
60             )
61         ),
62         'exportICS' => array(  
63             'description'    => "export calendar as ics", 
64             'params'         => array('container_id') 
65         ),
66     );
67     
68     /**
69      * return anonymous methods
70      * 
71      * @return array
72      */
73     public static function getAnonymousMethods()
74     {
75         return array('Calendar.repairDanglingDisplaycontainerEvents');
76     }
77     
78     /**
79      * import events
80      *
81      * @param Zend_Console_Getopt $_opts
82      */
83     public function import($_opts)
84     {
85         parent::_import($_opts);
86     }
87     
88     /**
89      * exports calendars as ICS
90      *
91      * @param Zend_Console_Getopt $_opts
92      */
93     public function exportICS($_opts)
94     {
95         $opts = $_opts->getRemainingArgs();
96         $container_id = $opts[0];
97         $filter = new Calendar_Model_EventFilter(array(
98             array(
99                 'field'     => 'container_id',
100                 'operator'  => 'equals',
101                 'value'     => $container_id
102             )
103
104         ));
105         $result = Calendar_Controller_MSEventFacade::getInstance()->search($filter, null, false, false, 'get');
106         if ($result->count() == 0) {
107             throw new Tinebase_Exception('this calendar does not contain any records.');
108         }
109         $converter = Calendar_Convert_Event_VCalendar_Factory::factory("generic");
110         $result = $converter->fromTine20RecordSet($result);
111         print $result->serialize();
112     }
113     
114     /**
115      * delete duplicate events
116      * 
117      * @see 0008182: event with lots of exceptions breaks calendar sync
118      * 
119      * @todo allow user to set params
120      */
121     public function deleteDuplicateEvents()
122     {
123         $writer = new Zend_Log_Writer_Stream('php://output');
124         $writer->addFilter(new Zend_Log_Filter_Priority(6));
125         Tinebase_Core::getLogger()->addWriter($writer);
126         
127         $be = new Calendar_Backend_Sql();
128         $filter = new Calendar_Model_EventFilter(array(array(
129             'field'    => 'dtstart',
130             'operator' => 'after',
131             'value'    => Tinebase_DateTime::now(),
132         ), array(
133             'field'    => 'organizer',
134             'operator' => 'equals',
135             'value'    => 'contactid', // TODO: set correct contact_id or use container_id filter
136         )));
137         $dryrun = TRUE;
138         $be->deleteDuplicateEvents($filter, $dryrun);
139     }
140     
141     /**
142      * repair dangling attendee records (no displaycontainer_id)
143      * 
144      * @see https://forge.tine20.org/mantisbt/view.php?id=8172
145      */
146     public function repairDanglingDisplaycontainerEvents()
147     {
148         $writer = new Zend_Log_Writer_Stream('php://output');
149         $writer->addFilter(new Zend_Log_Filter_Priority(5));
150         Tinebase_Core::getLogger()->addWriter($writer);
151         
152         $be = new Calendar_Backend_Sql();
153         $be->repairDanglingDisplaycontainerEvents();
154     }
155     
156     /**
157      * import calendars from a CalDav source
158      * 
159      * param Zend_Console_Getopt $_opts
160      */
161     public function importCalDavCalendars(Zend_Console_Getopt $_opts)
162     {
163         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile'));
164         
165         $this->_addOutputLogWriter(4);
166         
167         $users = $this->_readCalDavUserFile($args['caldavuserfile']);
168         
169         $this->_importAllCalendars($users, $args['url']);
170     }
171     
172     protected function _importAllCalendars($users, $uri)
173     {
174         $client = new Calendar_Import_CalDav_Client(array('baseUri' => $uri), 'MacOSX');
175         $client->setVerifyPeer(false);
176         $client->importAllCalendarsForUsers($users);
177     }
178     
179     /**
180      * import calendar events from a CalDav source
181      * 
182      * param Zend_Console_Getopt $_opts
183      */
184     public function importCalDavData(Zend_Console_Getopt $_opts)
185     {
186         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile'));
187         
188         $writer = new Zend_Log_Writer_Stream('php://output');
189         $writer->addFilter(new Zend_Log_Filter_Priority(4));
190         Tinebase_Core::getLogger()->addWriter($writer);
191         
192         $users = $this->_readCalDavUserFile($args['caldavuserfile']);
193         
194         $client = new Calendar_Import_CalDav_Client(array('baseUri' => $args['url']), 'MacOSX');
195         $client->setVerifyPeer(false);
196         
197         $client->importAllCalendarDataForUsers($users);
198     }
199     
200     /**
201      * import calendars and calendar events from a CalDav source using multiple parallel processes
202      * 
203      * param Zend_Console_Getopt $_opts
204      */
205     public function importCalDavMultiProc(Zend_Console_Getopt $_opts)
206     {
207         $this->_runImportUpdateMultiproc($_opts, 'import');
208     }
209     
210     protected function _runImportUpdateMultiproc(Zend_Console_Getopt $_opts, $mode)
211     {
212         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile', 'numProc'));
213         
214         $numProc = intval($args['numProc']);
215         $this->_validateNumProc($numProc);
216         
217         if (empty($_opts->passwordfile)) {
218             throw new Exception('Passwordfile required for this method');
219         }
220         
221         $this->_addOutputLogWriter(4);
222         
223         $users = $this->_readCalDavUserFile($args['caldavuserfile']);
224         
225         if ($mode === 'import') {
226             // first import the calendars, serial sadly
227             $this->_importAllCalendars($users, $args['url']);
228         }
229         
230         $cliParams = '--username ' . $_opts->username . ' --passwordfile ' . $_opts->passwordfile
231         . ' --method Calendar.' . $mode . 'CalDavDataForUser'
232                 . ' url=' .  $args['url'] . ' caldavuserfile=' . $args['caldavuserfile'];
233         
234         $numberOfRuns = 2;
235         for ($run = 1; $run <= $numberOfRuns; ++$run)
236         {
237             $this->_runMultiProcessImportUpdate($numProc, $cliParams, $users, $run);
238         }
239     }
240     
241     protected function _validateNumProc($numProc)
242     {
243         if ($numProc < 1) {
244             throw new Exception('numProc: ' . $numProc . ' needs to be at least 1');
245         }
246         if ($numProc > 32) {
247             throw new Exception('numProc: ' . $numProc . ' needs to be lower than 33');
248         }
249     }
250     
251     /**
252      * run multi process command
253      * 
254      * do multiprocess part, no system resources may be used as of here, 
255      * like file handles, db resources... see pcntl_fork man page!
256      * 
257      * @todo add pids to processes array to allow better control 
258      * @todo generalize and move to Tinebase_Frontend_Cli_Abstract
259      * 
260      * @param integer $numProc
261      * @param string $cliParams
262      * @param array $users
263      * @param integer $run
264      */
265     protected function _runMultiProcessImportUpdate($numProc, $cliParams, $users, $run = 1)
266     {
267         // $processes = array();
268         $processes = 0;
269         $line = 0;
270         foreach ($users as $user => $pwd)
271         {
272             ++$line;
273             //if (count($processes) >= $numProc) {
274             if ($processes >= $numProc) {
275                 $pid = pcntl_wait($status);
276     
277                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__
278                         . ' pid: ' . $pid);
279     
280                 // debug
281                 //                     echo '1' . (int) pcntl_wexitstatus($status);
282                 //                     echo '2' . (int) pcntl_wifexited($status);
283                 //                     echo '3' . (int) pcntl_wifsignaled($status);
284                 //                     echo '4' . (int) pcntl_wifstopped($status);
285                 //                     echo '5' . (int) pcntl_wstopsig($status);
286                 //                     echo '6' . (int) pcntl_wtermsig($status);
287     
288                 if (pcntl_wifexited($status) === false ) {
289                     exit('pcntl_wait return value was not found in process list: ' . $pid . ' status: ' . $status . PHP_EOL);
290                 }
291                 
292                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__
293                     . ' Child exited');
294                 
295                 // unset($processes[$pid]);
296                 --$processes;
297             }
298     
299             $pid = pcntl_fork();
300             if ($pid == -1) {
301                 exit('could not fork' . PHP_EOL);
302             } else if ($pid) {
303                 // we are the parent
304                 // $processes[$pid] = true;
305                 ++$processes;
306             } else {
307                 // we are the child
308                 $command = './tine20.php ' . $cliParams . ' run=' . $run . ' line=' . $line;
309                 
310                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__
311                      . ' Spawning new child with command: ' . $command);
312                 
313                 exec($command);
314                 exit();
315             }
316         }
317         // wait for childs to finish
318         // while (count($processes) > 0) {
319         while ($processes > 0) {
320             $pid = pcntl_wait($status);
321             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__
322                     . ' pid: ' . $pid);
323     
324             if (pcntl_wifexited($status) === false) {
325                 exit('pcntl_wait return value was not found in process list: ' . $pid . ' status: ' . $status . PHP_EOL);
326             }
327             // unset($processes[$pid]);
328             --$processes;
329         }
330     }
331     
332     /**
333      * update calendar events from a CalDav source using multiple parallel processes
334      * 
335      * param Zend_Console_Getopt $_opts
336      */
337     public function updateCalDavMultiProc(Zend_Console_Getopt $_opts)
338     {
339         $this->_runImportUpdateMultiproc($_opts, 'update');
340     }
341     
342     /**
343      * import calendar events from a CalDav source for one user
344      * 
345      * param Zend_Console_Getopt $_opts
346      */
347     public function importCalDavDataForUser(Zend_Console_Getopt $_opts)
348     {
349         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile', 'line', 'run'));
350         
351         $writer = new Zend_Log_Writer_Stream('php://output');
352         $writer->addFilter(new Zend_Log_Filter_Priority(4));
353         Tinebase_Core::getLogger()->addWriter($writer);
354         
355         $user = $this->_readCalDavUserFile($args['caldavuserfile'], $args['line']);
356
357         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
358             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' user: ' . $user['username']);
359         
360         $client = new Calendar_Import_CalDav_Client(array(
361                 'baseUri' => $args['url'],
362                 'userName' => $user['username'],
363                 'password' => $user['password'],
364                 ), 'MacOSX');
365         $client->setVerifyPeer(false);
366         
367         $client->importAllCalendarData($args['run']==1 ? true : false);
368     }
369     
370     /**
371      * update calendar/events from a CalDav source using etags for one user
372      * 
373      * @param Zend_Console_Getopt $_opts
374      */
375     public function updateCalDavDataForUser(Zend_Console_Getopt $_opts)
376     {
377         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile', 'line', 'run'));
378         
379         $writer = new Zend_Log_Writer_Stream('php://output');
380         $writer->addFilter(new Zend_Log_Filter_Priority(4));
381         Tinebase_Core::getLogger()->addWriter($writer);
382         
383         $user = $this->_readCalDavUserFile($args['caldavuserfile'], $args['line']);
384         
385         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
386             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' user: ' . $user['username']);
387         
388         $client = new Calendar_Import_CalDav_Client(array(
389                 'baseUri' => $args['url'],
390                 'userName' => $user['username'],
391                 'password' => $user['password'],
392                 ), 'MacOSX');
393         $client->setVerifyPeer(false);
394         
395         $client->updateAllCalendarData($args['run']==1 ? true : false);
396     }
397    
398     /**
399      * update calendar/events from a CalDav source using etags
400      * 
401      * param Zend_Console_Getopt $_opts
402      */
403     public function updateCalDavData(Zend_Console_Getopt $_opts)
404     {
405         $args = $this->_parseArgs($_opts, array('url', 'caldavuserfile'));
406         
407         $writer = new Zend_Log_Writer_Stream('php://output');
408         $writer->addFilter(new Zend_Log_Filter_Priority(4));
409         Tinebase_Core::getLogger()->addWriter($writer);
410         
411         $users = $this->_readCalDavUserFile($args['caldavuserfile']);
412         
413         $client = new Calendar_Import_CalDav_Client(array('baseUri' => $args['url']), 'MacOSX');
414         $client->setVerifyPeer(false);
415         
416         $client->updateAllCalendarDataForUsers($users);
417     }
418     
419     /**
420      * read caldav user credentials file
421      * 
422      * - file should have the following format (CSV):
423      * USERNAME1;PASSWORD1
424      * USERNAME2;PASSWORD2
425      * 
426      * @param string $file
427      * @throws Exception
428      */
429     protected function _readCalDavUserFile($file, $line = 0)
430     {
431         if (!($fh = fopen($file, 'r'))) {
432             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Couldn\'t open file: '.$file);
433             throw new Exception('Couldn\'t open file: '.$file);
434         }
435         $users = array();
436         $i = 0;
437         while ($row = fgetcsv($fh, 2048, ';'))
438         {
439             if ($line > 0 && ++$i == $line) {
440                 return array('username' => $row[0], 'password' => $row[1]);
441             }
442             $users[$row[0]] = $row[1];
443         }
444         if (count($users) < 1) {
445             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' No users found in: '.$file);
446             throw new Exception('No users found in: '.$file);
447         }
448         if ($line > 0) {
449             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Line: '.$line. ' out of bounds');
450             throw new Exception('No user found, line: '.$line. ' out of bounds');
451         }
452         return $users;
453     }
454 }