[3733] | 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('iCalendar.php'); |
---|
| 14 | require_once('DAVResource.php'); |
---|
| 15 | |
---|
| 16 | $dav_resource = new DAVResource($request->path); |
---|
| 17 | if ( ! ($dav_resource->HavePrivilegeTo('DAV::write-properties') || $dav_resource->IsBinding() ) ) { |
---|
| 18 | $request->DoResponse( 403 ); |
---|
| 19 | } |
---|
| 20 | |
---|
| 21 | $position = 0; |
---|
| 22 | $xmltree = BuildXMLTree( $request->xml_tags, $position); |
---|
| 23 | |
---|
| 24 | // echo $xmltree->Render(); |
---|
| 25 | |
---|
| 26 | if ( $xmltree->GetTag() != "DAV::propertyupdate" ) { |
---|
| 27 | $request->DoResponse( 403 ); |
---|
| 28 | } |
---|
| 29 | |
---|
| 30 | /** |
---|
| 31 | * Find the properties being set, and the properties being removed |
---|
| 32 | */ |
---|
| 33 | $setprops = $xmltree->GetPath("/DAV::propertyupdate/DAV::set/DAV::prop/*"); |
---|
| 34 | $rmprops = $xmltree->GetPath("/DAV::propertyupdate/DAV::remove/DAV::prop/*"); |
---|
| 35 | |
---|
| 36 | /** |
---|
| 37 | * We build full status responses for failures. For success we just record |
---|
| 38 | * it, since the multistatus response only applies to failure. While it is |
---|
| 39 | * not explicitly stated in RFC2518, from reading between the lines (8.2.1) |
---|
| 40 | * a success will return 200 OK [with an empty response]. |
---|
| 41 | */ |
---|
| 42 | $failure = array(); |
---|
| 43 | $success = array(); |
---|
| 44 | |
---|
| 45 | /** |
---|
| 46 | * Not much for it but to process the incoming settings in a big loop, doing |
---|
| 47 | * the special-case stuff as needed and falling through to a default which |
---|
| 48 | * stuffs the property somewhere we will be able to retrieve it from later. |
---|
| 49 | */ |
---|
| 50 | $qry = new AwlQuery(); |
---|
| 51 | $qry->Begin(); |
---|
| 52 | $setcalendar = count($xmltree->GetPath('/DAV::propertyupdate/DAV::set/DAV::prop/DAV::resourcetype/urn:ietf:params:xml:ns:caldav:calendar')); |
---|
| 53 | foreach( $setprops AS $k => $setting ) { |
---|
| 54 | $tag = $setting->GetTag(); |
---|
| 55 | $content = $setting->RenderContent(); |
---|
| 56 | |
---|
| 57 | switch( $tag ) { |
---|
| 58 | |
---|
| 59 | case 'DAV::displayname': |
---|
| 60 | /** |
---|
| 61 | * Can't set displayname on resources - only collections or principals |
---|
| 62 | */ |
---|
| 63 | if ( $dav_resource->IsCollection() || $dav_resource->IsPrincipal() ) { |
---|
| 64 | if ( $dav_resource->IsBinding() ) { |
---|
| 65 | $qry->QDo('UPDATE dav_binding SET dav_displayname = :displayname WHERE dav_name = :dav_name', |
---|
| 66 | array( ':displayname' => $content, ':dav_name' => $dav_resource->dav_name()) ); |
---|
| 67 | } |
---|
| 68 | else if ( $dav_resource->IsPrincipal() ) { |
---|
| 69 | $qry->QDo('UPDATE dav_principal SET fullname = :displayname, displayname = :displayname, modified = current_timestamp WHERE user_no = :user_no', |
---|
| 70 | array( ':displayname' => $content, ':user_no' => $request->user_no) ); |
---|
| 71 | } |
---|
| 72 | else { |
---|
| 73 | $qry->QDo('UPDATE collection SET dav_displayname = :displayname, modified = current_timestamp WHERE dav_name = :dav_name', |
---|
| 74 | array( ':displayname' => $content, ':dav_name' => $dav_resource->dav_name()) ); |
---|
| 75 | } |
---|
| 76 | $success[$tag] = 1; |
---|
| 77 | } |
---|
| 78 | else { |
---|
| 79 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 80 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 81 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 82 | new XMLElement( 'responsedescription', array( |
---|
| 83 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 84 | translate("The displayname may only be set on collections, principals or bindings.") ) |
---|
| 85 | ) |
---|
| 86 | |
---|
| 87 | )); |
---|
| 88 | } |
---|
| 89 | break; |
---|
| 90 | |
---|
| 91 | case 'DAV::resourcetype': |
---|
| 92 | /** |
---|
| 93 | * We only allow resourcetype setting on a normal collection, and not on a resource, a principal or a bind. |
---|
| 94 | * Only collections may be CalDAV calendars or addressbooks, and they may not be both. |
---|
| 95 | */ |
---|
| 96 | $setcollection = count($setting->GetPath('DAV::resourcetype/DAV::collection')); |
---|
| 97 | $setaddressbook = count($setting->GetPath('DAV::resourcetype/urn:ietf:params:xml:ns:carddav:addressbook')); |
---|
| 98 | if ( $dav_resource->IsCollection() && $setcollection && ! $dav_resource->IsPrincipal() |
---|
| 99 | && ! $dav_resource->IsBinding() && ! ($setaddressbook && $setcalendar) ) { |
---|
| 100 | $resourcetypes = $setting->GetPath('DAV::resourcetype/*'); |
---|
| 101 | $resourcetypes = str_replace( "\n", "", implode('',$resourcetypes)); |
---|
| 102 | $qry->QDo('UPDATE collection SET is_calendar = :is_calendar::boolean, is_addressbook = :is_addressbook::boolean, |
---|
| 103 | resourcetypes = :resourcetypes WHERE dav_name = :dav_name', |
---|
| 104 | array( ':dav_name' => $dav_resource->dav_name(), ':resourcetypes' => $resourcetypes, |
---|
| 105 | ':is_calendar' => $setcalendar, ':is_addressbook' => $setaddressbook ) ); |
---|
| 106 | $success[$tag] = 1; |
---|
| 107 | } |
---|
| 108 | else { |
---|
| 109 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 110 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 111 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 112 | new XMLElement( 'responsedescription', array( |
---|
| 113 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 114 | translate("Resources may not be changed to / from collections.") ) |
---|
| 115 | ) |
---|
| 116 | )); |
---|
| 117 | } |
---|
| 118 | break; |
---|
| 119 | |
---|
| 120 | case 'urn:ietf:params:xml:ns:caldav:schedule-calendar-transp': |
---|
| 121 | if ( $dav_resource->IsCollection() && ( $dav_resource->IsCalendar() || $setcalendar ) && !$dav_resource->IsBinding() ) { |
---|
| 122 | $transparency = $setting->GetPath('urn:ietf:params:xml:ns:caldav:schedule-calendar-transp/*'); |
---|
| 123 | $transparency = preg_replace( '{^.*:}', '', $transparency[0]->GetTag()); |
---|
| 124 | $qry->QDo('UPDATE collection SET schedule_transp = :transparency WHERE dav_name = :dav_name', |
---|
| 125 | array( ':dav_name' => $dav_resource->dav_name(), ':transparency' => $transparency ) ); |
---|
| 126 | $success[$tag] = 1; |
---|
| 127 | } |
---|
| 128 | else { |
---|
| 129 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 130 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 131 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 132 | new XMLElement( 'responsedescription', array( |
---|
| 133 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 134 | translate("The CalDAV:schedule-calendar-transp property may only be set on calendars.") ) |
---|
| 135 | ) |
---|
| 136 | )); |
---|
| 137 | } |
---|
| 138 | break; |
---|
| 139 | |
---|
| 140 | case 'urn:ietf:params:xml:ns:caldav:calendar-free-busy-set': |
---|
| 141 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 142 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 143 | new XMLElement( 'status', 'HTTP/1.1 409 Conflict' ), |
---|
| 144 | new XMLElement( 'responsedescription', translate("The calendar-free-busy-set is superseded by the schedule-transp property of a calendar collection.") ) |
---|
| 145 | )); |
---|
| 146 | break; |
---|
| 147 | |
---|
| 148 | case 'urn:ietf:params:xml:ns:caldav:calendar-timezone': |
---|
| 149 | if ( $dav_resource->IsCollection() && $dav_resource->IsCalendar() && ! $dav_resource->IsBinding() ) { |
---|
| 150 | $tzcomponent = $setting->GetPath('urn:ietf:params:xml:ns:caldav:calendar-timezone'); |
---|
| 151 | $tzstring = $tzcomponent[0]->GetContent(); |
---|
| 152 | $calendar = new iCalendar( array( 'icalendar' => $tzstring) ); |
---|
| 153 | $timezones = $calendar->component->GetComponents('VTIMEZONE'); |
---|
| 154 | if ( $timezones === false || count($timezones) == 0 ) break; |
---|
| 155 | $tz = $timezones[0]; // Backward compatibility |
---|
| 156 | $tzid = $tz->GetPValue('TZID'); |
---|
| 157 | $qry->QDo('UPDATE collection SET timezone = :tzid WHERE dav_name = :dav_name', |
---|
| 158 | array( ':tzid' => $tzid, ':dav_name' => $dav_resource->dav_name()) ); |
---|
| 159 | } |
---|
| 160 | else { |
---|
| 161 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 162 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 163 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 164 | new XMLElement( 'responsedescription', array( |
---|
| 165 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 166 | translate("calendar-timezone property is only valid for a calendar.") ) |
---|
| 167 | ) |
---|
| 168 | )); |
---|
| 169 | } |
---|
| 170 | break; |
---|
| 171 | |
---|
| 172 | /** |
---|
| 173 | * The following properties are read-only, so they will cause the request to fail |
---|
| 174 | */ |
---|
| 175 | case 'http://calendarserver.org/ns/:getctag': |
---|
| 176 | case 'DAV::owner': |
---|
| 177 | case 'DAV::principal-collection-set': |
---|
| 178 | case 'urn:ietf:params:xml:ns:caldav:calendar-user-address-set': |
---|
| 179 | case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL': |
---|
| 180 | case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL': |
---|
| 181 | case 'DAV::getetag': |
---|
| 182 | case 'DAV::getcontentlength': |
---|
| 183 | case 'DAV::getcontenttype': |
---|
| 184 | case 'DAV::getlastmodified': |
---|
| 185 | case 'DAV::creationdate': |
---|
| 186 | case 'DAV::lockdiscovery': |
---|
| 187 | case 'DAV::supportedlock': |
---|
| 188 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 189 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 190 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 191 | new XMLElement( 'responsedescription', array( |
---|
| 192 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 193 | translate("Property is read-only") ) |
---|
| 194 | ) |
---|
| 195 | )); |
---|
| 196 | break; |
---|
| 197 | |
---|
| 198 | /** |
---|
| 199 | * If we don't have any special processing for the property, we just store it verbatim (which will be an XML fragment). |
---|
| 200 | */ |
---|
| 201 | default: |
---|
| 202 | $qry->QDo('SELECT set_dav_property( :dav_name, :user_no, :tag::text, :value::text)', |
---|
| 203 | array( ':dav_name' => $dav_resource->dav_name(), ':user_no' => $request->user_no, ':tag' => $tag, ':value' => $content) ); |
---|
| 204 | $success[$tag] = 1; |
---|
| 205 | break; |
---|
| 206 | } |
---|
| 207 | } |
---|
| 208 | |
---|
| 209 | |
---|
| 210 | |
---|
| 211 | foreach( $rmprops AS $k => $setting ) { |
---|
| 212 | $tag = $setting->GetTag(); |
---|
| 213 | $content = $setting->RenderContent(); |
---|
| 214 | |
---|
| 215 | switch( $tag ) { |
---|
| 216 | |
---|
| 217 | case 'DAV::resourcetype': |
---|
| 218 | $failure['rm-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 219 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 220 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 221 | new XMLElement( 'responsedescription', array( |
---|
| 222 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 223 | translate("DAV::resourcetype may only be set to a new value, it may not be removed.") ) |
---|
| 224 | ) |
---|
| 225 | )); |
---|
| 226 | break; |
---|
| 227 | |
---|
| 228 | case 'urn:ietf:params:xml:ns:caldav:calendar-timezone': |
---|
| 229 | if ( $dav_resource->IsCollection() && $dav_resource->IsCalendar() && ! $dav_resource->IsBinding() ) { |
---|
| 230 | $qry->QDo('UPDATE collection SET timezone = NULL WHERE dav_name = :dav_name', array( ':dav_name' => $dav_resource->dav_name()) ); |
---|
| 231 | } |
---|
| 232 | else { |
---|
| 233 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 234 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 235 | new XMLElement( 'status', 'HTTP/1.1 403 Forbidden' ), |
---|
| 236 | new XMLElement( 'responsedescription', array( |
---|
| 237 | new XMLElement( 'error', new XMLElement( 'cannot-modify-protected-property') ), |
---|
| 238 | translate("calendar-timezone property is only valid for a calendar.") ) |
---|
| 239 | ) |
---|
| 240 | )); |
---|
| 241 | } |
---|
| 242 | break; |
---|
| 243 | |
---|
| 244 | /** |
---|
| 245 | * The following properties are read-only, so they will cause the request to fail |
---|
| 246 | */ |
---|
| 247 | case 'http://calendarserver.org/ns/:getctag': |
---|
| 248 | case 'DAV::owner': |
---|
| 249 | case 'DAV::principal-collection-set': |
---|
| 250 | case 'urn:ietf:params:xml:ns:caldav:CALENDAR-USER-ADDRESS-SET': |
---|
| 251 | case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL': |
---|
| 252 | case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL': |
---|
| 253 | case 'DAV::getetag': |
---|
| 254 | case 'DAV::getcontentlength': |
---|
| 255 | case 'DAV::getcontenttype': |
---|
| 256 | case 'DAV::getlastmodified': |
---|
| 257 | case 'DAV::creationdate': |
---|
| 258 | case 'DAV::displayname': |
---|
| 259 | case 'DAV::lockdiscovery': |
---|
| 260 | case 'DAV::supportedlock': |
---|
| 261 | $failure['rm-'.$tag] = new XMLElement( 'propstat', array( |
---|
| 262 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 263 | new XMLElement( 'status', 'HTTP/1.1 409 Conflict' ), |
---|
| 264 | new XMLElement('responsedescription', translate("Property is read-only") ) |
---|
| 265 | )); |
---|
| 266 | dbg_error_log( 'PROPPATCH', ' RMProperty %s is read only and cannot be removed', $tag); |
---|
| 267 | break; |
---|
| 268 | |
---|
| 269 | /** |
---|
| 270 | * If we don't have any special processing then we must have to just delete it. Nonexistence is not failure. |
---|
| 271 | */ |
---|
| 272 | default: |
---|
| 273 | $qry->QDo('DELETE FROM property WHERE dav_name=:dav_name AND property_name=:property_name', |
---|
| 274 | array( ':dav_name' => $dav_resource->dav_name(), ':property_name' => $tag) ); |
---|
| 275 | $success[$tag] = 1; |
---|
| 276 | break; |
---|
| 277 | } |
---|
| 278 | } |
---|
| 279 | |
---|
| 280 | |
---|
| 281 | /** |
---|
| 282 | * If we have encountered any instances of failure, the whole damn thing fails. |
---|
| 283 | */ |
---|
| 284 | if ( count($failure) > 0 ) { |
---|
| 285 | foreach( $success AS $tag => $v ) { |
---|
| 286 | // Unfortunately although these succeeded, we failed overall, so they didn't happen... |
---|
| 287 | $failure[] = new XMLElement( 'propstat', array( |
---|
| 288 | new XMLElement( 'prop', new XMLElement($tag)), |
---|
| 289 | new XMLElement( 'status', 'HTTP/1.1 424 Failed Dependency' ), |
---|
| 290 | )); |
---|
| 291 | } |
---|
| 292 | |
---|
| 293 | $url = ConstructURL($request->path); |
---|
| 294 | array_unshift( $failure, new XMLElement('href', $url ) ); |
---|
| 295 | $failure[] = new XMLElement('responsedescription', translate("Some properties were not able to be changed.") ); |
---|
| 296 | |
---|
| 297 | $qry->Rollback(); |
---|
| 298 | |
---|
| 299 | $multistatus = new XMLElement( "multistatus", new XMLElement( 'response', $failure ), array('xmlns'=>'DAV:') ); |
---|
| 300 | $request->DoResponse( 207, $multistatus->Render(0,'<?xml version="1.0" encoding="utf-8" ?>'), 'text/xml; charset="utf-8"' ); |
---|
| 301 | |
---|
| 302 | } |
---|
| 303 | |
---|
| 304 | /** |
---|
| 305 | * Otherwise we will try and do the SQL. This is inside a transaction, so PostgreSQL guarantees the atomicity |
---|
| 306 | */ |
---|
| 307 | ; |
---|
| 308 | if ( $qry->Commit() ) { |
---|
| 309 | $url = ConstructURL($request->path); |
---|
| 310 | $href = new XMLElement('href', $url ); |
---|
| 311 | $desc = new XMLElement('responsedescription', translate("All requested changes were made.") ); |
---|
| 312 | |
---|
| 313 | $multistatus = new XMLElement( "multistatus", new XMLElement( 'response', array( $href, $desc ) ), array('xmlns'=>'DAV:') ); |
---|
| 314 | $request->DoResponse( 200, $multistatus->Render(0,'<?xml version="1.0" encoding="utf-8" ?>'), 'text/xml; charset="utf-8"' ); |
---|
| 315 | } |
---|
| 316 | |
---|
| 317 | /** |
---|
| 318 | * Or it was all crap. |
---|
| 319 | */ |
---|
| 320 | $request->DoResponse( 500 ); |
---|
| 321 | |
---|
| 322 | exit(0); |
---|
| 323 | |
---|