1 | <?php |
---|
2 | /** |
---|
3 | * CalDAV Server - handle MKCOL and MKCALENDAR method |
---|
4 | * |
---|
5 | * @package davical |
---|
6 | * @subpackage caldav |
---|
7 | * @author Andrew McMillan <andrew@mcmillan.net.nz> |
---|
8 | * @copyright Catalyst IT Ltd, Morphoss Ltd - http://www.morphoss.com/ |
---|
9 | * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 |
---|
10 | */ |
---|
11 | dbg_error_log('MKCOL', 'method handler'); |
---|
12 | require_once('AwlQuery.php'); |
---|
13 | |
---|
14 | $request->NeedPrivilege('DAV::bind'); |
---|
15 | $displayname = $request->path; |
---|
16 | |
---|
17 | // Enforce trailling '/' on collection name |
---|
18 | if ( ! preg_match( '#/$#', $request->path ) ) { |
---|
19 | dbg_error_log( 'MKCOL', 'Add trailling "/" to "%s"', $request->path); |
---|
20 | $request->path .= '/'; |
---|
21 | } |
---|
22 | |
---|
23 | $parent_container = '/'; |
---|
24 | if ( preg_match( '#^(.*/)([^/]+)(/)?$#', $request->path, $matches ) ) { |
---|
25 | $parent_container = $matches[1]; |
---|
26 | $displayname = $matches[2]; |
---|
27 | } |
---|
28 | |
---|
29 | require_once('DAVResource.php'); |
---|
30 | $parent = new DAVResource( $parent_container ); |
---|
31 | if ( $parent->IsSchedulingCollection( 'inbox' ) ) { |
---|
32 | $request->PreconditionFailed(403, 'urn:ietf:params:xml:ns:caldav:no-mkcol-in-inbox' ); |
---|
33 | } |
---|
34 | |
---|
35 | |
---|
36 | $request_type = $request->method; |
---|
37 | $is_calendar = ($request_type == 'MKCALENDAR'); |
---|
38 | $is_addressbook = false; |
---|
39 | |
---|
40 | $resourcetypes = '<DAV::collection/>'; |
---|
41 | if ($is_calendar) $resourcetypes .= '<urn:ietf:params:xml:ns:caldav:calendar/>'; |
---|
42 | |
---|
43 | require_once('XMLDocument.php'); |
---|
44 | $reply = new XMLDocument(array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' )); |
---|
45 | |
---|
46 | $failure_code = null; |
---|
47 | |
---|
48 | $failure = array(); |
---|
49 | $dav_properties = array(); |
---|
50 | |
---|
51 | if ( isset($request->xml_tags) ) { |
---|
52 | /** |
---|
53 | * The MKCOL request may contain XML to set some DAV properties |
---|
54 | */ |
---|
55 | $position = 0; |
---|
56 | $xmltree = BuildXMLTree( $request->xml_tags, $position); |
---|
57 | if ( $xmltree->GetTag() == 'DAV::mkcol' ) $request_type = 'extended-mkcol'; |
---|
58 | |
---|
59 | if ( $xmltree->GetTag() != 'urn:ietf:params:xml:ns:caldav:mkcalendar' && $request_type != 'extended-mkcol' ) { |
---|
60 | $request->DoResponse( 406, sprintf('The XML is not a "DAV::mkcol" or "urn:ietf:params:xml:ns:caldav:mkcalendar" document (%s)', $xmltree->GetTag()) ); |
---|
61 | } |
---|
62 | $setprops = $xmltree->GetContent(); // <set> |
---|
63 | $setprops = $setprops[0]->GetContent(); // <prop> |
---|
64 | $setprops = $setprops[0]->GetContent(); // the array of properties. |
---|
65 | |
---|
66 | foreach( $setprops AS $k => $setting ) { |
---|
67 | $tag = $setting->GetTag(); |
---|
68 | $content = $setting->RenderContent(); |
---|
69 | |
---|
70 | dbg_error_log( 'MKCOL', 'Processing tag "%s"', $tag); |
---|
71 | |
---|
72 | switch( $tag ) { |
---|
73 | |
---|
74 | case 'DAV::resourcetype': |
---|
75 | /** Any value for resourcetype other than 'calendar' is ignored */ |
---|
76 | dbg_error_log( 'MKCOL', 'Extended MKCOL with resourcetype specified. "%s"', $content); |
---|
77 | $is_addressbook = count($setting->GetPath('DAV::resourcetype/urn:ietf:params:xml:ns:carddav:addressbook')); |
---|
78 | $is_calendar = count($setting->GetPath('DAV::resourcetype/urn:ietf:params:xml:ns:caldav:calendar')); |
---|
79 | if ( $is_addressbook && $is_calendar ) { |
---|
80 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
81 | new XMLElement( 'prop', new XMLElement($reply->Tag($tag))), |
---|
82 | new XMLElement( 'status', 'HTTP/1.1 409 Conflict' ), |
---|
83 | new XMLElement('responsedescription', translate('Collections may not be both CalDAV calendars and CardDAV addressbooks at the same time') ) |
---|
84 | )); |
---|
85 | } |
---|
86 | else { |
---|
87 | $resourcetypes = $setting->GetPath('DAV::resourcetype/*'); |
---|
88 | $resourcetypes = str_replace( "\n", "", implode('',$resourcetypes)); |
---|
89 | $success[$tag] = 1; |
---|
90 | } |
---|
91 | break; |
---|
92 | |
---|
93 | case 'DAV::displayname': |
---|
94 | $displayname = $content; |
---|
95 | /** |
---|
96 | * @todo This is definitely a bug in SOHO Organizer and we probably should respond |
---|
97 | * with an error, rather than silently doing what they *seem* to want us to do. |
---|
98 | */ |
---|
99 | if ( preg_match( '/^SOHO.Organizer.6\./', $_SERVER['HTTP_USER_AGENT'] ) ) { |
---|
100 | dbg_error_log( 'MKCOL', 'Displayname is "/" to "%s"', $request->path); |
---|
101 | $parent_container = $request->path; |
---|
102 | $request->path .= $content . '/'; |
---|
103 | } |
---|
104 | $success[$tag] = 1; |
---|
105 | break; |
---|
106 | |
---|
107 | case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set': /** Ignored, since we will support all component types */ |
---|
108 | case 'urn:ietf:params:xml:ns:caldav:supported-calendar-data': /** Ignored, since we will support iCalendar 2.0 */ |
---|
109 | case 'urn:ietf:params:xml:ns:caldav:calendar-data': /** Ignored, since we will support iCalendar 2.0 */ |
---|
110 | case 'urn:ietf:params:xml:ns:caldav:max-resource-size': /** Ignored, since we will support arbitrary size */ |
---|
111 | case 'urn:ietf:params:xml:ns:caldav:min-date-time': /** Ignored, since we will support arbitrary time */ |
---|
112 | case 'urn:ietf:params:xml:ns:caldav:max-date-time': /** Ignored, since we will support arbitrary time */ |
---|
113 | case 'urn:ietf:params:xml:ns:caldav:max-instances': /** Ignored, since we will support arbitrary instances */ |
---|
114 | $success[$tag] = 1; |
---|
115 | break; |
---|
116 | |
---|
117 | /** |
---|
118 | * The following properties are read-only, so they will cause the request to fail |
---|
119 | */ |
---|
120 | case 'DAV::getetag': |
---|
121 | case 'DAV::getcontentlength': |
---|
122 | case 'DAV::getcontenttype': |
---|
123 | case 'DAV::getlastmodified': |
---|
124 | case 'DAV::creationdate': |
---|
125 | case 'DAV::lockdiscovery': |
---|
126 | case 'DAV::supportedlock': |
---|
127 | $failure['set-'.$tag] = new XMLElement( 'propstat', array( |
---|
128 | new XMLElement( 'prop', new XMLElement($reply->Tag($tag))), |
---|
129 | new XMLElement( 'status', 'HTTP/1.1 409 Conflict' ), |
---|
130 | new XMLElement('responsedescription', translate('Property is read-only') ) |
---|
131 | )); |
---|
132 | if ( isset($failure_code) && $failure_code != 409 ) $failure_code = 207; |
---|
133 | else if ( !isset($failure_code) ) $failure_code = 409; |
---|
134 | break; |
---|
135 | |
---|
136 | /** |
---|
137 | * If we don't have any special processing for the property, we just store it verbatim (which will be an XML fragment). |
---|
138 | */ |
---|
139 | default: |
---|
140 | $dav_properties[$tag] = $content; |
---|
141 | $success[$tag] = 1; |
---|
142 | break; |
---|
143 | } |
---|
144 | } |
---|
145 | |
---|
146 | /** |
---|
147 | * If we have encountered any instances of failure, the whole damn thing fails. |
---|
148 | */ |
---|
149 | if ( count($failure) > 0 ) { |
---|
150 | $props = array(); |
---|
151 | $status = array(); |
---|
152 | foreach( $success AS $tag => $v ) { |
---|
153 | // Unfortunately although these succeeded, we failed overall, so they didn't happen... |
---|
154 | $props[] = new XMLElement($reply->Tag($tag)); |
---|
155 | } |
---|
156 | |
---|
157 | $status[] = new XMLElement( 'propstat', array( |
---|
158 | new XMLElement('prop', $props), |
---|
159 | new XMLElement('status', 'HTTP/1.1 424 Failed Dependency' ) |
---|
160 | )); |
---|
161 | |
---|
162 | if ( $request_type == 'extended-mkcol' ) { |
---|
163 | $request->DoResponse( $failure_code, $reply->Render('mkcol-response', array_merge( $status, $failure ), 'text/xml; charset="utf-8"' ) ); |
---|
164 | } |
---|
165 | else { |
---|
166 | array_unshift( $failure, $reply->href( ConstructURL($request->path) ) ); |
---|
167 | $failure[] = new XMLElement('responsedescription', translate('Some properties were not able to be set.') ); |
---|
168 | |
---|
169 | $request->DoResponse( 207, $reply->Render('multistatus', new XMLElement( 'response', $failure )), 'text/xml; charset="utf-8"' ); |
---|
170 | } |
---|
171 | |
---|
172 | } |
---|
173 | } |
---|
174 | |
---|
175 | $sql = 'SELECT * FROM collection WHERE dav_name = :dav_name'; |
---|
176 | $qry = new AwlQuery( $sql, array( ':dav_name' => $request->path) ); |
---|
177 | if ( ! $qry->Exec('MKCOL',__LINE__,__FILE__) ) { |
---|
178 | $request->DoResponse( 500, translate('Error querying database.') ); |
---|
179 | } |
---|
180 | if ( $qry->rows() != 0 ) { |
---|
181 | $request->DoResponse( 405, translate('A collection already exists at that location.') ); |
---|
182 | } |
---|
183 | |
---|
184 | $qry = new AwlQuery(); |
---|
185 | $qry->Begin(); |
---|
186 | |
---|
187 | if ( ! $qry->QDo( 'INSERT INTO collection ( user_no, parent_container, dav_name, dav_etag, dav_displayname, |
---|
188 | is_calendar, is_addressbook, resourcetypes, created, modified ) |
---|
189 | VALUES( :user_no, :parent_container, :dav_name, :dav_etag, :dav_displayname, |
---|
190 | :is_calendar, :is_addressbook, :resourcetypes, current_timestamp, current_timestamp )', |
---|
191 | array( |
---|
192 | ':user_no' => $request->user_no, |
---|
193 | ':parent_container' => $parent_container, |
---|
194 | ':dav_name' => $request->path, |
---|
195 | ':dav_etag' => md5($request->user_no. $request->path), |
---|
196 | ':dav_displayname' => $displayname, |
---|
197 | ':is_calendar' => ($is_calendar ? 't' : 'f'), |
---|
198 | ':is_addressbook' => ($is_addressbook ? 't' : 'f'), |
---|
199 | ':resourcetypes' => $resourcetypes |
---|
200 | ) ) ) { |
---|
201 | $request->DoResponse( 500, translate('Error writing calendar details to database.') ); |
---|
202 | } |
---|
203 | foreach( $dav_properties AS $k => $v ) { |
---|
204 | if ( ! $qry->QDo('SELECT set_dav_property( :dav_name, :user_no, :tag::text, :value::text )', |
---|
205 | array( ':dav_name' => $request->path, ':user_no' => $request->user_no, ':tag' => $k, ':value' => $v) ) ) { |
---|
206 | $request->DoResponse( 500, translate('Error writing calendar properties to database.') ); |
---|
207 | } |
---|
208 | } |
---|
209 | if ( !$qry->Commit() ) { |
---|
210 | $request->DoResponse( 500, translate('Error writing calendar details to database.') ); |
---|
211 | } |
---|
212 | dbg_error_log( 'MKCOL', 'New calendar "%s" created named "%s" for user "%d" in parent "%s"', $request->path, $displayname, $session->user_no, $parent_container); |
---|
213 | header('Cache-Control: no-cache'); /** RFC4791 mandates this at 5.3.1 */ |
---|
214 | $request->DoResponse( 201, '' ); |
---|
215 | |
---|
216 | /** |
---|
217 | * @todo We could also respond to the request... |
---|
218 | * |
---|
219 | * <?xml version="1.0" encoding="utf-8" ?> |
---|
220 | * <C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> |
---|
221 | * <D:set> |
---|
222 | * <D:prop> |
---|
223 | * <D:displayname>Lisa's Events</D:displayname> |
---|
224 | * <C:calendar-description xml:lang="en">Calendar restricted to events.</C:calendar-description> |
---|
225 | * <C:supported-calendar-component-set> |
---|
226 | * <C:comp name="VEVENT"/> |
---|
227 | * </C:supported-calendar-component-set> |
---|
228 | * <C:calendar-timezone><![CDATA[BEGIN:VCALENDAR |
---|
229 | * PRODID:-//Example Corp.//CalDAV Client//EN |
---|
230 | * VERSION:2.0 |
---|
231 | * BEGIN:VTIMEZONE |
---|
232 | * TZID:US-Eastern |
---|
233 | * LAST-MODIFIED:19870101T000000Z |
---|
234 | * BEGIN:STANDARD |
---|
235 | * DTSTART:19671029T020000 |
---|
236 | * RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 |
---|
237 | * TZOFFSETFROM:-0400 |
---|
238 | * TZOFFSETTO:-0500 |
---|
239 | * TZNAME:Eastern Standard Time (US & Canada) |
---|
240 | * END:STANDARD |
---|
241 | * BEGIN:DAYLIGHT |
---|
242 | * DTSTART:19870405T020000 |
---|
243 | * RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 |
---|
244 | * TZOFFSETFROM:-0500 |
---|
245 | * TZOFFSETTO:-0400 |
---|
246 | * TZNAME:Eastern Daylight Time (US & Canada) |
---|
247 | * END:DAYLIGHT |
---|
248 | * END:VTIMEZONE |
---|
249 | * END:VCALENDAR |
---|
250 | * ]]></C:calendar-timezone> |
---|
251 | * </D:prop> |
---|
252 | * </D:set> |
---|
253 | * </C:mkcalendar> |
---|
254 | * |
---|
255 | */ |
---|
256 | |
---|