Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None
  • PHP

Classes

  • WritableCollection

Functions

  • bits_to_privilege
  • ConstructURL
  • DateToISODate
  • DeconstructURL
  • early_catch_fatal_error
  • early_exception_handler
  • getStatusMessage
  • ISODateToHTTPDate
  • privilege_to_bits
  • privileges_to_XML
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: include_once('DAVResource.php');
  3: 
  4: class WritableCollection extends DAVResource {
  5: 
  6:   /**
  7:    * Get a TZID string from this VEVENT/VTODO/... component if we can
  8:    * @param vComponent $comp
  9:    * @return The TZID value we found, or null
 10:    */
 11:   private static function GetTZID( vComponent $comp ) {
 12:     $p = $comp->GetProperty('DTSTART');
 13:     if ( !isset($p) && $comp->GetType() == 'VTODO' ) {
 14:       $p = $comp->GetProperty('DUE');
 15:     }
 16:     if ( !isset($p) ) return null;
 17:     return $p->GetParameterValue('TZID');
 18:   }
 19: 
 20:   /**
 21:    * Writes the data to a member in the collection and returns the segment_name of the
 22:    * resource in our internal namespace.
 23:    *
 24:    * @param vCalendar $vcal The resource to be written.
 25:    * @param boolean $create_resource True if this is a new resource.
 26:    * @param boolean $do_scheduling True if we should also do scheduling for this write. Default false.
 27:    * @param string $segment_name The name of the resource within the collection, or null if this
 28:    *                             call should invent one based on the UID of the vCalendar.
 29:    * @param boolean $log_action Whether to log this action.  Defaults to false since this is normally called
 30:    *                             in situations where one is writing secondary data.
 31:    * @return string The segment_name of the resource within the collection, as written, or false on failure.
 32:    */
 33:   function WriteCalendarMember( vCalendar $vcal, $create_resource, $do_scheduling=false, $segment_name = null, $log_action=false ) {
 34:     if ( !$this->IsSchedulingCollection() && !$this->IsCalendar() ) {
 35:       dbg_error_log( 'PUT', '"%s" is not a calendar or scheduling collection!', $this->dav_name);
 36:       return false;
 37:     }
 38: 
 39:     global $session, $caldav_context;
 40: 
 41:     $resources = $vcal->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE
 42:     $user_no = $this->user_no();
 43:     $collection_id = $this->collection_id();
 44: 
 45:     if ( !isset($resources[0]) ) {
 46:       dbg_error_log( 'PUT', 'No calendar content!');
 47:       rollback_on_error( $caldav_context, $user_no, $this->dav_name.'/'.$segment_name, translate('No calendar content'), 412 );
 48:       return false;
 49:     }
 50:     else {
 51:       $first = $resources[0];
 52:       $resource_type = $first->GetType();
 53:     }
 54: 
 55:     $uid = $vcal->GetUID();
 56:     if ( empty($segment_name) ) {
 57:       $segment_name = $uid.'.ics';
 58:     }
 59:     $path = $this->dav_name() . $segment_name;
 60: 
 61:     $caldav_data = $vcal->Render();
 62:     $etag = md5($caldav_data);
 63:     $weak_etag = null;
 64: 
 65:     $qry = new AwlQuery();
 66:     $existing_transaction_state = $qry->TransactionState();
 67:     if ( $existing_transaction_state == 0 ) $qry->Begin();
 68: 
 69: 
 70:     if ( $create_resource ) {
 71:       $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id');
 72:     }
 73:     else {
 74:       $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path));
 75:     }
 76:     if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) {
 77:       if ( !$create_resource ) {
 78:         // Looks like we will have to create it, even if the caller thought we wouldn't
 79:         $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id');
 80:         if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) {
 81:           // No dav_id?  => We're toast!
 82:           trace_bug( 'No dav_id for "%s" on %s!!!', $path, ($create_resource ? 'create': 'update'));
 83:           rollback_on_error( $caldav_context, $user_no, $path);
 84:           return false;
 85:         }
 86:         $create_resource = true;
 87:         dbg_error_log( 'PUT', 'Unexpected need to create resource at "%s"', $path);
 88:       }
 89:     }
 90:     $dav_id = $row->dav_id;
 91: 
 92:     $calitem_params = array(
 93:         ':dav_name' => $path,
 94:         ':user_no' => $user_no,
 95:         ':etag' => $etag,
 96:         ':dav_id' => $dav_id
 97:     );
 98: 
 99:     $dav_params = array_merge($calitem_params, array(
100:         ':dav_data' => $caldav_data,
101:         ':caldav_type' => $resource_type,
102:         ':session_user' => $session->user_no,
103:         ':weak_etag' => $weak_etag
104:     ) );
105: 
106:     if ( !$this->IsSchedulingCollection() && $do_scheduling ) {
107:       if ( do_scheduling_requests($vcal, $create_resource ) ) {
108:         $dav_params[':dav_data'] = $vcal->Render(null, true);
109:         $etag = null;
110:       }
111:     }
112: 
113:     if ( $create_resource ) {
114:       $sql = 'INSERT INTO caldav_data ( dav_id, user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag )
115:               VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, current_timestamp, current_timestamp, :collection_id, :weak_etag )';
116:       $dav_params[':collection_id'] = $collection_id;
117:     }
118:     else {
119:       $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
120:               modified=current_timestamp, weak_etag=:weak_etag WHERE dav_id=:dav_id';
121:     }
122:     if ( !$qry->QDo($sql,$dav_params) ) {
123:       rollback_on_error( $caldav_context, $user_no, $path);
124:       return false;
125:     }
126: 
127:     $dtstart = $first->GetPValue('DTSTART');
128:     $calitem_params[':dtstart'] = $dtstart;
129:     if ( (!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '' ) {
130:       $dtstart = $first->GetPValue('DUE');
131:     }
132: 
133:     $dtend = $first->GetPValue('DTEND');
134:     if ( isset($dtend) && $dtend != '' ) {
135:       dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') );
136:       $calitem_params[':dtend'] = $dtend;
137:       $dtend = ':dtend';
138:     }
139:     else {
140:       $dtend = 'NULL';
141:       if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) {
142:         $duration = preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') );
143:         $dtend = '(:dtstart::timestamp with time zone + :duration::interval)';
144:         $calitem_params[':duration'] = $duration;
145:       }
146:       elseif ( $first->GetType() == 'VEVENT' ) {
147:         /**
148:         * From RFC2445 4.6.1:
149:         * For cases where a "VEVENT" calendar component specifies a "DTSTART"
150:         * property with a DATE data type but no "DTEND" property, the events
151:         * non-inclusive end is the end of the calendar date specified by the
152:         * "DTSTART" property. For cases where a "VEVENT" calendar component specifies
153:         * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property,
154:         * the event ends on the same calendar date and time of day specified by the
155:         * "DTSTART" property.
156:         *
157:         * So we're looking for 'VALUE=DATE', to identify the duration, effectively.
158:         *
159:         */
160:         $value_type = $first->GetProperty('DTSTART')->GetParameterValue('VALUE');
161:         dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type );
162:         if ( isset($value_type) && $value_type == 'DATE' )
163:           $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)';
164:         else
165:           $dtend = ':dtstart';
166:       }
167:     }
168: 
169:     $last_modified = $first->GetPValue('LAST-MODIFIED');
170:     if ( !isset($last_modified) || $last_modified == '' ) {
171:       $last_modified = gmdate( 'Ymd\THis\Z' );
172:     }
173:     $calitem_params[':modified'] = $last_modified;
174: 
175:     $dtstamp = $first->GetPValue('DTSTAMP');
176:     if ( !isset($dtstamp) || $dtstamp == '' ) {
177:       $dtstamp = $last_modified;
178:     }
179:     $calitem_params[':dtstamp'] = $dtstamp;
180: 
181:     $class = $first->GetPValue('CLASS');
182:     /*
183:      * It seems that some calendar clients don't set a class...
184:      * RFC2445, 4.8.1.3: Default is PUBLIC
185:      */
186:     if ( $this->IsPublicOnly() || !isset($class) || $class == '' ) {
187:       $class = 'PUBLIC';
188:     }
189:     $calitem_params[':class'] = $class;
190: 
191:     /** Calculate what timezone to set, first, if possible */
192:     $last_olson = 'Turkmenikikamukau';  // I really hope this location doesn't exist!
193:     $tzid = self::GetTZID($first);
194:     if ( !empty($tzid) ) {
195:       $tz = $vcal->GetTimeZone($tzid);
196:       $olson = $vcal->GetOlsonName($tz);
197: 
198:       if ( !empty($olson) && ($olson != $last_olson) ) {
199:         dbg_error_log( 'PUT', ' Setting timezone to %s', $olson );
200:         $qry->QDo('SET TIMEZONE TO \''.$olson."'" );
201:         $last_olson = $olson;
202:       }
203:     }
204: 
205:     $created = $first->GetPValue('CREATED');
206:     if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z';
207:     $calitem_params[':created'] = $created;
208: 
209:     $calitem_params[':tzid'] = $tzid;
210:     $calitem_params[':uid'] = $uid;
211:     $calitem_params[':summary'] = $first->GetPValue('SUMMARY');
212:     $calitem_params[':location'] = $first->GetPValue('LOCATION');
213:     $calitem_params[':transp'] = $first->GetPValue('TRANSP');
214:     $calitem_params[':description'] = $first->GetPValue('DESCRIPTION');
215:     $calitem_params[':rrule'] = $first->GetPValue('RRULE');
216:     $calitem_params[':url'] = $first->GetPValue('URL');
217:     $calitem_params[':priority'] = $first->GetPValue('PRIORITY');
218:     $calitem_params[':due'] = $first->GetPValue('DUE');
219:     $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
220:     $calitem_params[':status'] = $first->GetPValue('STATUS');
221:     if ( $create_resource ) {
222:       $sql = <<<EOSQL
223: INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
224:                 dtstart, dtend, summary, location, class, transp,
225:                 description, rrule, tz_id, last_modified, url, priority,
226:                 created, due, percent_complete, status, collection_id )
227:    VALUES ( :user_no, :dav_name, currval('dav_id_seq'), :etag, :uid, :dtstamp,
228:                 :dtstart, $dtend, :summary, :location, :class, :transp,
229:                 :description, :rrule, :tzid, :modified, :url, :priority,
230:                 :created, :due, :percent_complete, :status, $collection_id )
231: EOSQL;
232:       $sync_change = 201;
233:     }
234:     else {
235:       $sql = <<<EOSQL
236: UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
237:                 dtstart=:dtstart, dtend=$dtend, summary=:summary, location=:location, class=:class, transp=:transp,
238:                 description=:description, rrule=:rrule, tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority,
239:                 created=:created, due=:due, percent_complete=:percent_complete, status=:status
240:        WHERE user_no=:user_no AND dav_name=:dav_name
241: EOSQL;
242:       $sync_change = 200;
243:     }
244: 
245:     if ( !$this->IsSchedulingCollection() ) {
246:       $this->WriteCalendarAlarms($dav_id, $vcal);
247:       $this->WriteCalendarAttendees($dav_id, $vcal);
248:       $put_action_type = ($create_resource ? 'INSERT' : 'UPDATE');
249:       if ( $log_action && function_exists('log_caldav_action') ) {
250:         log_caldav_action( $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path );
251:       }
252:       else if ( $log_action  ) {
253:         dbg_error_log( 'PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.',
254:                 $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path );
255:       }
256:     }
257: 
258:     $qry = new AwlQuery( $sql, $calitem_params );
259:     if ( !$qry->Exec('PUT',__LINE__,__FILE__) ) {
260:       rollback_on_error( $caldav_context, $user_no, $path);
261:       return false;
262:     }
263:     $qry->QDo("SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(':dav_name' => $path ) );
264:     if ( $existing_transaction_state == 0 ) $qry->Commit();
265: 
266:     dbg_error_log( 'PUT', 'User: %d, ETag: %s, Path: %s', $session->user_no, $etag, $path);
267: 
268: 
269:     return $segment_name;
270:   }
271: 
272:   /**
273:    * Writes the data to a member in the collection and returns the segment_name of the
274:    * resource in our internal namespace.
275:    *
276:    * A caller who wants scheduling not to happen for this write must already
277:    * know they are dealing with a calendar, so should be calling WriteCalendarMember
278:    * directly.
279:    *
280:    * @param $resource mixed The resource to be written.
281:    * @param $create_resource boolean True if this is a new resource.
282:    * @param $segment_name The name of the resource within the collection, or false on failure.
283:    * @param boolean $log_action Whether to log this action.  Defaults to true since this is normally called
284:    *                             in situations where one is writing primary data.
285:    * @return string The segment_name that was given, or one that was assigned if null was given.
286:    */
287:   function WriteMember( $resource, $create_resource, $segment_name = null, $log_action=true ) {
288:     if ( ! $this->IsCollection() ) {
289:       dbg_error_log( 'PUT', '"%s" is not a collection path', $this->dav_name);
290:       return false;
291:     }
292:     if ( ! is_object($resource) ) {
293:       dbg_error_log( 'PUT', 'No data supplied!' );
294:       return false;
295:     }
296: 
297:     if ( $resource instanceof vCalendar ) {
298:       return $this->WriteCalendarMember($resource,$create_resource,true,$segment_name,$log_action);
299:     }
300:     else if ( $resource instanceof VCard )
301:       trace_bug( "Calling undefined function WriteAddressbookMember!? Please report this to the davical project: davical-general@lists.sourceforge.net" );
302:       return $this->WriteAddressbookMember($resource,$create_resource,$segment_name, $log_action);
303: 
304:     return $segment_name;
305:   }
306: 
307: 
308:   /**
309:   * Given a dav_id and an original vCalendar, pull out each of the VALARMs
310:   * and write the values into the calendar_alarm table.
311:   *
312:   * @return null
313:   */
314:   function WriteCalendarAlarms( $dav_id, vCalendar $vcal ) {
315:     $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = '.$dav_id );
316:     $qry->Exec('PUT',__LINE__,__FILE__);
317: 
318:     $components = $vcal->GetComponents();
319: 
320:     $qry->SetSql('INSERT INTO calendar_alarm ( dav_id, action, trigger, summary, description, component, next_trigger )
321:             VALUES( '.$dav_id.', :action, :trigger, :summary, :description, :component,
322:                                         :related::timestamp with time zone + :related_trigger::interval )' );
323:     $qry->Prepare();
324:     foreach( $components AS $component ) {
325:       if ( $component->GetType() == 'VTIMEZONE' ) continue;
326:       $alarms = $component->GetComponents('VALARM');
327:       if ( count($alarms) < 1 ) return;
328: 
329:       foreach( $alarms AS $v ) {
330:         $trigger = array_merge($v->GetProperties('TRIGGER'));
331:         if ( $trigger == null ) continue; // Bogus data.
332:         $trigger = $trigger[0];
333:         $related = null;
334:         $related_trigger = '0M';
335:         $trigger_type = $trigger->GetParameterValue('VALUE');
336:         if ( !isset($trigger_type) || $trigger_type == 'DURATION' ) {
337:           switch ( $trigger->GetParameterValue('RELATED') ) {
338:             case 'DTEND':  $related = $component->GetPValue('DTEND'); break;
339:             case 'DUE':    $related = $component->GetPValue('DUE');   break;
340:             default:       $related = $component->GetPValue('DTSTART');
341:           }
342:           $duration = $trigger->Value();
343:           if ( !preg_match('{^-?P(:?\d+W)?(:?\d+D)?(:?T(:?\d+H)?(:?\d+M)?(:?\d+S)?)?$}', $duration ) ) continue;
344:           $minus = (substr($duration,0,1) == '-');
345:           $related_trigger = trim(preg_replace( '#[PT-]#', ' ', $duration ));
346:           if ( $minus ) {
347:             $related_trigger = preg_replace( '{(\d+[WDHMS])}', '-$1 ', $related_trigger );
348:           }
349:           else {
350:             $related_trigger = preg_replace( '{(\d+[WDHMS])}', '$1 ', $related_trigger );
351:           }
352:         }
353:         else {
354:           if ( false === strtotime($trigger->Value()) ) continue; // Invalid date.
355:         }
356:         $qry->Bind(':action', $v->GetPValue('ACTION'));
357:         $qry->Bind(':trigger', $trigger->Render());
358:         $qry->Bind(':summary', $v->GetPValue('SUMMARY'));
359:         $qry->Bind(':description', $v->GetPValue('DESCRIPTION'));
360:         $qry->Bind(':component', $v->Render());
361:         $qry->Bind(':related', $related );
362:         $qry->Bind(':related_trigger', $related_trigger );
363:         $qry->Exec('PUT',__LINE__,__FILE__);
364:       }
365:     }
366:   }
367: 
368: 
369:   /**
370:    * Parse out the attendee property and write a row to the
371:    * calendar_attendee table for each one.
372:    * @param int $dav_id The dav_id of the caldav_data we're processing
373:    * @param vComponent The VEVENT or VTODO containing the ATTENDEEs
374:    * @return null
375:    */
376:   function WriteCalendarAttendees( $dav_id, vCalendar $vcal ) {
377:     $qry = new AwlQuery('DELETE FROM calendar_attendee WHERE dav_id = '.$dav_id );
378:     $qry->Exec('PUT',__LINE__,__FILE__);
379: 
380:     $attendees = $vcal->GetAttendees();
381:     if ( count($attendees) < 1 ) return;
382: 
383:     $qry->SetSql('INSERT INTO calendar_attendee ( dav_id, status, partstat, cn, attendee, role, rsvp, property )
384:             VALUES( '.$dav_id.', :status, :partstat, :cn, :attendee, :role, :rsvp, :property )' );
385:     $qry->Prepare();
386:     $processed = array();
387:     foreach( $attendees AS $v ) {
388:       $attendee = $v->Value();
389:       if ( isset($processed[$attendee]) ) {
390:         dbg_error_log( 'LOG', 'Duplicate attendee "%s" in resource "%d"', $attendee, $dav_id );
391:         dbg_error_log( 'LOG', 'Original:  "%s"', $processed[$attendee] );
392:         dbg_error_log( 'LOG', 'Duplicate: "%s"', $v->Render() );
393:         continue; /** @todo work out why we get duplicate ATTENDEE on one VEVENT */
394:       }
395:       $qry->Bind(':attendee', $attendee );
396:       $qry->Bind(':status',   $v->GetParameterValue('STATUS') );
397:       $qry->Bind(':partstat', $v->GetParameterValue('PARTSTAT') );
398:       $qry->Bind(':cn',       $v->GetParameterValue('CN') );
399:       $qry->Bind(':role',     $v->GetParameterValue('ROLE') );
400:       $qry->Bind(':rsvp',     $v->GetParameterValue('RSVP') );
401:       $qry->Bind(':property', $v->Render() );
402:       $qry->Exec('PUT',__LINE__,__FILE__);
403:       $processed[$attendee] = $v->Render();
404:     }
405:   }
406: 
407:   /**
408:    * Writes the data to a member in the collection and returns the segment_name of the
409:    * resource in our internal namespace.
410:    *
411:    * @param vCalendar $member_dav_name The path to the resource to be deleted.
412:    * @return boolean Success is true, or false on failure.
413:    */
414:   function actualDeleteCalendarMember( $member_dav_name ) {
415:     global $session, $caldav_context;
416: 
417:     // A quick sanity check...
418:     $segment_name = str_replace( $this->dav_name(), '', $member_dav_name );
419:     if ( strstr($segment_name, '/') !== false ) {
420:       @dbg_error_log( "DELETE", "DELETE: Refused to delete member '%s' from calendar '%s'!", $member_dav_name, $this->dav_name() );
421:       return false;
422:     }
423: 
424:     // We need to serialise access to this process just for this collection
425:     $cache = getCacheInstance();
426:     $myLock = $cache->acquireLock('collection-'.$this->dav_name());
427: 
428:     $qry = new AwlQuery();
429:     $params = array( ':dav_name' => $member_dav_name );
430: 
431:     if ( $qry->QDo("SELECT write_sync_change(collection_id, 404, caldav_data.dav_name) FROM caldav_data WHERE dav_name = :dav_name", $params )
432:                     && $qry->QDo("DELETE FROM property WHERE dav_name = :dav_name", $params )
433:                     && $qry->QDo("DELETE FROM locks WHERE dav_name = :dav_name", $params )
434:                     && $qry->QDo("DELETE FROM caldav_data WHERE dav_name = :dav_name", $params ) ) {
435:       @dbg_error_log( "DELETE", "DELETE: Calendar member %s deleted from calendar '%s'", $member_dav_name, $this->dav_name() );
436: 
437:       $cache->releaseLock($myLock);
438: 
439:       return true;
440:     }
441: 
442:     $cache->releaseLock($myLock);
443:     return false;
444: 
445:   }
446: 
447: 
448:   /**
449:    *
450:    * @param unknown_type $some_old_token
451:    */
452:   public function whatChangedSince( $some_old_token ) {
453:     $params = array( ':collection_id' => $this->collection_id() );
454:     if ( $some_old_token == 0 || empty($some_old_token) ) {
455:       $sql = <<<EOSQL
456:   SELECT calendar_item.*, caldav_data.*, addressbook_resource.*, 201 AS sync_status,
457:          COALESCE(addressbook_resource.uid,calendar_item.uid) AS uid
458:       FROM caldav_data
459:       LEFT JOIN calendar_item USING (dav_id)
460:       LEFT JOIN addressbook_resource USING (dav_id)
461:       WHERE caldav_data.collection_id = :collection_id
462:       ORDER BY caldav_data.collection_id, caldav_data.dav_id
463: EOSQL;
464:     }
465:     else {
466:       $params[':sync_token'] = $some_old_token;
467:       $sql = <<<EOSQL
468:   SELECT calendar_item.*, caldav_data.*, addressbook_resource.*, sync_changes.*,
469:          COALESCE(addressbook_resource.uid,calendar_item.uid) AS uid
470:       FROM sync_changes
471:       LEFT JOIN caldav_data USING (collection_id,dav_id)
472:       LEFT JOIN calendar_item USING (collection_id,dav_id)
473:       LEFT JOIN addressbook_resource USING (dav_id)
474:       WHERE sync_changes.collection_id = :collection_id
475:             AND sync_time >= (SELECT modification_time FROM sync_tokens WHERE sync_token = :sync_token)
476:       ORDER BY sync_changes.collection_id, sync_changes.dav_id, sync_changes.sync_time
477: EOSQL;
478: 
479:     }
480:     $qry = new AwlQuery($sql, $params );
481: 
482:     $changes = array();
483:     if ( $qry->Exec('WritableCollection') && $qry->rows() ) {
484:       while( $change = $qry->Fetch() ) {
485:         $changes[$change->uid] = $change;
486:       }
487:     }
488: 
489:     return $changes;
490:   }
491: }
492: 
DAViCal API documentation generated by ApiGen 2.8.0