[3733] | 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 | |
---|