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

Functions

  • add_failure
  • check_for_expansion
  • component_to_xml
  • controlRequestContainer
  • delete_collection
  • deliverItipCancel
  • display_status
  • do_scheduling_for_delete
  • do_scheduling_reply
  • do_scheduling_requests
  • doImipMessage
  • doItipAttendeeReply
  • doItipOrganizerCancel
  • export_iCalendar
  • GetItip
  • GetTZID
  • handle_cancel_request
  • handle_freebusy_request
  • handle_schedule_reply
  • handle_schedule_request
  • import_addressbook_collection
  • import_calendar_collection
  • import_collection
  • late_catch_fatal_error
  • logRequestHeaders
  • obfuscated_event
  • process_ace
  • processItipCancel
  • property_response
  • public_events_only
  • rollback
  • rollback_on_error
  • send_dav_header
  • simple_write_resource
  • write_alarms
  • write_attendees
  • write_resource
  • Overview
  • Package
  • Function
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: /**
  3: * CalDAV Server - handle PROPPATCH method
  4: *
  5: * @package   davical
  6: * @subpackage   caldav
  7: * @author    Andrew McMillan <andrew@mcmillan.net.nz>
  8: * @copyright Morphoss Ltd - http://www.morphoss.com/
  9: * @license   http://gnu.org/copyleft/gpl.html GNU GPL v2
 10: */
 11: dbg_error_log("PROPPATCH", "method handler");
 12: 
 13: require_once('vCalendar.php');
 14: require_once('DAVResource.php');
 15: 
 16: $dav_resource = new DAVResource($request->path);
 17: if ( !$dav_resource->HavePrivilegeTo('DAV::write-properties') ) {
 18:   $parent = $dav_resource->GetParentContainer();
 19:   if ( !$dav_resource->IsBinding() || !$parent->HavePrivilegeTo('DAV::write') ) {
 20:     $request->PreconditionFailed(403, 'DAV::write-properties', 'You do not have permission to write properties to that resource' );
 21:   }
 22: }
 23: 
 24: $position = 0;
 25: $xmltree = BuildXMLTree( $request->xml_tags, $position);
 26: 
 27: // echo $xmltree->Render();
 28: 
 29: if ( $xmltree->GetNSTag() != "DAV::propertyupdate" ) {
 30:   $request->PreconditionFailed( 403, 'DAV::propertyupdate', 'XML request did not contain a &lt;propertyupdate&gt; tag' );
 31: }
 32: 
 33: /**
 34: * Find the properties being set, and the properties being removed
 35: */
 36: $setprops = $xmltree->GetPath("/DAV::propertyupdate/DAV::set/DAV::prop/*");
 37: $rmprops  = $xmltree->GetPath("/DAV::propertyupdate/DAV::remove/DAV::prop/*");
 38: 
 39: /**
 40: * We build full status responses for failures.  For success we just record
 41: * it, since the multistatus response only applies to failure.  While it is
 42: * not explicitly stated in RFC2518, from reading between the lines (8.2.1)
 43: * a success will return 200 OK [with an empty response].
 44: */
 45: $failure   = array();
 46: $success   = array();
 47: 
 48: $reply = new XMLDocument( array( 'DAV:' => '') );
 49: 
 50: /**
 51:  * Small utility function to add propstat for one failure
 52:  * @param unknown_type $tag
 53:  * @param unknown_type $status
 54:  * @param unknown_type $description
 55:  * @param unknown_type $error_tag
 56:  */
 57: function add_failure( $type, $tag, $status, $description=null, $error_tag = null) {
 58:   global $failure, $reply;
 59:   $prop = new XMLElement('prop');
 60:   $reply->NSElement($prop, $tag);
 61:   $propstat = array($prop,new XMLElement( 'status', $status ));
 62: 
 63:   if ( isset($description))
 64:     $propstat[] = new XMLElement( 'responsedescription', $description );
 65:   if ( isset($error_tag) )
 66:     $propstat[] = new XMLElement( 'error', new XMLElement( $error_tag ) );
 67: 
 68:   $failure[$type.'-'.$tag] = new XMLElement('propstat', $propstat );
 69: }
 70: 
 71: 
 72: /**
 73: * Not much for it but to process the incoming settings in a big loop, doing
 74: * the special-case stuff as needed and falling through to a default which
 75: * stuffs the property somewhere we will be able to retrieve it from later.
 76: */
 77: $qry = new AwlQuery();
 78: $qry->Begin();
 79: $setcalendar = count($xmltree->GetPath('/DAV::propertyupdate/DAV::set/DAV::prop/DAV::resourcetype/urn:ietf:params:xml:ns:caldav:calendar'));
 80: foreach( $setprops AS $k => $setting ) {
 81:   $tag = $setting->GetNSTag();
 82:   $content = $setting->RenderContent(0,null,true);
 83: 
 84:   switch( $tag ) {
 85: 
 86:     case 'DAV::displayname':
 87:       /**
 88:       * Can't set displayname on resources - only collections or principals
 89:       */
 90:       if ( $dav_resource->IsCollection() || $dav_resource->IsPrincipal() ) {
 91:         if ( $dav_resource->IsBinding() ) {
 92:           $qry->QDo('UPDATE dav_binding SET dav_displayname = :displayname WHERE dav_name = :dav_name',
 93:                                             array( ':displayname' => $content, ':dav_name' => $dav_resource->dav_name()) );
 94:         }
 95:         else if ( $dav_resource->IsPrincipal() ) {
 96:           $qry->QDo('UPDATE dav_principal SET fullname = :displayname, displayname = :displayname, modified = current_timestamp WHERE user_no = :user_no',
 97:                                             array( ':displayname' => $content, ':user_no' => $request->user_no) );
 98:         }
 99:         else {
100:           $qry->QDo('UPDATE collection SET dav_displayname = :displayname, modified = current_timestamp WHERE dav_name = :dav_name',
101:                                             array( ':displayname' => $content, ':dav_name' => $dav_resource->dav_name()) );
102:         }
103:         $success[$tag] = 1;
104:       }
105:       else {
106:         add_failure('set', $tag, 'HTTP/1.1 403 Forbidden',
107:              translate("The displayname may only be set on collections, principals or bindings."), 'cannot-modify-protected-property');
108:       }
109:       break;
110: 
111:     case 'DAV::resourcetype':
112:       /**
113:       * We only allow resourcetype setting on a normal collection, and not on a resource, a principal or a bind.
114:       * Only collections may be CalDAV calendars or addressbooks, and they may not be both.
115:       */
116:       $resourcetypes = $setting->GetPath('DAV::resourcetype/*');
117:       $setcollection = false;
118:       $setcalendar = false;
119:       $setaddressbook = false;
120:       $setother = false;
121:       foreach( $resourcetypes AS $xnode ) {
122:         switch( $xnode->GetNSTag() ) {
123:           case 'urn:ietf:params:xml:ns:caldav:calendar':      $setcalendar = true;      break;
124:           case 'urn:ietf:params:xml:ns:carddav:addressbook':  $setaddressbook = true;   break;
125:           case 'DAV::collection': $setcollection = true; break;
126:           default:
127:             $setother = true;
128:         }
129:       }
130:       if ( $dav_resource->IsCollection() && $setcollection && ! $dav_resource->IsPrincipal() && ! $dav_resource->IsBinding()
131:           && !($setcalendar && $setaddressbook) && !$setother ) {
132:         $resourcetypes = '<collection xmlns="DAV:"/>';
133:         if ( $setcalendar ) $resourcetypes .= '<calendar xmlns="urn:ietf:params:xml:ns:caldav"/>';
134:         else if ( $setaddressbook ) $resourcetypes .= '<addressbook xmlns="urn:ietf:params:xml:ns:carddav"/>';
135:         $qry->QDo('UPDATE collection SET is_calendar = :is_calendar::boolean, is_addressbook = :is_addressbook::boolean,
136:                      resourcetypes = :resourcetypes WHERE dav_name = :dav_name',
137:                     array( ':dav_name' => $dav_resource->dav_name(), ':resourcetypes' => $resourcetypes,
138:                            ':is_calendar' => $setcalendar, ':is_addressbook' => $setaddressbook ) );
139:         $success[$tag] = 1;
140:       }
141:       else if ( $setcalendar && $setaddressbook ) {
142:         add_failure('set', $tag, 'HTTP/1.1 409 Conflict',
143:             translate("A collection may not be both a calendar and an addressbook."));
144:       }
145:       else if ( $setother ) {
146:         add_failure('set', $tag, 'HTTP/1.1 403 Forbidden',
147:              translate("Unsupported resourcetype modification."), 'cannot-modify-protected-property');
148:       }
149:       else {
150:         add_failure('set', $tag, 'HTTP/1.1 403 Forbidden',
151:              translate("Resources may not be changed to / from collections."), 'cannot-modify-protected-property');
152:       }
153:       break;
154: 
155:     case 'urn:ietf:params:xml:ns:caldav:schedule-calendar-transp':
156:       if ( $dav_resource->IsCollection() && ( $dav_resource->IsCalendar() || $setcalendar ) && !$dav_resource->IsBinding() ) {
157:         $transparency = $setting->GetPath('urn:ietf:params:xml:ns:caldav:schedule-calendar-transp/*');
158:         $transparency = preg_replace( '{^.*:}', '', $transparency[0]->GetNSTag());
159:         $qry->QDo('UPDATE collection SET schedule_transp = :transparency WHERE dav_name = :dav_name',
160:                     array( ':dav_name' => $dav_resource->dav_name(), ':transparency' => $transparency ) );
161:         $success[$tag] = 1;
162:       }
163:       else {
164:         add_failure('set', $tag, 'HTTP/1.1 409 Conflict',
165:               translate("The CalDAV:schedule-calendar-transp property may only be set on calendars."));
166:       }
167:       break;
168: 
169:     case 'urn:ietf:params:xml:ns:caldav:calendar-free-busy-set':
170:       add_failure('set', $tag, 'HTTP/1.1 409 Conflict',
171:             translate("The calendar-free-busy-set is superseded by the  schedule-calendar-transp property of a calendar collection.") );
172:       break;
173: 
174:     case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
175:       if ( $dav_resource->IsCollection() && $dav_resource->IsCalendar() && ! $dav_resource->IsBinding() ) {
176:         $tzcomponent = $setting->GetPath('urn:ietf:params:xml:ns:caldav:calendar-timezone');
177:         $tzstring = $tzcomponent[0]->GetContent();
178:         $calendar = new vCalendar( $tzstring );
179:         $timezones = $calendar->GetComponents('VTIMEZONE');
180:         if ( count($timezones) == 0 ) break;
181:         $tz = $timezones[0];  // Backward compatibility
182:         $tzid = $tz->GetPValue('TZID');
183:         $params = array( ':tzid' => $tzid );
184:         $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params );
185:         if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) {
186:           $params[':olson_name'] = $calendar->GetOlsonName($tz);
187:           $params[':vtimezone'] = (isset($tz) ? $tz->Render() : null );
188:           $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params );
189:         }
190: 
191:         $qry->QDo('UPDATE collection SET timezone = :tzid WHERE dav_name = :dav_name',
192:                                        array( ':tzid' => $tzid, ':dav_name' => $dav_resource->dav_name()) );
193:       }
194:       else {
195:         add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("calendar-timezone property is only valid for a calendar."));
196:       }
197:       break;
198: 
199:     /**
200:     * The following properties are read-only, so they will cause the request to fail
201:     */
202:     case 'http://calendarserver.org/ns/:getctag':
203:     case 'DAV::owner':
204:     case 'DAV::principal-collection-set':
205:     case 'urn:ietf:params:xml:ns:caldav:calendar-user-address-set':
206:     case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL':
207:     case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL':
208:     case 'DAV::getetag':
209:     case 'DAV::getcontentlength':
210:     case 'DAV::getcontenttype':
211:     case 'DAV::getlastmodified':
212:     case 'DAV::creationdate':
213:     case 'DAV::lockdiscovery':
214:     case 'DAV::supportedlock':
215:       add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("Property is read-only"), new XMLElement( 'cannot-modify-protected-property'));
216:       break;
217: 
218:     /**
219:     * If we don't have any special processing for the property, we just store it verbatim (which will be an XML fragment).
220:     */
221:     default:
222:       $qry->QDo('SELECT set_dav_property( :dav_name, :user_no::integer, :tag::text, :value::text)',
223:             array( ':dav_name' => $dav_resource->dav_name(), ':user_no' => $request->user_no, ':tag' => $tag, ':value' => $content) );
224:       $success[$tag] = 1;
225:       break;
226:   }
227: }
228: 
229: foreach( $rmprops AS $k => $setting ) {
230:   $tag = $setting->GetNSTag();
231:   $content = $setting->RenderContent();
232: 
233:   switch( $tag ) {
234: 
235:     case 'DAV::resourcetype':
236:       add_failure('rm', $tag, 'HTTP/1.1 409 Conflict',
237:             translate("DAV::resourcetype may only be set to a new value, it may not be removed."), 'cannot-modify-protected-property');
238:       break;
239: 
240:     case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
241:       if ( $dav_resource->IsCollection() && $dav_resource->IsCalendar() && ! $dav_resource->IsBinding() ) {
242:         $qry->QDo('UPDATE collection SET timezone = NULL WHERE dav_name = :dav_name', array( ':dav_name' => $dav_resource->dav_name()) );
243:       }
244:       else {
245:         add_failure('rm', $tag, 'HTTP/1.1 409 Conflict',
246:               translate("calendar-timezone property is only valid for a calendar."), 'cannot-modify-protected-property');
247:       }
248:       break;
249: 
250:     /**
251:     * The following properties are read-only, so they will cause the request to fail
252:     */
253:     case 'http://calendarserver.org/ns/:getctag':
254:     case 'DAV::owner':
255:     case 'DAV::principal-collection-set':
256:     case 'urn:ietf:params:xml:ns:caldav:CALENDAR-USER-ADDRESS-SET':
257:     case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL':
258:     case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL':
259:     case 'DAV::getetag':
260:     case 'DAV::getcontentlength':
261:     case 'DAV::getcontenttype':
262:     case 'DAV::getlastmodified':
263:     case 'DAV::creationdate':
264:     case 'DAV::displayname':
265:     case 'DAV::lockdiscovery':
266:     case 'DAV::supportedlock':
267:       add_failure('rm', $tag, 'HTTP/1.1 409 Conflict', translate("Property is read-only"));
268:       dbg_error_log( 'PROPPATCH', ' RMProperty %s is read only and cannot be removed', $tag);
269:       break;
270: 
271:     /**
272:     * If we don't have any special processing then we must have to just delete it.  Nonexistence is not failure.
273:     */
274:     default:
275:       $qry->QDo('DELETE FROM property WHERE dav_name=:dav_name AND property_name=:property_name',
276:                   array( ':dav_name' => $dav_resource->dav_name(), ':property_name' => $tag) );
277:       $success[$tag] = 1;
278:       break;
279:   }
280: }
281: 
282: 
283: /**
284: * If we have encountered any instances of failure, the whole damn thing fails.
285: */
286: if ( count($failure) > 0 ) {
287: 
288:   $qry->Rollback();
289: 
290:   $url = ConstructURL($request->path);
291:   $multistatus = new XMLElement('multistatus');
292:   array_unshift($failure,new XMLElement('responsedescription', translate("Some properties were not able to be changed.") ));
293:   array_unshift($failure,new XMLElement('href', $url));
294:   $response = $reply->DAVElement($multistatus,'response', $failure);
295: 
296:   if ( !empty($success) ) {
297:     $prop = new XMLElement('prop');
298:     foreach( $success AS $tag => $v ) {
299:       $reply->NSElement($prop, $tag);
300:     }
301:     $reply->DAVElement($response, 'propstat', array( $prop, new XMLElement( 'status', 'HTTP/1.1 424 Failed Dependency' )) );
302:   }
303:   $request->DoResponse( 207, $reply->Render($multistatus), 'text/xml; charset="utf-8"' );
304: 
305: }
306: 
307: /**
308: * Otherwise we will try and do the SQL. This is inside a transaction, so PostgreSQL guarantees the atomicity
309: */
310: if ( $qry->Commit() ) {
311: 
312:   $cache = getCacheInstance();
313:   $cache_ns = null;
314:   if ( $dav_resource->IsPrincipal() ) {
315:     $cache_ns = 'principal-'.$dav_resource->dav_name();
316:   }
317:   else if ( $dav_resource->IsCollection() ) {
318:     // Uncache anything to do with the collection
319:     $cache_ns = 'collection-'.$dav_resource->dav_name();
320:   }
321: 
322:   if ( isset($cache_ns) ) $cache->delete( $cache_ns, null );
323: 
324:   if ( $request->PreferMinimal() ) {
325:     $request->DoResponse(200); // Does not return.
326:   }
327: 
328:   $url = ConstructURL($request->path);
329:   $multistatus = new XMLElement('multistatus');
330:   $response = $multistatus->NewElement('response');
331:   $reply->DAVElement($response,'href', $url);
332:   $reply->DAVElement($response,'responsedescription', translate("All requested changes were made.") );
333: 
334:   $prop = new XMLElement('prop');
335:   foreach( $success AS $tag => $v ) {
336:     $reply->NSElement($prop, $tag);
337:   }
338:   $reply->DAVElement($response, 'propstat', array( $prop, new XMLElement( 'status', 'HTTP/1.1 200 OK' )) );
339: 
340:   $url = ConstructURL($request->path);
341:   array_unshift( $failure, new XMLElement('href', $url ) );
342: 
343:   $request->DoResponse( 207, $reply->Render($multistatus), 'text/xml; charset="utf-8"' );
344: }
345: 
346: /**
347: * Or it was all crap.
348: */
349: $request->DoResponse( 500 );
350: exit(0); // unneccessary
351: 
352: 
DAViCal API documentation generated by ApiGen 2.8.0