Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Calendar / Setup / Update / Release8.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Calendar
6  * @subpackage  Setup
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL3
8  * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius WeiƟ <c.weiss@metaways.de>
10  */
11 class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
12 {
13     /**
14      * update to 8.1
15      * - move ack & snooze time from attendee to alarm
16      */
17     public function update_0()
18     {
19         // find all events with ack or snooze times set
20         $eventIds = $this->_db->query(
21             "SELECT DISTINCT " . $this->_db->quoteIdentifier('cal_event_id') .
22                 " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
23                 " WHERE " . $this->_db->quoteIdentifier("alarm_ack_time") . " IS NOT NULL OR ". $this->_db->quoteIdentifier("alarm_snooze_time") . " IS NOT NULL"
24             )->fetchAll(Zend_Db::FETCH_ASSOC);
25         
26         $attendeeBE = new Calendar_Backend_Sql_Attendee();
27         $alarmBE = Tinebase_Alarm::getInstance();
28         
29         foreach ($eventIds as $eventId) {
30             $eventId = $eventId['cal_event_id'];
31             
32             $attendeeFilter = new Tinebase_Model_Filter_FilterGroup();
33             $attendeeFilter->addFilter(new Tinebase_Model_Filter_Text('cal_event_id', 'equals', $eventId));
34             $attendees = $attendeeBE->search($attendeeFilter);
35             
36             $alarms = $alarmBE->search(new Tinebase_Model_AlarmFilter(array(
37                 array('field' => 'model',     'operator' => 'equals', 'value' =>'Calendar_Model_Event'),
38                 array('field' => 'record_id', 'operator' => 'equals', 'value' => $eventId)
39             )));
40             
41             foreach ($alarms as $alarm) {
42                 foreach ($attendees as $attendee) {
43                     if ($attendee->alarm_ack_time instanceof Tinebase_DateTime) {
44                         $alarm->setOption("acknowledged-{$attendee->user_id}", $attendee->alarm_ack_time->format(Tinebase_Record_Abstract::ISO8601LONG));
45                     }
46                     if ($attendee->alarm_snooze_time instanceof Tinebase_DateTime) {
47                         $alarm->setOption("snoozed-{$attendee->user_id}", $attendee->alarm_snooze_time->format(Tinebase_Record_Abstract::ISO8601LONG));
48                     }
49                     
50                 }
51                 $alarmBE->update($alarm);
52             }
53         }
54         
55         // delte ack & snooze from attendee
56         $this->_backend->dropCol('cal_attendee', 'alarm_ack_time');
57         $this->_backend->dropCol('cal_attendee', 'alarm_snooze_time');
58         
59         $this->setTableVersion('cal_attendee', 5);
60         $this->setApplicationVersion('Calendar', '8.1');
61     }
62     
63     /**
64      * update to 8.2
65      * 
66      */
67     public function update_1()
68     {
69         $eventBE = new Tinebase_Backend_Sql(array(
70                 'modelName'    => 'Calendar_Model_Event',
71                 'tableName'    => 'cal_events',
72                 'modlogActive' => false
73         ));
74         // find all events without organizer
75         $eventIds = $this->_db->query(
76                 "SELECT " . $this->_db->quoteIdentifier('id') .
77                 " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
78                 " WHERE " . $this->_db->quoteIdentifier("organizer") . " IS NULL OR " .
79                             $this->_db->quoteIdentifier("organizer") . " = ''"
80         )->fetchAll(Zend_Db::FETCH_ASSOC);
81         
82         foreach ($eventIds as $eventId) {
83             $event = $eventBE->get($eventId['id']);
84             $event->organizer = (string) Tinebase_User::getInstance()->getFullUserById($event->created_by)->contact_id;
85             
86             $where  = array(
87                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $eventId),
88             );
89             
90             try {
91                 $this->_db->update(SQL_TABLE_PREFIX . "cal_events", $event->toArray(), $where);
92             } catch (Tinebase_Exception_Record_Validation $terv) {
93                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
94                     . ' Could not fix invalid event record: ' . print_r($event->toArray(), true));
95                 Tinebase_Exception::log($terv);
96             }
97         }
98         
99         // find all events CONFIDENTIAL class
100         $eventIds = $this->_db->query(
101                 "SELECT " . $this->_db->quoteIdentifier('id') .
102                 " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
103                 " WHERE " . $this->_db->quoteIdentifier("class") . " = 'CONFIDENTIAL'"
104         )->fetchAll(Zend_Db::FETCH_ASSOC);
105         
106         foreach ($eventIds as $eventId) {
107             $event = $eventBE->get($eventId['id']);
108             $class = Calendar_Model_Event::CLASS_PRIVATE;
109             $event->class = (string) $class;
110             
111             $where  = array(
112                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $eventId),
113             );
114             
115             try {
116                 $this->_db->update(SQL_TABLE_PREFIX . "cal_events", $event->toArray(), $where);
117             } catch (Tinebase_Exception_Record_Validation $terv) {
118                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
119                     . ' Could not fix invalid event record: ' . print_r($event->toArray(), true));
120                 Tinebase_Exception::log($terv);
121             }
122         }
123          $this->setApplicationVersion('Calendar', '8.2');
124     }
125     
126     /**
127      * update to 8.3
128      * - normalize all rrules
129      */
130     public function update_2()
131     {
132         // find all events with rrule
133         $eventIds = $this->_db->query(
134                 "SELECT " . $this->_db->quoteIdentifier('id') .
135                 " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
136                 " WHERE " . $this->_db->quoteIdentifier("rrule") . " IS NOT NULL"
137         )->fetchAll(Zend_Db::FETCH_ASSOC);
138         
139         // NOTE: we need a generic sql BE to circumvent calendar specific acl issues
140         $eventBE = new Tinebase_Backend_Sql(array(
141                 'modelName'    => 'Calendar_Model_Event',
142                 'tableName'    => 'cal_events',
143                 'modlogActive' => false
144         ));
145         
146         foreach ($eventIds as $eventId) {
147             $event = $eventBE->get($eventId['id']);
148             $oldRruleString = (string) $event->rrule;
149             $rrule = Calendar_Model_Rrule::getRruleFromString($oldRruleString);
150             $rrule->normalize($event);
151             
152             if ($oldRruleString != (string) $rrule) {
153                 $event->rrule = (string) $rrule;
154                 try {
155                     $eventBE->update($event);
156                 } catch (Tinebase_Exception_Record_Validation $terv) {
157                     Tinebase_Exception::log($terv, null, $event->toArray());
158                 } catch (Tinebase_Exception_UnexpectedValue $teuv) {
159                     Tinebase_Exception::log($teuv, null, $event->toArray());
160                 }
161             }
162         }
163         
164         $this->setApplicationVersion('Calendar', '8.3');
165     }
166     
167     /**
168      * update to 8.4
169      * 
170      * - adds etag column
171      */
172     public function update_3()
173     {
174         if (! $this->_backend->columnExists('etag', 'cal_events')) {
175             $declaration = new Setup_Backend_Schema_Field_Xml('
176                 <field>
177                     <name>etag</name>
178                     <type>text</type>
179                     <length>60</length>
180                 </field>');
181             $this->_backend->addCol('cal_events', $declaration);
182
183             $declaration = new Setup_Backend_Schema_Index_Xml('
184                 <index>
185                     <name>etag</name>
186                     <field>
187                         <name>etag</name>
188                     </field>
189                 </index>');
190             $this->_backend->addIndex('cal_events', $declaration);
191         }
192         
193         $this->setTableVersion('cal_events', 7);
194         $this->setApplicationVersion('Calendar', '8.4');
195     }
196     
197     /**
198      * - update import / export
199      */
200     public function update_4()
201     {
202         Setup_Controller::getInstance()->createImportExportDefinitions(Tinebase_Application::getInstance()->getApplicationByName('Calendar'));
203         $this->setTableVersion('cal_events', 7);
204         $this->setApplicationVersion('Calendar', '8.5');
205     }
206     
207     /**
208      * adds external_seq col
209      * 
210      * @see 0009890: improve external event invitation support
211      */
212     public function update_5()
213     {
214         if (! $this->_backend->columnExists('external_seq', 'cal_events')) {
215             $seqCol = '<field>
216                 <name>external_seq</name>
217                 <type>integer</type>
218                 <notnull>true</notnull>
219                 <default>0</default>
220             </field>';
221             
222             $declaration = new Setup_Backend_Schema_Field_Xml($seqCol);
223             $this->_backend->addCol('cal_events', $declaration);
224         }
225         
226         $this->setTableVersion('cal_events', '8');
227         $this->setApplicationVersion('Calendar', '8.6');
228     }
229     
230     /**
231      * add rrule index
232      * 
233      * @see 0010214: improve calendar performance / yearly base events
234      *
235      * TODO re-enable this when it is fixed for postgresql
236      * @see 0011194: only drop index if it exists
237      */
238     public function update_6()
239     {
240 //        $declaration = new Setup_Backend_Schema_Index_Xml('
241 //            <index>
242 //                <name>rrule</name>
243 //                <field>
244 //                    <name>rrule</name>
245 //                </field>
246 //            </index>');
247 //        try {
248 //            $this->_backend->addIndex('cal_events', $declaration);
249 //        } catch (Zend_Db_Statement_Exception $e) {
250 //            Tinebase_Exception::log($e);
251 //        }
252         
253         $this->setTableVersion('cal_events', '9');
254         $this->setApplicationVersion('Calendar', '8.7');
255     }
256
257     /**
258      * repair missing displaycontainer_id
259      */
260     public function update_7()
261     {
262         $allUser = $this->_db->query(
263             "SELECT " . $this->_db->quoteIdentifier('id') . "," . $this->_db->quoteIdentifier('contact_id') .
264             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "accounts") .
265             " WHERE " . $this->_db->quoteIdentifier("contact_id") . " IS NOT NULL"
266         )->fetchAll(Zend_Db::FETCH_ASSOC);
267
268         $contactUserMap = array();
269         foreach ($allUser as $id => $user) {
270             $contactUserMap[$user['contact_id']] = $user['id'];
271         }
272
273         // find all user/groupmember attendees with missing displaycontainer
274         $attendees = $this->_db->query(
275             "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
276             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
277             " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
278             "  AND " . $this->_db->quoteIdentifier("user_type") . $this->_db->quoteInto(" IN (?)", array('user', 'groupmemeber')) .
279             "  AND " . $this->_db->quoteIdentifier("user_id") . $this->_db->quoteInto(" IN (?)", array_keys($contactUserMap))
280         )->fetchAll(Zend_Db::FETCH_ASSOC);
281
282         // find all user/groupmember attendees with missing displaycontainer
283         $attendees = array_merge($attendees, $this->_db->query(
284             "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
285             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
286             " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
287             "  AND " . $this->_db->quoteIdentifier("user_type") . $this->_db->quoteInto(" IN (?)", array('resource'))
288         )->fetchAll(Zend_Db::FETCH_ASSOC));
289
290         $resources = $this->_db->query(
291             "SELECT " . $this->_db->quoteIdentifier('id') . "," . $this->_db->quoteIdentifier('container_id') .
292             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_resources")
293         )->fetchAll(Zend_Db::FETCH_ASSOC);
294
295         $resourceContainerMap = array();
296         foreach ($resources as $resource) {
297             $resourceContainerMap[$resource['id']] = $resource['container_id'];
298         }
299
300         foreach ($attendees as $attendee) {
301             //find out displaycontainer
302             if ($attendee['user_type'] != 'resource') {
303                 $userAccountId = $contactUserMap[$attendee['user_id']];
304                 try {
305                     $attendee['displaycontainerId'] = Calendar_Controller_Event::getDefaultDisplayContainerId($userAccountId);
306                 } catch (Tinebase_Exception_NotFound $tenf) {
307                     Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " Could not find user with id " . $attendee['user_id']);
308                     continue;
309                 }
310             } else {
311                 $attendee['displaycontainerId'] = $resourceContainerMap[$attendee['user_id']];
312             }
313
314             // update displaycontainer
315             $this->_db->query(
316                 "UPDATE" . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
317                 " SET " . $this->_db->quoteIdentifier("displaycontainer_id") . " = " . $this->_db->quote($attendee['displaycontainerId']) .
318                 " WHERE " . $this->_db->quoteIdentifier("user_type") . " = " . $this->_db->quote($attendee['user_type']) .
319                 "  AND " . $this->_db->quoteIdentifier("user_id") . " = " . $this->_db->quote($attendee['user_id'])
320             );
321         }
322
323         $this->setApplicationVersion('Calendar', '8.8');
324     }
325
326     /**
327      * identify base event via new base_event_id field instead of UID
328      */
329     public function update_8()
330     {
331         /* find possibly broken events
332          SELECT group_concat(id), uid, count(id) as cnt from tine20_cal_events
333              WHERE rrule IS NOT NULL
334              GROUP BY uid
335              HAVING cnt > 1;
336          */
337
338         $declaration = new Setup_Backend_Schema_Field_Xml('
339             <field>
340                 <name>base_event_id</name>
341                 <type>text</type>
342                 <length>40</length>
343             </field>');
344         $this->_backend->addCol('cal_events', $declaration);
345
346         $declaration = new Setup_Backend_Schema_Index_Xml('
347             <index>
348                 <name>base_event_id</name>
349                 <field>
350                     <name>base_event_id</name>
351                 </field>
352             </index>');
353         $this->_backend->addIndex('cal_events', $declaration);
354
355         // find all events with rrule
356         $events = $this->_db->query(
357             "SELECT " . $this->_db->quoteIdentifier('id') .
358                  ', ' . $this->_db->quoteIdentifier('uid') .
359                  ', ' . $this->_db->quoteIdentifier('container_id') .
360                  ', ' . $this->_db->quoteIdentifier('created_by') .
361             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
362             " WHERE " . $this->_db->quoteIdentifier("rrule") . " IS NOT NULL" .
363               " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
364         )->fetchAll(Zend_Db::FETCH_ASSOC);
365
366         // update all exdates in same container
367         foreach($events as $event) {
368             $this->_db->query(
369                 "UPDATE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
370                   " SET " . $this->_db->quoteIdentifier('base_event_id') . ' = ' . $this->_db->quote($event['id']) .
371                 " WHERE " . $this->_db->quoteIdentifier('uid') . ' = ' . $this->_db->quote($event['uid']) .
372                   " AND " . $this->_db->quoteIdentifier("container_id") . ' = ' . $this->_db->quote($event['container_id']) .
373                   " AND " . $this->_db->quoteIdentifier("recurid") . " IS NOT NULL" .
374                   " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
375             );
376         }
377
378         // find all container move exdates
379         $danglingExdates = $this->_db->query(
380             "SELECT " . $this->_db->quoteIdentifier('uid') .
381                 ', ' . $this->_db->quoteIdentifier('id') .
382                 ', ' . $this->_db->quoteIdentifier('created_by') .
383             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
384             " WHERE " . $this->_db->quoteIdentifier("recurid") . " IS NOT NULL" .
385               " AND " . $this->_db->quoteIdentifier("base_event_id") . " IS NULL" .
386               " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
387         )->fetchAll(Zend_Db::FETCH_ASSOC);
388
389         // try to match by creator
390         foreach ($danglingExdates as $exdate) {
391             $possibleBaseEvents = array();
392             $matches = array_filter($events, function ($event) use ($exdate, $possibleBaseEvents) {
393                 if ($event['uid'] == $exdate['uid']) {
394                     $possibleBaseEvents[] = $event;
395                     return $event['created_by'] == $exdate['created_by'];
396                 }
397                 return false;
398             });
399
400             switch(count($matches)) {
401                 case 0:
402                     // no match :-(
403                     if (count($possibleBaseEvents) == 0) {
404                         // garbage? exdate without any base event
405                         Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " dangling exdate with id {$exdate['id']}");
406                         continue 2;
407                     }
408                     Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " no match for exdate with id {$exdate['id']}");
409                     $baseEvent = current($possibleBaseEvents);
410                     break;
411                 case 1:
412                     // exact match :-)
413                     $baseEvent = current($matches);
414                     break;
415                 default:
416                     // to much matches :-(
417                     Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " multiple matches for exdate with id {$exdate['id']}");
418                     $baseEvent = current($matches);
419             }
420
421             $this->_db->query(
422                 "UPDATE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
423                 " SET " . $this->_db->quoteIdentifier('base_event_id') . ' = ' . $this->_db->quote($baseEvent['id']) .
424                 " WHERE " . $this->_db->quoteIdentifier('id') . ' = ' . $this->_db->quote($exdate['id'])
425             );
426         }
427
428         $this->setTableVersion('cal_events', '10');
429         $this->setApplicationVersion('Calendar', '8.9');
430     }
431
432     /**
433      * @see 0011266: increase size of event fields summary and location
434      */
435     public function update_9()
436     {
437         $fieldsToChange = array('location', 'summary');
438
439         foreach ($fieldsToChange as $name) {
440             $seqCol = '<field>
441                 <name>' . $name . '</name>
442                 <type>text</type>
443                 <length>1024</length>
444             </field>';
445
446             $declaration = new Setup_Backend_Schema_Field_Xml($seqCol);
447             $this->_backend->alterCol('cal_events', $declaration);
448         }
449
450         $this->setTableVersion('cal_events', 11);
451         $this->setApplicationVersion('Calendar', '8.10');
452     }
453
454     /**
455      * force activesync calendar resync for iOS devices
456      */
457     public function update_10()
458     {
459         $deviceBackend = new ActiveSync_Backend_Device();
460         $usersWithiPhones = $deviceBackend->search(new ActiveSync_Model_DeviceFilter(array(
461             'devicetype' => 'iphone'
462         )))->owner_id;
463
464         $activeSyncController = ActiveSync_Controller::getInstance();
465         foreach($usersWithiPhones as $userId) {
466             try {
467                 $activeSyncController->resetSyncForUser($userId, 'Calendar');
468             } catch (Exception $e) {
469                 Tinebase_Exception::log($e, /* suppress trace */ false);
470             }
471         }
472
473         $this->setApplicationVersion('Calendar', '8.11');
474     }
475 }