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 | |
---|