2008-05-08 22:31:32 +02:00
< ? php
/**
2009-10-17 14:22:40 +02:00
* EGroupware : CalDAV / CardDAV / GroupDAV access
2008-05-08 22:31:32 +02:00
*
* @ link http :// www . egroupware . org
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package api
2016-04-02 12:44:17 +02:00
* @ subpackage caldav
2008-05-08 22:31:32 +02:00
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
2016-03-20 17:19:53 +01:00
* @ copyright ( c ) 2007 - 16 by Ralf Becker < RalfBecker - AT - outdoor - training . de >
2008-05-08 22:31:32 +02:00
* @ version $Id $
*/
2016-04-02 12:44:17 +02:00
namespace EGroupware\Api ;
use EGroupware\Api\CalDAV\Handler ;
use EGroupware\Api\CalDAV\Principals ;
// explicit import non-namespaced classes
require_once ( __DIR__ . '/WebDAV/Server.php' );
2021-09-16 20:53:43 +02:00
2023-11-29 14:47:27 +01:00
use EGroupware\Api\CalDAV\JsParseException ;
2016-04-02 12:44:17 +02:00
use HTTP_WebDAV_Server ;
use calendar_hooks ;
2008-05-08 22:31:32 +02:00
/**
2021-09-17 20:15:36 +02:00
* EGroupware : CalDAV / CardDAV server
2008-05-08 22:31:32 +02:00
*
2014-12-11 11:35:38 +01:00
* Using a modified PEAR HTTP / WebDAV / Server class from API !
2009-10-03 12:22:14 +02:00
*
2021-09-17 20:15:36 +02:00
* One can use the following URLs relative ( ! ) to https :// example . org / egroupware / groupdav . php
2009-10-03 12:22:14 +02:00
*
2011-09-20 21:16:24 +02:00
* - / base of Cal | Card | GroupDAV tree , only certain clients ( KDE , Apple ) can autodetect folders from here
* - / principals / principal - collection - set for WebDAV ACL
* - / principals / users /< username >/
* - / principals / groups /< groupname >/
* - /< username >/ users home - set with
2009-10-03 12:22:14 +02:00
* - /< username >/ addressbook / addressbook of user or group < username > given the user has rights to view it
2012-09-27 17:46:08 +02:00
* - /< current - username >/ addressbook -< other - username >/ shared addressbooks from other user or group
* - /< current - username >/ addressbook - accounts / all accounts current user has rights to see
2009-10-03 12:22:14 +02:00
* - /< username >/ calendar / calendar of user < username > given the user has rights to view it
2015-11-13 16:23:36 +01:00
* - /< username >/ calendar / ? download download whole calendar as . ics file ( GET request ! )
2012-09-27 17:46:08 +02:00
* - /< current - username >/ calendar -< other - username >/ shared calendar from other user or group ( only current < username >! )
2011-09-22 20:46:16 +02:00
* - /< username >/ inbox / scheduling inbox of user < username >
* - /< username >/ outbox / scheduling outbox of user < username >
2009-10-03 12:22:14 +02:00
* - /< username >/ infolog / InfoLog ' s of user < username > given the user has rights to view it
2011-09-20 21:16:24 +02:00
* - / addressbook / all addressbooks current user has rights to , announced as directory - gateway now
2012-04-11 22:33:24 +02:00
* - / addressbook - accounts / all accounts current user has rights to see
2011-09-20 21:16:24 +02:00
* - / calendar / calendar of current user
* - / infolog / infologs of current user
2023-07-21 08:54:06 +02:00
* - / mail / mail REST API , see doc / REST - CalDAV - CardDAV / Mail . md
2012-09-27 17:46:08 +02:00
* - / ( resources | locations ) /< resource - name >/ calendar calendar of a resource / location , if user has rights to view
* - /< current - username >/ ( resource | location ) -< resource - name > shared calendar from a resource / location
*
2021-09-17 20:15:36 +02:00
* Shared addressbooks or calendars are only shown in the users home - set , if he subscribed to it via his CalDAV preferences !
2009-10-03 12:22:14 +02:00
*
* Calling one of the above collections with a GET request / regular browser generates an automatic index
2021-09-17 20:15:36 +02:00
* from the data of a allprop PROPFIND , allow browsing CalDAV / CardDAV tree with a regular browser .
*
2023-07-21 08:54:06 +02:00
* Using EGroupware CalDAV / CardDAV as REST API : currently only for contacts and mail ( sending )
2021-09-17 20:15:36 +02:00
* ===========================================
* GET requests to collections with an " Accept: application/json " header return a JSON response similar to a PROPFIND
* following GET parameters are supported to customize the returned properties :
2023-07-21 08:54:06 +02:00
* - props [] =< DAV - prop - name > e . g . props [] = getetag to return only the ETAG ( multiple DAV properties can be specified )
2021-09-17 20:15:36 +02:00
* Default for addressbook collections is to only return address - data ( JsContact ), other collections return all props .
* - sync - token =< token > to only request change since last sync - token , like rfc6578 sync - collection REPORT
* - nresults = N limit number of responses ( only for sync - collection / given sync - token parameter ! )
* this will return a " more-results " = true attribute and a new " sync-token " attribute to query for the next chunk
* POST requests to collection with a " Content-Type: application/json " header add new entries in addressbook or calendar collections
* ( Location header in response gives URL of new resource )
* GET requests with an " Accept: application/json " header can be used to retrieve single resources / JsContact or JsCalendar schema
* PUT requests with a " Content-Type: application/json " header allow modifying single resources
* DELETE requests delete single resources
2023-07-21 08:54:06 +02:00
* PATCH modify existing resource with partial data
2008-05-08 22:31:32 +02:00
*
2012-02-21 21:04:45 +01:00
* Permanent error_log () calls should use groupdav -> log ( $str ) instead , to be send to PHP error_log ()
* and our request - log ( prefixed with " ### " after request and response , like exceptions ) .
*
2011-09-20 21:16:24 +02:00
* @ link http :// www . groupdav . org / GroupDAV spec
* @ link http :// caldav . calconnect . org / CalDAV resources
* @ link http :// carddav . calconnect . org / CardDAV resources
* @ link http :// calendarserver . org / Apple calendar and contacts server
2008-05-08 22:31:32 +02:00
*/
2016-04-02 12:44:17 +02:00
class CalDAV extends HTTP_WebDAV_Server
2008-05-08 22:31:32 +02:00
{
2010-03-07 00:06:43 +01:00
/**
* DAV namespace
*/
const DAV = 'DAV:' ;
2008-05-08 22:31:32 +02:00
/**
* GroupDAV namespace
*/
const GROUPDAV = 'http://groupdav.org/' ;
/**
* CalDAV namespace
*/
const CALDAV = 'urn:ietf:params:xml:ns:caldav' ;
/**
* CardDAV namespace
*/
const CARDDAV = 'urn:ietf:params:xml:ns:carddav' ;
2010-01-06 00:25:17 +01:00
/**
2023-07-21 08:54:06 +02:00
* Apple Calendarserver namespace ( e . g . for ctag )
2010-01-06 00:25:17 +01:00
*/
const CALENDARSERVER = 'http://calendarserver.org/ns/' ;
2011-09-16 12:21:40 +02:00
/**
2023-07-21 08:54:06 +02:00
* Apple Addressbookserver namespace ( e . g . for ctag )
2011-09-16 12:21:40 +02:00
*/
const ADDRESSBOOKSERVER = 'http://addressbookserver.org/ns/' ;
2010-04-13 17:31:59 +02:00
/**
2023-07-21 08:54:06 +02:00
* Apple iCal namespace ( e . g . for calendar color )
2010-04-13 17:31:59 +02:00
*/
2010-09-28 10:32:11 +02:00
const ICAL = 'http://apple.com/ns/ical/' ;
2008-05-08 22:31:32 +02:00
/**
* Realm and powered by string
*/
2010-04-13 17:31:59 +02:00
const REALM = 'EGroupware CalDAV/CardDAV/GroupDAV server' ;
2008-05-08 22:31:32 +02:00
var $dav_powered_by = self :: REALM ;
2008-05-20 06:59:26 +02:00
var $http_auth_realm = self :: REALM ;
2008-05-08 22:31:32 +02:00
2011-09-21 22:08:21 +02:00
/**
* Folders in root or user home
*
* @ var array
*/
2008-05-08 22:31:32 +02:00
var $root = array (
2011-09-21 22:08:21 +02:00
'addressbook' => array (
'resourcetype' => array ( self :: GROUPDAV => 'vcard-collection' , self :: CARDDAV => 'addressbook' ),
'component-set' => array ( self :: GROUPDAV => 'VCARD' ),
),
2008-08-04 21:08:09 +02:00
'calendar' => array (
'resourcetype' => array ( self :: GROUPDAV => 'vevent-collection' , self :: CALDAV => 'calendar' ),
'component-set' => array ( self :: GROUPDAV => 'VEVENT' ),
),
2011-09-22 17:22:52 +02:00
'inbox' => array (
2011-09-21 22:08:21 +02:00
'resourcetype' => array ( self :: CALDAV => 'schedule-inbox' ),
'app' => 'calendar' ,
2011-09-22 17:22:52 +02:00
'user-only' => true , // display just in user home
2008-08-04 21:08:09 +02:00
),
2011-09-21 22:08:21 +02:00
'outbox' => array (
'resourcetype' => array ( self :: CALDAV => 'schedule-outbox' ),
'app' => 'calendar' ,
2011-09-22 17:22:52 +02:00
'user-only' => true , // display just in user home
),
2008-08-04 21:08:09 +02:00
'infolog' => array (
2010-03-07 00:06:43 +01:00
'resourcetype' => array ( self :: GROUPDAV => 'vtodo-collection' , self :: CALDAV => 'calendar' ),
2008-08-04 21:08:09 +02:00
'component-set' => array ( self :: GROUPDAV => 'VTODO' ),
),
2008-05-08 22:31:32 +02:00
);
/**
* Debug level : 0 = nothing , 1 = function calls , 2 = more info , 3 = complete $_SERVER array
*
2023-07-21 08:54:06 +02:00
* Can now be enabled on a per - user basis in GroupDAV preferences , if it is set here to 0 !
2010-10-31 08:56:29 +01:00
*
2008-05-08 22:31:32 +02:00
* The debug messages are send to the apache error_log
*
* @ var integer
*/
2010-03-07 00:32:28 +01:00
var $debug = 0 ;
2008-05-08 22:31:32 +02:00
/**
* eGW ' s charset
*
* @ var string
*/
var $egw_charset ;
/**
* Instance of our application specific handler
*
2016-04-02 12:44:17 +02:00
* @ var Handler
2008-05-08 22:31:32 +02:00
*/
var $handler ;
2010-03-07 00:06:43 +01:00
/**
2011-09-18 12:56:56 +02:00
* current - user - principal URL
2010-03-07 00:06:43 +01:00
*
* @ var string
*/
2011-09-18 12:56:56 +02:00
var $current_user_principal ;
2010-03-07 00:06:43 +01:00
/**
* Reference to the accounts class
*
* @ var accounts
*/
var $accounts ;
2011-09-21 22:08:21 +02:00
/**
* Supported privileges with name and description
*
* privileges are hierarchical
*
* @ var array
*/
var $supported_privileges = array (
'all' => array (
'*description*' => 'all privileges' ,
2011-09-22 20:46:16 +02:00
'read' => array (
'*description*' => 'read resource' ,
'read-free-busy' => array (
'*ns*' => self :: CALDAV ,
'*description*' => 'allow free busy report query' ,
'*only*' => '/calendar/' ,
),
),
2011-09-21 22:08:21 +02:00
'write' => array (
'*description*' => 'write resource' ,
'write-properties' => 'write resource properties' ,
'write-content' => 'write resource content' ,
'bind' => 'add child resource' ,
'unbind' => 'remove child resource' ,
),
'unlock' => 'unlock resource without ownership of lock' ,
'read-acl' => 'read resource access control list' ,
'write-acl' => 'write resource access control list' ,
'read-current-user-privilege-set' => 'read privileges for current principal' ,
2011-09-22 20:46:16 +02:00
'schedule-deliver' => array (
'*ns*' => self :: CALDAV ,
'*description*' => 'schedule privileges for current principal' ,
'*only*' => '/inbox/' ,
),
'schedule-send' => array (
'*ns*' => self :: CALDAV ,
'*description*' => 'schedule privileges for current principal' ,
'*only*' => '/outbox/' ,
),
2011-09-21 22:08:21 +02:00
),
);
/**
2023-07-21 08:54:06 +02:00
* $options parameter to PROPFIND request , e . g . to check what props are requested
2011-09-21 22:08:21 +02:00
*
* @ var array
*/
var $propfind_options ;
2010-03-07 00:06:43 +01:00
2012-10-04 13:59:04 +02:00
/**
* Reference to active instance , used by exception handler
*
2021-03-31 17:49:43 +02:00
* @ var self
2012-10-04 13:59:04 +02:00
*/
protected static $instance ;
2008-05-08 22:31:32 +02:00
function __construct ()
{
2015-09-30 05:27:29 +02:00
// log which CalDAVTester test is currently running, set as User-Agent header
if ( substr ( $_SERVER [ 'HTTP_USER_AGENT' ], 0 , 14 ) == 'scripts/tests/' ) error_log ( '****** ' . $_SERVER [ 'HTTP_USER_AGENT' ]);
2010-10-31 08:56:29 +01:00
if ( ! $this -> debug ) $this -> debug = ( int ) $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'debug_level' ];
2008-05-17 14:54:26 +02:00
if ( $this -> debug > 2 ) error_log ( 'groupdav: $_SERVER=' . array2string ( $_SERVER ));
2008-05-08 22:31:32 +02:00
2012-02-20 10:06:24 +01:00
// setting our own exception handler, to be able to still log the requests
set_exception_handler ( array ( __CLASS__ , 'exception_handler' ));
2011-09-18 12:56:56 +02:00
// crrnd: client refuses redundand namespace declarations
2011-09-20 21:16:24 +02:00
// setting redundand namespaces as the default for (Cal|Card|Group)DAV, as the majority of the clients either require or can live with it
2012-08-13 11:32:03 +02:00
$this -> crrnd = false ;
2011-09-20 21:16:24 +02:00
// identify clients, which do NOT support path AND full url in <D:href> of PROPFIND request
2016-04-02 12:44:17 +02:00
switch (( $agent = Handler :: get_agent ()))
2009-09-14 10:44:37 +02:00
{
case 'kde' : // KAddressbook (at least in 3.5 can NOT subscribe / does NOT find addressbook)
$this -> client_require_href_as_url = true ;
break ;
2010-09-25 11:08:37 +02:00
case 'cfnetwork' : // Apple addressbook app
case 'dataaccess' : // iPhone addressbook
$this -> client_require_href_as_url = false ;
break ;
2009-09-14 10:44:37 +02:00
case 'davkit' : // iCal app in OS X 10.6 created wrong request, if full url given
2011-08-02 14:59:23 +02:00
case 'coredav' : // iCal app in OS X 10.7
2011-11-08 22:03:49 +01:00
case 'calendarstore' : // Apple iCal 5.0.1 under OS X 10.7.2
2009-09-14 10:44:37 +02:00
$this -> client_require_href_as_url = false ;
break ;
2010-04-21 19:44:36 +02:00
case 'cfnetwork_old' :
$this -> crrnd = true ; // Older Apple Addressbook.app does not cope with namespace redundancy
2010-05-18 12:45:46 +02:00
break ;
2009-09-14 10:44:37 +02:00
}
2012-08-13 11:32:03 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " () HTTP_USER_AGENT=' $_SERVER[HTTP_USER_AGENT] ' --> ' $agent ' --> client_requires_href_as_url= $this->client_require_href_as_url , crrnd(client refuses redundand namespace declarations)= $this->crrnd " );
2011-09-18 12:56:56 +02:00
2023-07-21 08:54:06 +02:00
// adding EGroupware version to X-Dav-Powered-By header e.g. "EGroupware 1.8.001 CalDAV/CardDAV/GroupDAV server"
2010-09-28 10:32:11 +02:00
$this -> dav_powered_by = str_replace ( 'EGroupware' , 'EGroupware ' . $GLOBALS [ 'egw_info' ][ 'server' ][ 'versions' ][ 'phpgwapi' ],
$this -> dav_powered_by );
2023-06-29 12:49:50 +02:00
// detected available additional APIs from applications
$this -> root += Cache :: getInstance ( __CLASS__ , 'user-' . $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ], static function ()
{
$apis = [];
foreach ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ] as $app => $data )
{
if ( class_exists ( 'EGroupware\\' . ucfirst ( $app ) . '\\ApiHandler' ))
{
$apis [ $app ] = [];
}
}
return $apis ;
}, [], 86400 );
2016-04-02 10:40:34 +02:00
parent :: __construct ();
2012-02-04 21:24:01 +01:00
// hack to allow to use query parameters in WebDAV, which HTTP_WebDAV_Server interprets as part of the path
list ( $this -> _SERVER [ 'REQUEST_URI' ]) = explode ( '?' , $this -> _SERVER [ 'REQUEST_URI' ]);
2019-09-12 09:10:03 +02:00
// OSX Addressbook sends ?add-member url-encoded
if ( substr ( $this -> _SERVER [ 'REQUEST_URI' ], - 14 ) == '/%3Fadd-member' )
2012-02-04 21:24:01 +01:00
{
$_GET [ 'add-member' ] = '' ;
2019-09-12 09:10:03 +02:00
$this -> _SERVER [ 'REQUEST_URI' ] = substr ( $this -> _SERVER [ 'REQUEST_URI' ], 0 , - 14 );
}
2012-02-04 21:24:01 +01:00
//error_log($_SERVER['REQUEST_URI']." --> ".$this->_SERVER['REQUEST_URI']);
2008-05-08 22:31:32 +02:00
2016-04-02 12:44:17 +02:00
$this -> egw_charset = Translation :: charset ();
2010-03-07 00:06:43 +01:00
if ( strpos ( $this -> base_uri , 'http' ) === 0 )
{
2023-07-21 08:54:06 +02:00
$this -> current_user_principal = self :: _slashify ( $this -> base_uri );
2010-03-07 00:06:43 +01:00
}
else
{
2019-11-15 13:54:34 +01:00
$this -> current_user_principal = Framework :: getUrl ( $_SERVER [ 'SCRIPT_NAME' ]) . '/' ;
2010-03-07 00:06:43 +01:00
}
2011-09-18 12:56:56 +02:00
$this -> current_user_principal .= 'principals/users/' . $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ] . '/' ;
2010-10-31 08:56:29 +01:00
2010-09-25 11:08:37 +02:00
// if client requires pathes instead of URLs
2011-09-18 12:56:56 +02:00
if ( ! $this -> client_require_href_as_url )
2010-09-25 11:08:37 +02:00
{
2011-09-18 12:56:56 +02:00
$this -> current_user_principal = parse_url ( $this -> current_user_principal , PHP_URL_PATH );
2010-09-25 11:08:37 +02:00
}
2010-03-07 00:06:43 +01:00
$this -> accounts = $GLOBALS [ 'egw' ] -> accounts ;
2012-10-04 13:59:04 +02:00
self :: $instance = $this ;
2008-05-08 22:31:32 +02:00
}
2008-05-17 14:54:26 +02:00
/**
* get the handler for $app
*
* @ param string $app
2016-04-02 12:44:17 +02:00
* @ return Handler
2008-05-17 14:54:26 +02:00
*/
function app_handler ( $app )
2008-05-08 22:31:32 +02:00
{
2011-09-21 22:08:21 +02:00
if ( isset ( $this -> root [ $app ][ 'app' ])) $app = $this -> root [ $app ][ 'app' ];
2016-04-02 12:44:17 +02:00
return Handler :: app_handler ( $app , $this );
2008-05-08 22:31:32 +02:00
}
/**
* OPTIONS request , allow to modify the standard responses from the pear - class
*
* @ param string $path
* @ param array & $dav
* @ param array & $allow
*/
function OPTIONS ( $path , & $dav , & $allow )
{
2016-04-02 10:40:34 +02:00
unset ( $allow ); // not used, but required by function signature
2012-02-07 21:19:16 +01:00
// locking support
2012-02-09 21:09:49 +01:00
if ( ! in_array ( '2' , $dav )) $dav [] = '2' ;
2012-02-07 21:19:16 +01:00
2023-07-21 08:54:06 +02:00
if ( preg_match ( '#/(calendar(-[^/]+)?|inbox|outbox)/#' , $path )) // e.g. /<username>/calendar-<otheruser>/
2008-05-08 22:31:32 +02:00
{
2011-09-25 14:00:20 +02:00
$app = 'calendar' ;
}
2023-07-21 08:54:06 +02:00
elseif ( preg_match ( '#/addressbook(-[^/]+)?/#' , $path )) // e.g. /<username>/addressbook-<otheruser>/
2011-09-25 14:00:20 +02:00
{
$app = 'addressbook' ;
}
// CalDAV and CardDAV
$dav [] = 'access-control' ;
if ( $app !== 'addressbook' ) // CalDAV
{
$dav [] = 'calendar-access' ;
$dav [] = 'calendar-auto-schedule' ;
$dav [] = 'calendar-proxy' ;
// required by iOS iCal to use principal-property-search to autocomplete participants (and locations)
$dav [] = 'calendarserver-principal-property-search' ;
2012-02-07 21:19:16 +01:00
// required by iOS & OS X iCal to show private checkbox (X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALENDAR)
$dav [] = 'calendarserver-private-events' ;
2013-09-23 12:21:31 +02:00
// managed attachments
$dav [] = 'calendar-managed-attachments' ;
2011-09-25 14:00:20 +02:00
// other capabilities calendarserver announces
//$dav[] = 'calendar-schedule';
//$dav[] = 'calendar-availability';
//$dav[] = 'inbox-availability';
//$dav[] = 'calendarserver-private-comments';
//$dav[] = 'calendarserver-sharing';
//$dav[] = 'calendarserver-sharing-no-scheduling';
2008-05-08 22:31:32 +02:00
}
2011-09-26 08:39:13 +02:00
if ( $app !== 'calendar' ) // CardDAV
2011-09-25 14:00:20 +02:00
{
$dav [] = 'addressbook' ; // CardDAV uses "addressbook" NOT "addressbook-access"
}
//error_log(__METHOD__."('$path') --> app='$app' --> DAV: ".implode(', ', $dav));
2008-05-08 22:31:32 +02:00
}
2011-09-21 22:08:21 +02:00
/**
* PROPFIND and REPORT method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options general parameter passing array
* @ param array & $files return array for file properties
* @ param string $method " PROPFIND " ( default ) or " REPORT "
2011-09-21 22:08:21 +02:00
* @ return bool true on success
*/
function PROPFIND ( & $options , & $files , $method = 'PROPFIND' )
{
2012-01-21 02:45:48 +01:00
if ( $this -> debug ) error_log ( __CLASS__ . " :: $method ( " . array2string ( $options ) . ')' );
2011-09-21 22:08:21 +02:00
2023-07-21 08:54:06 +02:00
// make options (readonly) available to all class methods, e.g. prop_requested
2011-09-21 22:08:21 +02:00
$this -> propfind_options = $options ;
2022-11-07 20:50:45 +01:00
$nresults = null ;
foreach ( $options [ 'other' ] ? ? [] as $option )
{
if ( $option [ 'name' ] === 'nresults' && ( int ) $option [ 'data' ] > 0 )
{
$nresults = ( int ) $option [ 'data' ];
}
}
2011-09-21 22:08:21 +02:00
// parse path in form [/account_lid]/app[/more]
2016-04-02 10:40:34 +02:00
$id = $app = $user = $user_prefix = null ;
2023-07-21 08:54:06 +02:00
if ( ! $this -> _parse_path ( $options [ 'path' ], $id , $app , $user , $user_prefix ) && $app && ! $user && $user !== 0 )
2011-09-21 22:08:21 +02:00
{
if ( $this -> debug > 1 ) error_log ( __CLASS__ . " :: $method : user=' $user ', app=' $app ', id=' $id ': 404 not found! " );
return '404 Not Found' ;
}
2012-09-27 17:46:08 +02:00
if ( $this -> debug > 1 ) error_log ( __CLASS__ . " :: $method (path=' $options[path] '): user=' $user ', user_prefix=' $user_prefix ', app=' $app ', id=' $id ' " );
2011-09-21 22:08:21 +02:00
$files = array ( 'files' => array ());
2023-07-21 08:54:06 +02:00
$path = $user_prefix = self :: _slashify ( $user_prefix );
2011-09-21 22:08:21 +02:00
if ( ! $app ) // user root folder containing apps
{
// add root with current users apps
$this -> add_home ( $files , $path , $user , $options [ 'depth' ]);
2014-11-14 11:19:20 +01:00
if ( $path == '/' )
{
2016-05-11 20:58:10 +02:00
Hooks :: process ( array (
2014-11-14 11:19:20 +01:00
'location' => 'groupdav_root_props' ,
'props' => & $files [ 'files' ][ 0 ][ 'props' ],
'options' => $options ,
2016-04-27 15:28:05 +02:00
'caldav' => $this ,
2014-11-14 11:19:20 +01:00
));
}
2011-09-21 22:08:21 +02:00
// add principals and user-homes
if ( $path == '/' && $options [ 'depth' ])
{
// principals collection
$files [ 'files' ][] = $this -> add_collection ( '/principals/' , array (
2011-11-23 17:34:39 +01:00
'displayname' => lang ( 'Accounts' ),
2011-09-21 22:08:21 +02:00
));
2022-11-07 20:50:45 +01:00
foreach ( $this -> accounts -> search ([
'type' => 'both' ,
'order' => 'account_lid' ,
'start' => $_GET [ 'start' ] ? ? 0 ,
'offset' => $nresults ,
]) as $account )
2011-09-21 22:08:21 +02:00
{
2012-01-25 04:25:42 +01:00
$this -> add_home ( $files , $path . $account [ 'account_lid' ] . '/' , $account [ 'account_id' ], $options [ 'depth' ] == 'infinity' ? 'infinity' : $options [ 'depth' ] - 1 );
2011-09-21 22:08:21 +02:00
}
2022-11-07 20:50:45 +01:00
// if nresults-limit is set respond correct
if ( isset ( $nresults ) && $this -> accounts -> total > ( $_GET [ 'start' ] ? ? 0 ) + $nresults )
{
$handler = new CalDAV\Principals ( 'calendar' , $this );
$handler -> sync_collection_toke = '?start=' . (( $_GET [ 'start' ] ? ? 0 ) + $nresults );
$files [ 'sync-token' ] = [ $handler , 'get_sync_collection_token' ];
$files [ 'sync-token-parameters' ] = [ '/' , '' , true ];
}
2011-09-21 22:08:21 +02:00
}
return true ;
}
2012-09-27 17:46:08 +02:00
if ( $path == '/' && ( $app == 'resources' || $app == 'locations' ))
{
return $this -> add_resources_collection ( $files , '/' . $app . '/' , $options [ 'depth' ]);
}
2011-09-21 22:08:21 +02:00
if ( $app != 'principals' && ! isset ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ $this -> root [ $app ][ 'app' ] ? $this -> root [ $app ][ 'app' ] : $app ]))
{
if ( $this -> debug ) error_log ( __CLASS__ . " :: $method (path= $options[path] ) 403 Forbidden: no app rights for ' $app ' " );
return " 403 Forbidden: no app rights for ' $app ' " ; // no rights for the given app
}
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2011-09-21 22:08:21 +02:00
{
if ( $method != 'REPORT' && ! $id ) // no self URL for REPORT requests (only PROPFIND) or propfinds on an id
{
// KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes
2012-01-30 06:11:05 +01:00
$files [ 'files' ][ 0 ] = $this -> add_app ( $app , $app == 'addressbook' && $handler -> get_agent () == 'kde' , $user ,
2023-07-21 08:54:06 +02:00
self :: _slashify ( $options [ 'path' ]));
2011-09-21 22:08:21 +02:00
2015-09-03 08:54:06 +02:00
// Hack for iOS 5.0.1 addressbook to stop asking directory gateway permissions with depth != 0
// values for depth are 0, 1, "infinit" or not set which has to be interpreted as "infinit"
2015-09-02 15:38:36 +02:00
if ( $method == 'PROPFIND' && $options [ 'path' ] == '/addressbook/' &&
2015-09-03 08:54:06 +02:00
( ! isset ( $options [ 'depth' ]) || $options [ 'depth' ]) && $handler -> get_agent () == 'dataaccess' )
2012-01-21 02:45:48 +01:00
{
2012-02-21 21:04:45 +01:00
$this -> log ( __CLASS__ . " :: $method ( " . array2string ( $options ) . ') Enabling hack for iOS 5.0.1 addressbook: force Depth: 0 on PROPFIND for directory gateway!' );
2012-01-21 02:45:48 +01:00
return true ;
}
2011-09-21 22:08:21 +02:00
if ( ! $options [ 'depth' ]) return true ; // depth 0 --> show only the self url
}
2023-07-21 08:54:06 +02:00
return $handler -> propfind ( self :: _slashify ( $options [ 'path' ]), $options , $files , $user , $id );
2011-09-21 22:08:21 +02:00
}
return '501 Not Implemented' ;
}
2011-09-18 12:56:56 +02:00
/**
* Add a collection to a PROPFIND request
*
* @ param string $path
2016-04-02 10:40:34 +02:00
* @ param array $props = array () extra properties 'resourcetype' is added anyway , name => value pairs or name => HTTP_WebDAV_Server ([ namespace ,] name , value )
* @ param array $privileges = array ( 'read' ) values for current - user - privilege - set
2023-07-21 08:54:06 +02:00
* @ param array | null $supported_privileges = null default $this -> supported_privileges
2011-09-18 12:56:56 +02:00
* @ return array with values for keys 'path' and 'props'
*/
2011-09-24 23:10:53 +02:00
public function add_collection ( $path , array $props = array (), array $privileges = array ( 'read' , 'read-acl' , 'read-current-user-privilege-set' ), array $supported_privileges = null )
2011-09-18 12:56:56 +02:00
{
// resourcetype: collection
$props [ 'resourcetype' ][] = self :: mkprop ( 'collection' , '' );
2011-09-21 22:08:21 +02:00
if ( ! isset ( $props [ 'getcontenttype' ])) $props [ 'getcontenttype' ] = 'httpd/unix-directory' ;
return $this -> add_resource ( $path , $props , $privileges , $supported_privileges );
}
/**
2011-09-28 17:41:42 +02:00
* Add a resource to a PROPFIND request
2011-09-21 22:08:21 +02:00
*
* @ param string $path
2016-04-02 10:40:34 +02:00
* @ param array $props = array () extra properties 'resourcetype' is added anyway , name => value pairs or name => HTTP_WebDAV_Server ([ namespace ,] name , value )
* @ param array $privileges = array ( 'read' ) values for current - user - privilege - set
* @ param array $supported_privileges = null default $this -> supported_privileges
2011-09-21 22:08:21 +02:00
* @ return array with values for keys 'path' and 'props'
*/
public function add_resource ( $path , array $props = array (), array $privileges = array ( 'read' , 'read-current-user-privilege-set' ), array $supported_privileges = null )
{
2011-09-18 12:56:56 +02:00
// props for all collections: current-user-principal and principal-collection-set
$props [ 'current-user-principal' ] = array (
self :: mkprop ( 'href' , $this -> current_user_principal ));
$props [ 'principal-collection-set' ] = array (
self :: mkprop ( 'href' , $this -> base_uri . '/principals/' ));
// required props per WebDAV standard
foreach ( array (
'displayname' => basename ( $path ),
2011-10-08 13:34:55 +02:00
'getetag' => 'none' ,
2011-09-18 12:56:56 +02:00
'getcontentlength' => '' ,
'getlastmodified' => '' ,
2011-09-21 22:08:21 +02:00
'getcontenttype' => '' ,
'resourcetype' => '' ,
2011-09-18 12:56:56 +02:00
) as $name => $default )
{
if ( ! isset ( $props [ $name ])) $props [ $name ] = $default ;
}
2011-09-21 22:08:21 +02:00
// if requested add privileges
if ( is_null ( $supported_privileges )) $supported_privileges = $this -> supported_privileges ;
if ( $this -> prop_requested ( 'current-user-privilege-set' ) === true )
{
foreach ( $privileges as $name )
{
$props [ 'current-user-privilege-set' ][] = self :: mkprop ( 'privilege' , array (
is_array ( $name ) ? self :: mkprop ( $name [ 'ns' ], $name [ 'name' ], '' ) : self :: mkprop ( $name , '' )));
}
}
if ( $this -> prop_requested ( 'supported-privilege-set' ) === true )
{
foreach ( $supported_privileges as $name => $data )
{
2011-09-22 20:46:16 +02:00
$props [ 'supported-privilege-set' ][] = $this -> supported_privilege ( $name , $data , $path );
2011-09-21 22:08:21 +02:00
}
}
if ( ! isset ( $props [ 'owner' ]) && $this -> prop_requested ( 'owner' ) === true )
{
$props [ 'owner' ] = '' ;
}
2011-09-18 12:56:56 +02:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (path=' $path ', props= " . array2string ( $props ) . ')' );
// convert simple associative properties to HTTP_WebDAV_Server ones
foreach ( $props as $name => & $prop )
{
if ( ! is_array ( $prop ) || ! isset ( $prop [ 'name' ]))
{
$prop = self :: mkprop ( $name , $prop );
}
2011-10-05 10:15:24 +02:00
// add quotes around etag, if they are not already there
if ( $prop [ 'name' ] == 'getetag' && $prop [ 'val' ][ 0 ] != '"' )
{
$prop [ 'val' ] = '"' . $prop [ 'val' ] . '"' ;
}
2011-09-18 12:56:56 +02:00
}
return array (
'path' => $path ,
'props' => $props ,
);
}
2008-05-08 22:31:32 +02:00
/**
2023-07-21 17:41:37 +02:00
* Generate ( hierarchical ) supported - privilege property
2008-05-08 22:31:32 +02:00
*
2011-09-21 22:08:21 +02:00
* @ param string $name name of privilege
2023-07-21 08:54:06 +02:00
* @ param string | array $data string with description or array with aggregated privileges plus value for key '*description*' , '*ns*' , '*only*'
2016-04-02 10:40:34 +02:00
* @ param string $path = null path to match with $data [ '*only*' ]
2011-09-21 22:08:21 +02:00
* @ return array of self :: mkprop () arrays
2008-05-08 22:31:32 +02:00
*/
2011-09-22 20:46:16 +02:00
protected function supported_privilege ( $name , $data , $path = null )
2008-05-08 22:31:32 +02:00
{
2011-09-21 22:08:21 +02:00
$props = array ();
2011-09-22 20:46:16 +02:00
$props [] = self :: mkprop ( 'privilege' , array ( is_array ( $data ) && $data [ '*ns*' ] ?
self :: mkprop ( $data [ '*ns*' ], $name , '' ) : self :: mkprop ( $name , '' )));
2011-09-21 22:08:21 +02:00
$props [] = self :: mkprop ( 'description' , is_array ( $data ) ? $data [ '*description*' ] : $data );
if ( is_array ( $data ))
2008-05-08 22:31:32 +02:00
{
2023-07-21 08:54:06 +02:00
foreach ( $data as $n => $d )
2010-10-10 00:49:10 +02:00
{
2023-07-21 08:54:06 +02:00
if ( $n [ 0 ] == '*' ) continue ;
if ( is_array ( $d ) && $d [ '*only*' ] && strpos ( $path , $d [ '*only*' ]) === false )
2011-09-22 20:46:16 +02:00
{
continue ; // wrong path
}
2023-07-21 08:54:06 +02:00
$props [] = $this -> supported_privilege ( $n , $d , $path );
2008-05-08 22:31:32 +02:00
}
}
2011-09-21 22:08:21 +02:00
return self :: mkprop ( 'supported-privilege' , $props );
}
/**
* Checks if a given property was requested in propfind request
*
* @ param string $name property name
2016-04-02 10:40:34 +02:00
* @ param string $ns = null namespace , if that is to be checked too
* @ param boolean $return_prop = false if true return the property array with values for 'name' , 'xmlns' , 'attrs' , 'children'
2012-01-30 06:11:05 +01:00
* @ return boolean | string | array true : $name explicitly requested ( or autoindex ), " all " : allprop or " names " : propname requested , false : $name was not requested
2011-09-21 22:08:21 +02:00
*/
2012-01-24 06:27:26 +01:00
function prop_requested ( $name , $ns = null , $return_prop = false )
2011-09-21 22:08:21 +02:00
{
if ( ! is_array ( $this -> propfind_options ) || ! isset ( $this -> propfind_options [ 'props' ]))
2008-05-08 22:31:32 +02:00
{
2012-01-30 06:11:05 +01:00
$ret = true ; // no props set, should happen only in autoindex, we return true to show all available props
2008-05-08 22:31:32 +02:00
}
2012-01-30 06:11:05 +01:00
elseif ( ! is_array ( $this -> propfind_options [ 'props' ]))
2008-05-08 22:31:32 +02:00
{
2012-01-30 06:11:05 +01:00
$ret = $this -> propfind_options [ 'props' ]; // "all": allprop or "names": propname
}
else
{
$ret = false ;
foreach ( $this -> propfind_options [ 'props' ] as $prop )
2008-05-08 22:31:32 +02:00
{
2012-01-30 06:11:05 +01:00
if ( $prop [ 'name' ] == $name && ( is_null ( $ns ) || $prop [ 'xmlns' ] == $ns ))
{
$ret = $return_prop ? $prop : true ;
break ;
}
2008-05-10 22:32:03 +02:00
}
2008-05-08 22:31:32 +02:00
}
2012-01-30 06:11:05 +01:00
//error_log(__METHOD__."('$name', '$ns', $return_prop) propfind_options=".array2string($this->propfind_options));
2011-09-21 22:08:21 +02:00
return $ret ;
2008-05-08 22:31:32 +02:00
}
2011-09-20 21:16:24 +02:00
/**
* Add user home with addressbook , calendar , infolog
*
* @ param array $files
* @ param string $path / or /< username >/
* @ param int $user
* @ param int $depth
* @ return string | boolean http status or true | false
*/
protected function add_home ( array & $files , $path , $user , $depth )
{
if ( $user )
{
$account_lid = $this -> accounts -> id2name ( $user );
}
else
{
$account_lid = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ];
}
$account = $this -> accounts -> read ( $account_lid );
$calendar_user_address_set = array (
self :: mkprop ( 'href' , 'urn:uuid:' . $account [ 'account_lid' ]),
);
if ( $user < 0 )
{
$principalType = 'groups' ;
$displayname = lang ( 'Group' ) . ' ' . $account [ 'account_lid' ];
}
else
{
$principalType = 'users' ;
$displayname = $account [ 'account_fullname' ];
2015-01-23 10:39:34 +01:00
$calendar_user_address_set [] = self :: mkprop ( 'href' , 'mailto:' . $account [ 'account_email' ]);
2011-09-20 21:16:24 +02:00
}
$calendar_user_address_set [] = self :: mkprop ( 'href' , $this -> base_uri . '/principals/' . $principalType . '/' . $account [ 'account_lid' ] . '/' );
if ( $depth && $path == '/' )
{
$displayname = 'EGroupware (Cal|Card|Group)DAV server' ;
}
2016-04-02 12:44:17 +02:00
$displayname = Translation :: convert ( $displayname , Translation :: charset (), 'utf-8' );
2011-09-20 21:16:24 +02:00
// self url
2012-02-02 00:26:16 +01:00
$props = array (
2011-09-20 21:16:24 +02:00
'displayname' => $displayname ,
2011-09-21 22:08:21 +02:00
'owner' => $path == '/' ? '' : array ( self :: mkprop ( 'href' , $this -> base_uri . '/principals/' . $principalType . '/' . $account_lid . '/' )),
2012-02-02 00:26:16 +01:00
);
if ( $path != '/' )
{
2023-07-21 08:54:06 +02:00
// add props modifyable via proppatch from client, e.g. jqcalendar stores it's preferences there
2012-02-02 00:26:16 +01:00
foreach (( array ) $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ] as $name => $value )
{
list ( $prop , $prop4path , $ns ) = explode ( ':' , $name , 3 );
2012-09-26 12:01:02 +02:00
if ( $prop4path == $path && ( ! in_array ( $ns , self :: $ns_needs_explicit_named_props ) ||
isset ( self :: $proppatch_props [ $prop ]) && self :: $proppatch_props [ $prop ] === $ns ))
2012-02-02 00:26:16 +01:00
{
$props [] = self :: mkprop ( $ns , $prop , $value );
//error_log(__METHOD__."() arbitrary $ns:$prop=".array2string($value));
}
}
}
$files [ 'files' ][] = $this -> add_collection ( $path , $props );
2011-09-20 21:16:24 +02:00
if ( $depth )
{
foreach ( $this -> root as $app => $data )
{
2011-09-21 22:08:21 +02:00
if ( ! $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ $data [ 'app' ] ? $data [ 'app' ] : $app ]) continue ; // no rights for the given app
2011-09-22 17:22:52 +02:00
if ( ! empty ( $data [ 'user-only' ]) && ( $path == '/' || $user < 0 )) continue ;
2011-09-21 22:08:21 +02:00
2012-01-25 04:25:42 +01:00
$files [ 'files' ][] = $this -> add_app ( $app , false , $user , $path . $app . '/' );
2012-01-30 06:11:05 +01:00
// only add global /addressbook-accounts/ as the one in home-set is added (and controled) by add_shared
if ( $path == '/' && $app == 'addressbook' &&
2017-12-01 14:58:44 +01:00
$GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] !== '1' )
2012-01-30 06:11:05 +01:00
{
2012-02-03 19:21:20 +01:00
$file = $this -> add_app ( $app , false , 0 , $path . $app . '-accounts/' );
$file [ 'props' ][ 'resourcetype' ][ 'val' ][] = self :: mkprop ( self :: CALENDARSERVER , 'shared' , '' );
$files [ 'files' ][] = $file ;
2012-01-30 06:11:05 +01:00
}
2012-01-25 04:25:42 +01:00
// added shared calendars or addressbooks
$this -> add_shared ( $files [ 'files' ], $path , $app , $user );
2011-09-20 21:16:24 +02:00
}
2012-09-27 17:46:08 +02:00
if ( $path == '/' && $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'resources' ])
{
$this -> add_resources_collection ( $files , $path . 'resources/' );
$this -> add_resources_collection ( $files , $path . 'locations/' );
}
}
return true ;
}
/**
* Add collection with available resources or locations calendar - home - sets
*
* @ param array & $files
* @ param string $path / or /< username >/
2016-04-02 10:40:34 +02:00
* @ param int $depth = 0
2012-09-27 17:46:08 +02:00
* @ return string | boolean http status or true | false
*/
protected function add_resources_collection ( array & $files , $path , $depth = 0 )
{
if ( ! isset ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'resources' ]))
{
2016-04-02 10:40:34 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (path= $path ) 403 Forbidden: no app rights for 'resources' " );
2012-09-27 17:46:08 +02:00
return " 403 Forbidden: no app rights for 'resources' " ; // no rights for the given app
}
list (, $what ) = explode ( '/' , $path );
if (( $is_location = ( $what == 'locations' )))
{
$files [ 'files' ][] = $this -> add_collection ( '/locations/' , array ( 'displayname' => lang ( 'Location calendars' )));
}
else
{
$files [ 'files' ][] = $this -> add_collection ( '/resources/' , array ( 'displayname' => lang ( 'Resource calendars' )));
}
if ( $depth )
{
2016-04-02 12:44:17 +02:00
foreach ( Principals :: get_resources () as $resource )
2012-09-27 17:46:08 +02:00
{
2016-04-02 12:44:17 +02:00
if ( $is_location == Principals :: resource_is_location ( $resource ))
2012-09-27 17:46:08 +02:00
{
$files [ 'files' ][] = $this -> add_app ( 'calendar' , false , 'r' . $resource [ 'res_id' ],
2016-04-02 12:44:17 +02:00
'/' . Principals :: resource2name ( $resource , $is_location ) . '/' );
2012-09-27 17:46:08 +02:00
}
}
2011-09-20 21:16:24 +02:00
}
return true ;
}
2012-01-25 04:25:42 +01:00
/**
* Add shared addressbook , calendar , infolog to user home
*
* @ param array & $files
* @ param string $path /< username >/
* @ param int $app
* @ param int $user
* @ return string | boolean http status or true | false
*/
protected function add_shared ( array & $files , $path , $app , $user )
{
// currently only show shared calendars/addressbooks for current user and not in the root
2012-02-04 02:03:56 +01:00
if ( $path == '/' || $user != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] ||
! isset ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ $app ])) // also avoids principals, inbox and outbox
2012-01-25 04:25:42 +01:00
{
return true ;
}
2012-02-04 02:03:56 +01:00
$handler = $this -> app_handler ( $app );
if (( $shared = $handler -> get_shared ()))
{
foreach ( $shared as $id => $owner )
{
2012-09-27 17:46:08 +02:00
$file = $this -> add_app ( $app , false , $id , $path . $owner . '/' );
2012-02-09 21:09:49 +01:00
// mark other users calendar as shared (iOS 5.0.1 AB does NOT display AB marked as shared!)
if ( $app == 'calendar' ) $file [ 'props' ][ 'resourcetype' ][ 'val' ][] = self :: mkprop ( self :: CALENDARSERVER , 'shared' , '' );
2012-02-04 02:03:56 +01:00
$files [] = $file ;
}
2012-01-25 04:25:42 +01:00
}
return true ;
}
/**
* Format an account - name for use in displayname
*
* @ param int | array $account
* @ return string
*/
public function account_name ( $account )
{
if ( is_array ( $account ))
{
if ( $account [ 'account_id' ] < 0 )
{
$name = lang ( 'Group' ) . ' ' . $account [ 'account_lid' ];
}
else
{
$name = $account [ 'account_fullname' ];
}
}
else
{
if ( $account < 0 )
{
$name = lang ( 'Group' ) . ' ' . $this -> accounts -> id2name ( $account , 'account_lid' );
}
else
{
$name = $this -> accounts -> id2name ( $account , 'account_fullname' );
}
if ( empty ( $name )) $name = '#' . $account ;
}
return $name ;
}
2008-05-08 22:31:32 +02:00
/**
2011-09-21 22:08:21 +02:00
* Add an application collection to a user home or the root
2008-05-08 22:31:32 +02:00
*
* @ param string $app
2016-04-02 10:40:34 +02:00
* @ param boolean $no_extra_types = false should the GroupDAV and CalDAV types be added ( KAddressbook has problems with it in self URL )
* @ param int $user = null owner of the collection , default current user
* @ param string $path = '/'
2011-09-21 22:08:21 +02:00
* @ return array with values for keys 'path' and 'props'
2008-05-08 22:31:32 +02:00
*/
2011-09-21 22:08:21 +02:00
protected function add_app ( $app , $no_extra_types = false , $user = null , $path = '/' )
2008-05-08 22:31:32 +02:00
{
2010-11-03 11:05:08 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " (app=' $app ', no_extra_types= $no_extra_types , user=' $user ', path=' $path ') " );
2011-02-13 22:08:29 +01:00
$user_preferences = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ];
2016-04-02 12:44:17 +02:00
if ( is_string ( $user ) && $user [ 0 ] == 'r' && ( $resource = Principals :: read_resource ( substr ( $user , 1 ))))
2012-09-27 17:46:08 +02:00
{
2016-04-02 12:44:17 +02:00
$is_location = Principals :: resource_is_location ( $resource );
2016-04-02 10:40:34 +02:00
$displayname = null ;
2016-04-02 12:44:17 +02:00
list ( $principalType , $account_lid ) = explode ( '/' , Principals :: resource2name ( $resource , $is_location , $displayname ));
2012-09-27 17:46:08 +02:00
}
elseif ( $user )
2010-03-07 00:06:43 +01:00
{
$account_lid = $this -> accounts -> id2name ( $user );
2011-02-13 22:08:29 +01:00
if ( $user >= 0 && $GLOBALS [ 'egw' ] -> preferences -> account_id != $user )
{
$GLOBALS [ 'egw' ] -> preferences -> __construct ( $user );
$user_preferences = $GLOBALS [ 'egw' ] -> preferences -> read_repository ();
$GLOBALS [ 'egw' ] -> preferences -> __construct ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ]);
}
2012-09-27 17:46:08 +02:00
$principalType = $user < 0 ? 'groups' : 'users' ;
2010-03-07 00:06:43 +01:00
}
else
{
$account_lid = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ];
2010-09-18 10:45:46 +02:00
$principalType = 'users' ;
}
2012-09-27 17:46:08 +02:00
if ( ! isset ( $displayname )) $displayname = $this -> account_name ( $user );
2010-10-31 08:56:29 +01:00
2008-08-04 21:08:09 +02:00
$props = array (
2011-09-18 12:56:56 +02:00
'owner' => array ( self :: mkprop ( 'href' , $this -> base_uri . '/principals/' . $principalType . '/' . $account_lid . '/' )),
2010-03-07 00:06:43 +01:00
);
switch ( $app )
{
2011-09-21 22:08:21 +02:00
case 'inbox' :
2012-09-27 17:46:08 +02:00
$props [ 'displayname' ] = lang ( 'Scheduling inbox' ) . ' ' . $displayname ;
2011-09-21 22:08:21 +02:00
break ;
case 'outbox' :
2012-09-27 17:46:08 +02:00
$props [ 'displayname' ] = lang ( 'Scheduling outbox' ) . ' ' . $displayname ;
2011-09-21 22:08:21 +02:00
break ;
2012-01-25 04:25:42 +01:00
case 'addressbook' :
if ( $path == '/addressbook/' )
{
$props [ 'displayname' ] = lang ( 'All addressbooks' );
break ;
}
2017-12-01 14:58:44 +01:00
elseif ( ! $user && $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] !== '1' )
2012-01-25 04:25:42 +01:00
{
unset ( $props [ 'owner' ]);
$props [ 'displayname' ] = lang ( $app ) . ' ' . lang ( 'Accounts' );
break ;
}
// fall through
2010-03-07 00:06:43 +01:00
default :
2016-04-02 12:44:17 +02:00
$props [ 'displayname' ] = Translation :: convert ( lang ( $app ) . ' ' . $displayname , $this -> egw_charset , 'utf-8' );
2011-11-09 14:23:53 +01:00
}
2012-02-04 21:24:01 +01:00
// rfc 5995 (Use POST to add members to WebDAV collections): we use collection path with add-member query param
2013-09-24 14:29:17 +02:00
// leaving it switched off, until further testing, because OS X iCal seem to ignore it and OS X Addressbook uses POST to full URL without ?add-member
2012-02-04 21:24:01 +01:00
if ( $app && ! in_array ( $app , array ( 'inbox' , 'outbox' , 'principals' ))) // not on inbox, outbox or principals
{
$props [ 'add-member' ][] = self :: mkprop ( 'href' , $this -> base_uri . $path . '?add-member' );
2013-09-24 14:29:17 +02:00
}
2012-02-04 21:24:01 +01:00
2023-07-21 08:54:06 +02:00
// add props modifiable via proppatch from client, e.g. calendar-color, see self::$proppatch_props
2016-04-02 10:40:34 +02:00
$ns = null ;
2011-11-09 14:23:53 +01:00
foreach (( array ) $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ $app ] as $name => $value )
{
2012-01-30 06:11:05 +01:00
unset ( $ns );
list ( $prop , $prop4user , $ns ) = explode ( ':' , $name , 3 );
if ( $prop4user == ( string ) $user && isset ( self :: $proppatch_props [ $prop ]) && ! isset ( $ns ))
2011-11-09 14:23:53 +01:00
{
$props [ $prop ] = self :: mkprop ( self :: $proppatch_props [ $prop ], $prop , $value );
2012-01-30 06:11:05 +01:00
//error_log(__METHOD__."() explicit ".self::$proppatch_props[$prop].":$prop=".array2string($value));
}
// props in arbitrary namespaces not mentioned in self::$ns_needs_explicit_named_props
elseif ( isset ( $ns ) && ! in_array ( $ns , self :: $ns_needs_explicit_named_props ))
{
$props [] = self :: mkprop ( $ns , $prop , $value );
//error_log(__METHOD__."() arbitrary $ns:$prop=".array2string($value));
2011-11-09 14:23:53 +01:00
}
2010-03-07 00:06:43 +01:00
}
2009-10-03 12:22:14 +02:00
foreach (( array ) $this -> root [ $app ] as $prop => $values )
2008-05-08 22:31:32 +02:00
{
2011-09-21 22:08:21 +02:00
switch ( $prop )
2008-08-04 21:08:09 +02:00
{
2011-09-21 22:08:21 +02:00
case 'resourcetype' ;
if ( ! $no_extra_types )
2008-08-04 21:08:09 +02:00
{
2011-09-21 22:08:21 +02:00
foreach ( $this -> root [ $app ][ 'resourcetype' ] as $ns => $type )
{
$props [ 'resourcetype' ][] = self :: mkprop ( $ns , $type , '' );
}
// add /addressbook/ as directory gateway
2012-01-30 19:53:47 +01:00
if ( $path == '/addressbook/' )
2011-09-21 22:08:21 +02:00
{
$props [ 'resourcetype' ][] = self :: mkprop ( self :: CARDDAV , 'directory' , '' );
}
2008-08-04 21:08:09 +02:00
}
2011-09-21 22:08:21 +02:00
break ;
case 'app' :
2011-09-22 17:22:52 +02:00
case 'user-only' :
2011-09-21 22:08:21 +02:00
break ; // no props, already handled
default :
if ( is_array ( $values ))
2011-09-20 21:16:24 +02:00
{
2011-09-21 22:08:21 +02:00
foreach ( $values as $ns => $value )
{
$props [ $prop ] = self :: mkprop ( $ns , $prop , $value );
}
2011-09-20 21:16:24 +02:00
}
2011-09-21 22:08:21 +02:00
else
{
$props [ $prop ] = $values ;
}
break ;
2008-05-08 22:31:32 +02:00
}
}
2011-10-20 15:35:01 +02:00
// add other handler specific properties
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2011-09-21 22:08:21 +02:00
{
2011-10-20 15:35:01 +02:00
if ( method_exists ( $handler , 'extra_properties' ))
{
2012-09-27 17:46:08 +02:00
$props = $handler -> extra_properties ( $props , $displayname , $this -> base_uri , $user , $path );
2011-10-20 15:35:01 +02:00
}
// add ctag if handler implements it
2011-09-21 22:08:21 +02:00
if ( method_exists ( $handler , 'getctag' ) && $this -> prop_requested ( 'getctag' ) === true )
{
$props [ 'getctag' ] = self :: mkprop (
2016-04-02 12:44:17 +02:00
self :: CALENDARSERVER , 'getctag' , $handler -> getctag ( $path , $user ));
2011-09-21 22:08:21 +02:00
}
2012-09-24 12:26:29 +02:00
// add sync-token url if handler supports sync-collection report
if ( isset ( $props [ 'supported-report-set' ][ 'sync-collection' ]) && $this -> prop_requested ( 'sync-token' ) === true )
2012-09-23 22:19:35 +02:00
{
2012-09-24 12:26:29 +02:00
$props [ 'sync-token' ] = $handler -> get_sync_token ( $path , $user );
2012-09-23 22:19:35 +02:00
}
2011-09-21 22:08:21 +02:00
}
2013-01-22 09:37:58 +01:00
if ( $handler && ! is_null ( $user ))
2012-01-25 04:25:42 +01:00
{
return $this -> add_collection ( $path , $props , $handler -> current_user_privileges ( $path , $user ));
}
return $this -> add_collection ( $path , $props );
2008-05-08 22:31:32 +02:00
}
/**
* CalDAV / CardDAV REPORT method handler
*
* just calls PROPFIND ()
*
2023-07-21 08:54:06 +02:00
* @ param array & $options general parameter passing array
* @ param array & $files return array for file properties
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function REPORT ( & $options , & $files )
{
2008-05-17 14:54:26 +02:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
return $this -> PROPFIND ( $options , $files , 'REPORT' );
}
/**
* CalDAV / CardDAV REPORT method handler to get HTTP_WebDAV_Server to process REPORT requests
*
* Just calls http_PROPFIND ()
*/
function http_REPORT ()
{
parent :: http_PROPFIND ( 'REPORT' );
}
2021-09-25 12:20:31 +02:00
/**
* REST API PATCH handler
*
* Currently , only implemented for REST not CalDAV / CardDAV
*
2023-07-21 08:54:06 +02:00
* @ param array & $options
* @ return string
2021-09-25 12:20:31 +02:00
*/
function PATCH ( array & $options )
{
2024-02-06 15:39:12 +01:00
if ( ! self :: isJSON ())
2021-09-25 12:20:31 +02:00
{
return '501 Not implemented' ;
}
return $this -> PUT ( $options , 'PATCH' );
}
/**
* REST API PATCH handler
*
* Just calls http_PUT ()
*/
function http_PATCH ()
{
return parent :: http_PUT ( 'PATCH' );
}
2021-09-16 20:53:43 +02:00
/**
2021-09-19 11:09:44 +02:00
* Check if client want or sends JSON
2021-09-16 20:53:43 +02:00
*
2023-07-21 08:54:06 +02:00
* @ param string | null & $type = null
2021-09-19 11:09:44 +02:00
* @ return bool | string false : no json , true : application / json , string : application / ( string ) + json
2021-09-16 20:53:43 +02:00
*/
2021-09-20 16:01:22 +02:00
public static function isJSON ( string & $type = null )
2021-09-16 20:53:43 +02:00
{
if ( ! isset ( $type ))
{
2021-09-25 12:20:31 +02:00
$type = in_array ( $_SERVER [ 'REQUEST_METHOD' ], [ 'PUT' , 'POST' , 'PATCH' , 'PROPPATCH' ]) ?
2021-09-16 20:53:43 +02:00
$_SERVER [ 'HTTP_CONTENT_TYPE' ] : $_SERVER [ 'HTTP_ACCEPT' ];
}
2023-07-27 20:50:14 +02:00
// make sure the client is not just a CalDAV client wrongly sending a Content-Type or Accept header for JSON
if ( in_array ( $_SERVER [ 'REQUEST_METHOD' ], [ 'REPORT' , 'PROPFIND' , 'PROPPATCH' ]) || // no REST, but CalDAV methods
2023-07-28 09:43:25 +02:00
isset ( $_SERVER [ 'HTTP_CONTENT_TYPE' ]) && preg_match ( '#(application|text)/xml#' , $_SERVER [ 'HTTP_CONTENT_TYPE' ]))
2023-07-27 20:50:14 +02:00
{
return false ;
}
2021-09-19 11:09:44 +02:00
return preg_match ( '#application/(([^+ ;]+)\+)?json#' , $type , $matches ) ?
( empty ( $matches [ 1 ]) ? true : $matches [ 2 ]) : false ;
2021-09-16 20:53:43 +02:00
}
2008-05-08 22:31:32 +02:00
/**
* GET method handler
*
2009-08-16 17:24:43 +02:00
* @ param array $options parameter passing array
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function GET ( & $options )
{
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2009-10-03 12:22:14 +02:00
if ( ! $this -> _parse_path ( $options [ 'path' ], $id , $app , $user ) || $app == 'principals' )
2008-05-08 22:31:32 +02:00
{
2021-09-19 11:09:44 +02:00
if (( $json = self :: isJSON ()))
2021-09-16 20:53:43 +02:00
{
2021-09-19 11:09:44 +02:00
return $this -> jsonIndex ( $options , $json === 'pretty' );
2021-09-16 20:53:43 +02:00
}
2009-08-16 17:24:43 +02:00
return $this -> autoindex ( $options );
2008-05-08 22:31:32 +02:00
}
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2008-05-08 22:31:32 +02:00
{
2024-02-05 20:06:18 +01:00
// handle links for all apps supporting links
if ( preg_match ( '#/' . $app . '/' . $id . '/links/?$#' , $options [ 'path' ]) && self :: isJSON ())
{
return $handler -> getLinks ( $options , $id );
}
2011-03-05 11:21:32 +01:00
return $handler -> get ( $options , $id , $user );
2008-05-08 22:31:32 +02:00
}
2009-08-16 17:24:43 +02:00
error_log ( __METHOD__ . " ( " . array2string ( $options ) . " ) 501 Not Implemented " );
2008-05-08 22:31:32 +02:00
return '501 Not Implemented' ;
}
2021-09-16 20:53:43 +02:00
const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR ;
const JSON_OPTIONS_PRETTY = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR ;
/**
* JSON encode incl . modified pretty - print
*
* @ param $data
* @ return array | string | string [] | null
*/
public static function json_encode ( $data , $pretty = true )
{
if ( ! $pretty )
{
2021-09-19 11:09:44 +02:00
return json_encode ( $data , self :: JSON_OPTIONS );
2021-09-16 20:53:43 +02:00
}
return preg_replace ( '/: {\n\s*(.*?)\n\s*(},?\n)/' , ': { $1 $2' ,
json_encode ( $data , self :: JSON_OPTIONS_PRETTY ));
}
/**
* PROPFIND / REPORT like output for GET request on collection with Accept : application / ( .*+ ) ? json
*
* For addressbook - collections we give a REST - like output without any other properties
* {
* " /addressbook/ID " : {
* JsContact - data
* },
* ...
* }
*
* @ param array $options
2021-09-19 11:09:44 +02:00
* @ param bool $pretty = false true : pretty - print JSON
2021-09-16 20:53:43 +02:00
* @ return bool | string | void
*/
2021-09-19 11:09:44 +02:00
protected function jsonIndex ( array $options , bool $pretty )
2021-09-16 20:53:43 +02:00
{
header ( 'Content-Type: application/json; charset=utf-8' );
$is_addressbook = strpos ( $options [ 'path' ], '/addressbook' ) !== false ;
2024-05-03 19:55:47 +02:00
$is_calendar = ( bool ) preg_match ( '#/(calendar|infolog)#' , $options [ 'path' ]);
2021-09-16 20:53:43 +02:00
$propfind_options = array (
'path' => $options [ 'path' ],
'depth' => 1 ,
'props' => $is_addressbook ? [
'address-data' => self :: mkprop ( self :: CARDDAV , 'address-data' , '' )
2023-07-21 17:41:37 +02:00
] : ( $is_calendar ? [
'calendar-data' => self :: mkprop ( self :: CALDAV , 'calendar-data' , '' ),
2023-11-29 14:47:27 +01:00
] : [
'data' => self :: mkprop ( self :: CALDAV , 'data' , '' )
]),
2021-09-16 20:53:43 +02:00
'other' => [],
2024-02-06 11:52:02 +01:00
'root' => [ 'name' => null ],
2021-09-16 20:53:43 +02:00
);
// sync-collection report via GET parameter sync-token
if ( isset ( $_GET [ 'sync-token' ]))
{
$propfind_options [ 'root' ] = [ 'name' => 'sync-collection' ];
$propfind_options [ 'other' ][] = [ 'name' => 'sync-token' , 'data' => $_GET [ 'sync-token' ]];
$propfind_options [ 'other' ][] = [ 'name' => 'sync-level' , 'data' => $_GET [ 'sync-level' ] ? ? 1 ];
// clients want's pagination
if ( isset ( $_GET [ 'nresults' ]))
{
$propfind_options [ 'other' ][] = [ 'name' => 'nresults' , 'data' => ( int ) $_GET [ 'nresults' ]];
}
}
2023-10-19 20:34:38 +02:00
// client want data filtered
2021-09-16 20:53:43 +02:00
if ( isset ( $_GET [ 'filters' ]))
{
2023-10-19 20:34:38 +02:00
$propfind_options [ 'filters' ] = $_GET [ 'filters' ];
2021-09-16 20:53:43 +02:00
}
// properties to NOT get the default address-data for addressbook-collections and "all" for the rest
if ( isset ( $_GET [ 'props' ]))
{
$propfind_options [ 'props' ] = [];
foreach (( array ) $_GET [ 'props' ] as $value )
{
$parts = explode ( ':' , $value );
$name = array_pop ( $parts );
$ns = $parts ? implode ( ':' , $parts ) : 'DAV:' ;
$propfind_options [ 'props' ][ $name ] = self :: mkprop ( $ns , $name , '' );
}
}
$files = array ();
if (( $ret = $this -> REPORT ( $propfind_options , $files )) !== true )
{
return $ret ; // no collection
}
2024-02-02 08:33:20 +01:00
// nicer JSON formatting for application/pretty+json only
$tab = $nl = $sp = '' ;
if ( strpos ( $_SERVER [ 'HTTP_ACCEPT' ], 'application/pretty+json' ) !== false )
{
$tab = " \t " ;
$nl = " \n " ;
$sp = ' ' ;
}
2021-09-19 11:09:44 +02:00
// set start as prefix, to no have it in front of exceptions
2024-02-02 08:33:20 +01:00
$prefix = '{' . $nl . $tab . '"responses":' . $sp . '{' . $nl ;
2021-09-16 20:53:43 +02:00
foreach ( $files [ 'files' ] as $resource )
{
$path = $resource [ 'path' ];
2024-02-02 08:33:20 +01:00
echo $prefix . json_encode ( $path , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . ':' . $sp ;
2021-09-16 20:53:43 +02:00
if ( ! isset ( $resource [ 'props' ]))
{
echo 'null' ; // deleted in sync-report
}
else
{
$props = $propfind_options [ 'props' ] === 'all' ? $resource [ 'props' ] :
array_intersect_key ( $resource [ 'props' ], $propfind_options [ 'props' ]);
if ( count ( $props ) > 1 )
{
2023-07-21 08:54:06 +02:00
$props = $this -> jsonProps ( $props );
2021-09-16 20:53:43 +02:00
}
else
{
$props = current ( $props )[ 'val' ];
}
2021-09-19 11:09:44 +02:00
echo self :: json_encode ( $props , $pretty );
2021-09-16 20:53:43 +02:00
}
2024-02-02 08:33:20 +01:00
$prefix = " , $nl " ;
2021-09-19 11:09:44 +02:00
}
// happens with an empty response
2024-02-02 08:33:20 +01:00
if ( $prefix !== " , $nl " )
2021-09-19 11:09:44 +02:00
{
echo $prefix ;
2024-02-02 08:33:20 +01:00
$prefix = " , $nl " ;
2021-09-16 20:53:43 +02:00
}
2024-02-02 08:33:20 +01:00
echo " $nl $tab } " ;
2021-09-19 11:09:44 +02:00
// add sync-token and more-results to response
2021-09-16 20:53:43 +02:00
if ( isset ( $files [ 'sync-token' ]))
{
2024-02-02 08:33:20 +01:00
echo $prefix . $tab . '"sync-token": ' . json_encode ( ! is_callable ( $files [ 'sync-token' ]) ? $files [ 'sync-token' ] :
2021-09-16 20:53:43 +02:00
call_user_func_array ( $files [ 'sync-token' ], ( array ) $files [ 'sync-token-params' ]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
}
2024-02-02 08:33:20 +01:00
echo " $nl } " ;
2021-09-16 20:53:43 +02:00
// exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream
exit ;
}
/**
* Nicer way to display / encode DAV properties
*
* @ param array $props
* @ return array
*/
protected function jsonProps ( array $props )
{
$json = [];
foreach ( $props as $key => $prop )
{
if ( is_scalar ( $prop [ 'val' ]))
{
$value = is_int ( $key ) && $prop [ 'val' ] === '' ?
/*$prop['ns'].':'.*/ $prop [ 'name' ] : $prop [ 'val' ];
}
// check if this is a property-object
elseif ( count ( $prop ) === 3 && isset ( $prop [ 'name' ]) && isset ( $prop [ 'ns' ]) && isset ( $prop [ 'val' ]))
{
2023-11-29 14:47:27 +01:00
$value = in_array ( $prop [ 'name' ], [ 'address-data' , 'calendar-data' , 'data' ]) ? $prop [ 'val' ] : self :: jsonProps ( $prop [ 'val' ]);
2021-09-16 20:53:43 +02:00
}
else
{
$value = $prop ;
}
if ( is_int ( $key ))
{
$json [] = $value ;
}
else
{
$json [ /*($prop['ns'] === 'DAV:' ? '' : $prop['ns'].':').*/ $prop [ 'name' ]] = $value ;
}
}
return $json ;
}
2009-08-16 17:24:43 +02:00
/**
* Display an automatic index ( listing and properties ) for a collection
*
* @ param array $options parameter passing array , index " path " contains requested path
*/
protected function autoindex ( $options )
{
2022-11-07 20:50:45 +01:00
$chunk_size = 500 ;
2009-08-16 17:24:43 +02:00
$propfind_options = array (
'path' => $options [ 'path' ],
'depth' => 1 ,
2023-08-09 15:19:31 +02:00
// do NOT limit response, if GET parameter download is given
'other' => isset ( $_GET [ 'download' ]) ? [] : [[ 'name' => 'nresults' , 'data' => $chunk_size ]],
2009-08-16 17:24:43 +02:00
);
$files = array ();
if (( $ret = $this -> PROPFIND ( $propfind_options , $files )) !== true )
{
return $ret ; // no collection
}
2016-04-02 12:44:17 +02:00
header ( 'Content-type: text/html; charset=' . Translation :: charset ());
2009-08-16 17:24:43 +02:00
echo " <html> \n <head> \n \t <title> " . 'EGroupware (Cal|Card|Group)DAV server ' . htmlspecialchars ( $options [ 'path' ]) . " </title> \n " ;
echo " \t <meta http-equiv='content-type' content='text/html; charset=utf-8' /> \n " ;
2011-09-21 22:08:21 +02:00
echo " \t <style type='text/css'> \n .th { background-color: #e0e0e0; } \n .row_on { background-color: #F1F1F1; vertical-align: top; } \n " .
" .row_off { background-color: #ffffff; vertical-align: top; } \n td { padding-left: 5px; } \n th { padding-left: 5px; text-align: left; } \n \t </style> \n " ;
2009-08-16 17:24:43 +02:00
echo " </head> \n <body> \n " ;
echo '<h1>(Cal|Card|Group)DAV ' ;
$path = '/groupdav.php' ;
2023-07-21 08:54:06 +02:00
foreach ( explode ( '/' , self :: _unslashify ( $options [ 'path' ])) as $n => $name )
2009-08-16 17:24:43 +02:00
{
$path .= ( $n != 1 ? '/' : '' ) . $name ;
2016-04-02 12:44:17 +02:00
echo Html :: a_href ( htmlspecialchars ( $name . '/' ), $path );
2009-08-16 17:24:43 +02:00
}
echo " </h1> \n " ;
2009-10-03 12:22:14 +02:00
2012-01-25 04:25:42 +01:00
static $props2show = array (
2012-02-07 13:47:49 +01:00
'DAV:displayname' => 'Displayname' ,
2012-01-25 04:25:42 +01:00
'DAV:getlastmodified' => 'Last modified' ,
'DAV:getetag' => 'ETag' ,
2012-10-29 13:23:17 +01:00
//'CalDAV:schedule-tag' => 'Schedule-Tag',
2012-01-25 04:25:42 +01:00
'DAV:getcontenttype' => 'Content type' ,
'DAV:resourcetype' => 'Resource type' ,
2012-10-08 13:20:29 +02:00
//'http://calendarserver.org/ns/:created-by' => 'Created by',
//'http://calendarserver.org/ns/:updated-by' => 'Updated by',
2012-01-25 04:25:42 +01:00
//'DAV:owner' => 'Owner',
//'DAV:current-user-privilege-set' => 'current-user-privilege-set',
2012-02-07 13:47:49 +01:00
//'DAV:getcontentlength' => 'Size',
2012-09-26 12:01:02 +02:00
//'DAV:sync-token' => 'sync-token',
2012-01-25 04:25:42 +01:00
);
2009-10-17 14:22:40 +02:00
$n = 0 ;
2021-03-31 17:49:43 +02:00
$collection_props = $class = null ;
2009-10-17 14:22:40 +02:00
foreach ( $files [ 'files' ] as $file )
{
if ( ! isset ( $collection_props ))
{
2010-01-07 05:24:45 +01:00
$collection_props = $this -> props2array ( $file [ 'props' ]);
2009-10-17 14:22:40 +02:00
echo '<h3>' . lang ( 'Collection listing' ) . ': ' . htmlspecialchars ( $collection_props [ 'DAV:displayname' ]) . " </h3> \n " ;
2023-04-25 15:13:50 +02:00
continue ; // own entry --> displaying properties later
2009-10-17 14:22:40 +02:00
}
if ( ! $n ++ )
{
2012-01-25 04:25:42 +01:00
echo " <table> \n \t <tr class='th'> \n \t \t <th>#</th> \n \t \t <th> " . lang ( 'Name' ) . " </th> " ;
2016-04-02 10:40:34 +02:00
foreach ( $props2show as $label )
{
echo " \t \t <th> " . lang ( $label ) . " </th> \n " ;
}
2012-01-25 04:25:42 +01:00
echo " \t </tr> \n " ;
2009-10-17 14:22:40 +02:00
}
2023-04-25 15:13:50 +02:00
$props = $this -> props2array ( $file [ 'props' ] ? ? []);
2009-10-17 14:22:40 +02:00
//echo $file['path']; _debug_array($props);
2021-03-31 17:49:43 +02:00
$class = $class === 'row_on' ? 'row_off' : 'row_on' ;
2010-03-07 00:06:43 +01:00
2009-10-17 14:22:40 +02:00
if ( substr ( $file [ 'path' ], - 1 ) == '/' )
{
$name = basename ( substr ( $file [ 'path' ], 0 , - 1 )) . '/' ;
}
else
{
$name = basename ( $file [ 'path' ]);
}
2010-03-07 00:06:43 +01:00
2011-10-06 09:51:24 +02:00
echo " \t <tr class=' $class '> \n \t \t <td> $n </td> \n \t \t <td> " .
2016-04-02 12:44:17 +02:00
Html :: a_href ( htmlspecialchars ( $name ), '/groupdav.php' . strtr ( $file [ 'path' ], array (
2011-10-06 09:51:24 +02:00
'%' => '%25' ,
'#' => '%23' ,
'?' => '%3F' ,
))) . " </td> \n " ;
2012-01-25 04:25:42 +01:00
foreach ( $props2show as $prop => $label )
{
echo " \t \t <td> " . ( $prop == 'DAV:getlastmodified' &&! empty ( $props [ $prop ]) ? date ( 'Y-m-d H:i:s' , $props [ $prop ]) : $props [ $prop ]) . " </td> \n " ;
}
echo " \t </tr> \n " ;
2009-10-17 14:22:40 +02:00
}
if ( ! $n )
2009-08-16 17:24:43 +02:00
{
echo '<p>' . lang ( 'Collection empty.' ) . " </p> \n " ;
}
else
{
2022-11-07 20:50:45 +01:00
if ( ! empty ( $files [ 'sync-token-parameters' ][ 2 ]) || ! empty ( $_GET [ 'start' ]))
{
echo " \t <tr class='th'><td colspan=' " . ( 2 + count ( $props2show )) . " '> " .
( empty ( $_GET [ 'start' ]) ? '' :
Html :: a_href ( '<<< ' . lang ( 'Previous %1 accounts' , $chunk_size ), '/groupdav.php' . $options [ 'path' ] . '?start=' . max ( 0 , $_GET [ 'start' ] - $chunk_size ))) .
( ! empty ( $_GET [ 'start' ]) && ! empty ( $files [ 'sync-token-parameters' ][ 2 ]) ? ' | ' : '' ) .
( empty ( $files [ 'sync-token-parameters' ][ 2 ]) ? '' :
Html :: a_href ( lang ( 'Next %1 accounts' , $chunk_size ) . ' >>>' , '/groupdav.php' . $options [ 'path' ] . '?start=' . (( $_GET [ 'start' ] ? ? 0 ) + $chunk_size ))) .
" </td></tr></tr> \n " ;
}
2009-08-16 17:24:43 +02:00
echo " </table> \n " ;
}
echo '<h3>' . lang ( 'Properties' ) . " </h3> \n " ;
echo " <table> \n \t <tr class='th'><th> " . lang ( 'Namespace' ) . " </th><th> " . lang ( 'Name' ) . " </th><th> " . lang ( 'Value' ) . " </th></tr> \n " ;
foreach ( $collection_props as $name => $value )
{
$class = $class == 'row_on' ? 'row_off' : 'row_on' ;
2016-04-02 10:40:34 +02:00
$parts = explode ( ':' , $name );
$name = array_pop ( $parts );
$ns = implode ( ':' , $parts );
2011-09-16 12:21:40 +02:00
echo " \t <tr class=' $class '> \n \t \t <td> " . htmlspecialchars ( $ns ) . " </td><td style='white-space: nowrap'> " . htmlspecialchars ( $name ) . " </td> \n " ;
2011-09-21 22:08:21 +02:00
echo " \t \t <td> " . $value . " </td> \n \t </tr> \n " ;
2009-08-16 17:24:43 +02:00
}
echo " </table> \n " ;
2011-09-25 14:00:20 +02:00
$dav = array ( 1 );
$allow = false ;
$this -> OPTIONS ( $options [ 'path' ], $dav , $allow );
echo " <p>DAV: " . implode ( ', ' , $dav ) . " </p> \n " ;
2009-08-16 17:24:43 +02:00
echo " </body> \n </html> \n " ;
2016-04-02 12:44:17 +02:00
exit ;
2009-08-16 17:24:43 +02:00
}
/**
* Format a property value for output
*
* @ param mixed $value
* @ return string
*/
2010-01-07 05:24:45 +01:00
protected function prop_value ( $value )
2009-08-16 17:24:43 +02:00
{
if ( is_array ( $value ))
{
if ( isset ( $value [ 0 ][ 'ns' ]))
{
2016-08-28 11:12:18 +02:00
$ns_defs = '' ;
$ns_hash = array ();
$value = $this -> _hierarchical_prop_encode ( $value , '' , $ns_defs , $ns_hash );
2009-08-16 17:24:43 +02:00
}
2011-09-21 22:08:21 +02:00
$value = array2string ( $value );
2009-08-16 17:24:43 +02:00
}
2011-09-21 22:08:21 +02:00
if ( $value [ 0 ] == '<' && function_exists ( 'tidy_repair_string' ))
2009-08-16 17:24:43 +02:00
{
2011-09-21 22:08:21 +02:00
$value = tidy_repair_string ( $value , array (
'indent' => true ,
'show-body-only' => true ,
'output-encoding' => 'utf-8' ,
'input-encoding' => 'utf-8' ,
'input-xml' => true ,
'output-xml' => true ,
'wrap' => 0 ,
));
}
2012-10-08 13:20:29 +02:00
if (( $href = preg_match ( '/\<(D:)?href\>[^<]+\<\/(D:)?href\>/i' , $value )))
2011-09-21 22:08:21 +02:00
{
2012-10-08 13:20:29 +02:00
$value = preg_replace ( '/\<(D:)?href\>(' . preg_quote ( $this -> base_uri . '/' , '/' ) . ')?([^<]+)\<\/(D:)?href\>/i' , '<\\1href><a href="\\2\\3">\\3</a></\\4href>' , $value );
2009-08-16 17:24:43 +02:00
}
2016-04-02 10:40:34 +02:00
$ret = $value [ 0 ] == '<' || strpos ( $value , " \n " ) !== false ? '<pre>' . htmlspecialchars ( $value ) . '</pre>' : htmlspecialchars ( $value );
2012-10-08 13:20:29 +02:00
if ( $href )
2009-08-16 17:24:43 +02:00
{
2016-04-02 10:40:34 +02:00
$ret = str_replace ( '</a>' , '</a>' , preg_replace ( '/<a href="(.+)">/' , '<a href="\\1">' , $ret ));
2009-08-16 17:24:43 +02:00
}
2016-04-02 10:40:34 +02:00
return $ret ;
2009-08-16 17:24:43 +02:00
}
/**
* Return numeric indexed array with values for keys 'ns' , 'name' and 'val' as array 'ns:name' => 'val'
*
* @ param array $props
* @ return array
*/
2010-01-07 05:24:45 +01:00
protected function props2array ( array $props )
2009-08-16 17:24:43 +02:00
{
$arr = array ();
foreach ( $props as $prop )
{
2011-09-22 17:22:52 +02:00
$ns_hash = array ( 'DAV:' => 'D' );
2009-08-16 17:24:43 +02:00
switch ( $prop [ 'ns' ])
{
case 'DAV:' ;
$ns = 'DAV' ;
break ;
case self :: CALDAV :
2011-09-22 17:22:52 +02:00
$ns = $ns_hash [ $prop [ 'ns' ]] = 'CalDAV' ;
2009-08-16 17:24:43 +02:00
break ;
case self :: CARDDAV :
2011-09-22 17:22:52 +02:00
$ns = $ns_hash [ $prop [ 'ns' ]] = 'CardDAV' ;
2009-08-16 17:24:43 +02:00
break ;
case self :: GROUPDAV :
2011-09-22 17:22:52 +02:00
$ns = $ns_hash [ $prop [ 'ns' ]] = 'GroupDAV' ;
2009-08-16 17:24:43 +02:00
break ;
default :
$ns = $prop [ 'ns' ];
}
2011-09-16 12:21:40 +02:00
if ( is_array ( $prop [ 'val' ]))
{
2021-03-31 17:49:43 +02:00
$ns_defs = '' ;
$prop [ 'val' ] = $this -> _hierarchical_prop_encode ( $prop [ 'val' ], $prop [ 'ns' ], $ns_defs , $ns_hash );
2011-09-16 12:21:40 +02:00
// hack to show real namespaces instead of not (visibly) defined shortcuts
unset ( $ns_hash [ 'DAV:' ]);
2011-09-22 17:22:52 +02:00
$value = strtr ( $v = $this -> prop_value ( $prop [ 'val' ]), array_flip ( $ns_hash ));
2011-09-21 22:08:21 +02:00
}
else
{
$value = $this -> prop_value ( $prop [ 'val' ]);
2011-09-16 12:21:40 +02:00
}
2011-09-21 22:08:21 +02:00
$arr [ $ns . ':' . $prop [ 'name' ]] = $value ;
2009-08-16 17:24:43 +02:00
}
return $arr ;
}
2010-05-17 16:20:34 +02:00
/**
* POST method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options parameter passing array
2010-05-17 16:20:34 +02:00
* @ return bool true on success
*/
function POST ( & $options )
{
2012-02-04 21:24:01 +01:00
// for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member")
// POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys
2021-09-17 20:15:36 +02:00
if ( isset ( $_GET [ 'add-member' ]) || Handler :: get_agent () == 'cfnetwork' ||
2024-02-06 15:39:12 +01:00
// REST API: all but mail have no POST handler, therefore we have to call the PUT handler
2024-02-08 12:39:56 +01:00
! preg_match ( '#^(/[^/]+)?/mail(/|$)#' , $options [ 'path' ]) && self :: isJSON ())
2012-02-04 21:24:01 +01:00
{
$_GET [ 'add-member' ] = '' ; // otherwise we give no Location header
2021-09-25 12:20:31 +02:00
return $this -> PUT ( $options , 'POST' );
2012-02-04 21:24:01 +01:00
}
2013-09-23 12:21:31 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2013-09-23 12:21:31 +02:00
$this -> _parse_path ( $options [ 'path' ], $id , $app , $user );
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2010-05-17 16:20:34 +02:00
{
2024-02-05 20:06:18 +01:00
// handle links for all apps supporting links
if ( preg_match ( '#/' . $app . '/' . $id . '/links/#' , $options [ 'path' ]))
{
return $handler -> createLink ( $options , $id );
}
2013-09-23 12:21:31 +02:00
// managed attachments
if ( isset ( $_GET [ 'action' ]) && substr ( $_GET [ 'action' ], 0 , 11 ) === 'attachment-' )
2010-05-17 16:20:34 +02:00
{
2013-09-23 12:21:31 +02:00
return $this -> managed_attachements ( $options , $id , $handler , $_GET [ 'action' ]);
}
if ( method_exists ( $handler , 'post' ))
{
// read the content in a string, if a stream is given
2023-06-30 17:13:42 +02:00
if ( isset ( $options [ 'stream' ]) && ! self :: isFileUpload ())
2013-09-23 12:21:31 +02:00
{
$options [ 'content' ] = '' ;
while ( ! feof ( $options [ 'stream' ]))
{
$options [ 'content' ] .= fread ( $options [ 'stream' ], 8192 );
}
2023-06-30 17:13:42 +02:00
fseek ( $options [ 'stream' ], 0 );
2013-09-23 12:21:31 +02:00
}
return $handler -> post ( $options , $id , $user );
2010-05-17 16:20:34 +02:00
}
}
2013-09-23 12:21:31 +02:00
return '501 Not Implemented' ;
}
2010-10-31 08:56:29 +01:00
2013-09-23 12:21:31 +02:00
/**
* HTTP header containing managed id
*/
const MANAGED_ID_HEADER = 'Cal-Managed-ID' ;
2010-10-31 08:56:29 +01:00
2013-09-23 12:21:31 +02:00
/**
* Add , update or remove attachments
*
* @ param array & $options
* @ param string | int $id
2016-04-02 12:44:17 +02:00
* @ param Handler $handler
2013-09-23 12:21:31 +02:00
* @ param string $action 'attachment-add' , 'attachment-update' , 'attachment-remove'
* @ return string http status
*
* @ todo support for rid parameter
* @ todo managed - id does NOT change on update
* @ todo updates of attachments through vfs need to call $handler -> update_tags ( $id ) too
*/
2016-04-02 12:44:17 +02:00
protected function managed_attachements ( & $options , $id , Handler $handler , $action )
2013-09-23 12:21:31 +02:00
{
error_log ( __METHOD__ . " (path= $options[path] , id= $id , ..., action= $action ) _GET= " . array2string ( $_GET ));
$entry = $handler -> _common_get_put_delete ( 'GET' , $options , $id );
if ( ! is_array ( $entry ))
2010-05-17 16:20:34 +02:00
{
2013-09-23 12:21:31 +02:00
return $entry ? $entry : " 404 Not found " ;
2010-05-17 16:20:34 +02:00
}
2013-09-23 12:21:31 +02:00
2016-05-11 21:23:14 +02:00
if ( ! Link :: file_access ( $handler -> app , $entry [ 'id' ], Acl :: EDIT ))
2013-09-23 12:21:31 +02:00
{
return '403 Forbidden' ;
}
switch ( $action )
{
case 'attachment-add' :
2016-04-02 10:40:34 +02:00
$matches = null ;
2013-09-23 12:21:31 +02:00
if ( isset ( $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ]) &&
substr ( $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ], 0 , 10 ) === 'attachment' &&
preg_match ( '/filename="?([^";]+)/' , $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ], $matches ))
{
2016-04-02 12:44:17 +02:00
$filename = Vfs :: basename ( $matches [ 1 ]);
2013-09-23 12:21:31 +02:00
}
2016-04-02 10:40:34 +02:00
$path = null ;
2013-09-23 15:39:28 +02:00
if ( ! ( $to = self :: fopen_attachment ( $handler -> app , $handler -> get_id ( $entry ), $filename , $this -> _SERVER [ 'CONTENT_TYPE' ], $path )) ||
2013-09-23 12:21:31 +02:00
isset ( $options [ 'stream' ]) && ( $copied = stream_copy_to_stream ( $options [ 'stream' ], $to )) === false ||
isset ( $options [ 'content' ]) && ( $copied = fwrite ( $to , $options [ 'content' ])) === false )
{
return '403 Forbidden' ;
}
fclose ( $to );
2013-09-23 15:39:28 +02:00
error_log ( __METHOD__ . " () content-type= $options[content_type] , filename= $filename : $path created $copied bytes copied " );
2013-09-23 12:21:31 +02:00
$ret = '201 Created' ;
header ( self :: MANAGED_ID_HEADER . ': ' . self :: path2managed_id ( $path ));
2013-09-24 14:29:17 +02:00
header ( 'Location: ' . self :: path2location ( $path ));
2013-09-23 12:21:31 +02:00
break ;
case 'attachment-remove' :
case 'attachment-update' :
2016-09-13 15:55:55 +02:00
if ( empty ( $_GET [ 'managed-id' ]) || ! ( $path = self :: managed_id2path ( $_GET [ 'managed-id' ], $handler -> app , $entry [ 'id' ])))
2013-09-23 12:21:31 +02:00
{
2013-09-25 09:11:27 +02:00
self :: xml_error ( self :: mkprop ( self :: CALDAV , 'valid-managed-id-parameter' , '' ));
2013-09-25 14:37:42 +02:00
return '403 Forbidden' ;
2013-09-23 12:21:31 +02:00
}
if ( $action == 'attachment-remove' )
{
2016-04-02 12:44:17 +02:00
if ( ! Vfs :: unlink ( $path ))
2013-09-23 12:21:31 +02:00
{
2013-09-25 09:11:27 +02:00
self :: xml_error ( self :: mkprop ( self :: CALDAV , 'valid-managed-id-parameter' , '' ));
2013-09-23 12:21:31 +02:00
return '403 Forbidden' ;
}
$ret = '204 No content' ;
}
else
{
2015-09-29 14:27:49 +02:00
// check for rename of attachment via Content-Disposition:filename=
if ( isset ( $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ]) &&
substr ( $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ], 0 , 10 ) === 'attachment' &&
preg_match ( '/filename="?([^";]+)/' , $this -> _SERVER [ 'HTTP_CONTENT_DISPOSITION' ], $matches ) &&
2016-04-02 12:44:17 +02:00
( $filename = Vfs :: basename ( $matches [ 1 ])) != Vfs :: basename ( $path ))
2015-09-29 14:27:49 +02:00
{
$old_path = $path ;
2016-06-26 19:06:54 +02:00
if ( ! ( $dir = Vfs :: dirname ( $path )) || ! Vfs :: rename ( $old_path , $path = Vfs :: concat ( $dir , $filename )))
2015-09-29 14:27:49 +02:00
{
self :: xml_error ( self :: mkprop ( self :: CALDAV , 'valid-managed-id-parameter' , '' ));
return '403 Forbidden' ;
}
}
2016-04-02 12:44:17 +02:00
if ( ! ( $to = Vfs :: fopen ( $path , 'w' )) ||
2013-09-23 12:21:31 +02:00
isset ( $options [ 'stream' ]) && ( $copied = stream_copy_to_stream ( $options [ 'stream' ], $to )) === false ||
isset ( $options [ 'content' ]) && ( $copied = fwrite ( $to , $options [ 'content' ])) === false )
{
2013-09-25 09:11:27 +02:00
self :: xml_error ( self :: mkprop ( self :: CALDAV , 'valid-managed-id-parameter' , '' ));
2013-09-23 12:21:31 +02:00
return '403 Forbidden' ;
}
fclose ( $to );
error_log ( __METHOD__ . " () content-type= $options[content_type] , filename= $filename : $path updated $copied bytes copied " );
$ret = '200 Ok' ;
header ( self :: MANAGED_ID_HEADER . ': ' . self :: path2managed_id ( $path ));
2013-09-25 14:37:42 +02:00
header ( 'Location: ' . self :: path2location ( $path ));
2013-09-23 12:21:31 +02:00
}
break ;
default :
return '501 Unknown action parameter ' . $action ;
}
// update etag/ctag/sync-token by updating modification time
$handler -> update_tags ( $entry );
// check/handle Prefer: return-representation
2013-09-25 14:37:42 +02:00
// we can NOT use 204 No content (forbidds a body) with return=representation, therefore we need to use 200 Ok instead!
2016-04-02 10:40:34 +02:00
if ( $handler -> check_return_representation ( $options , $id ) && ( int ) $ret == 204 )
2013-09-25 14:37:42 +02:00
{
$ret = '200 Ok' ;
}
2013-09-23 12:21:31 +02:00
return $ret ;
}
2013-09-23 15:39:28 +02:00
/**
* Handle ATTACH attribute on importing iCals
*
* - turn inline attachments into managed attachments
* - delete NOT included attachments , $delete_via_put is true
* @ todo : store URLs not from our managed attachments
*
2023-07-21 08:54:06 +02:00
* @ param string $app e . g . 'calendar'
2013-09-23 15:39:28 +02:00
* @ param int | string $id
* @ param array $attach array of array with values for keys 'name' , 'params' , 'value'
* @ param boolean $delete_via_put
2023-07-21 08:54:06 +02:00
* @ return boolean false on error , e . g . invalid managed id , for false an xml - error body has been send
2013-09-23 15:39:28 +02:00
*/
public static function handle_attach ( $app , $id , $attach , $delete_via_put = false )
{
2016-06-20 18:34:35 +02:00
//error_log(__METHOD__."('$app', $id, attach=".array2string($attach).", delete_via_put=".array2string($delete_via_put).')');
2013-09-23 15:39:28 +02:00
2016-05-11 21:23:14 +02:00
if ( ! Link :: file_access ( $app , $id , Acl :: EDIT ))
2013-09-23 15:39:28 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) no rights to update attachments " );
return ; // no rights --> nothing to do
}
if ( ! is_array ( $attach )) $attach = array (); // could be PEAR_Error if not set
if ( $delete_via_put )
{
2016-04-02 12:44:17 +02:00
foreach ( Vfs :: find ( Link :: vfs_path ( $app , $id , '' , true ), array ( 'type' => 'F' )) as $path )
2013-09-23 15:39:28 +02:00
{
$found = false ;
foreach ( $attach as $key => $attr )
{
if ( $attr [ 'params' ][ 'MANAGED-ID' ] === self :: path2managed_id ( $path ))
{
$found = true ;
unset ( $attach [ $key ]);
break ;
}
}
if ( ! $found )
{
2016-04-02 12:44:17 +02:00
$ok = Vfs :: unlink ( $path );
error_log ( __METHOD__ . " (' $app ', $id , ...) Vfs::unlink(' $path ') returned " . array2string ( $ok ));
2013-09-23 15:39:28 +02:00
}
}
}
// turn inline attachments into managed ones
foreach ( $attach as $key => $attr )
{
2015-09-29 09:37:15 +02:00
if ( ! empty ( $attr [ 'params' ][ 'FMTTYPE' ]))
2013-09-23 15:39:28 +02:00
{
2015-09-30 05:27:29 +02:00
if ( isset ( $attr [ 'params' ][ 'MANAGED-ID' ]))
{
// invalid managed-id
2016-04-02 12:44:17 +02:00
if ( ! ( $path = self :: managed_id2path ( $attr [ 'params' ][ 'MANAGED-ID' ])) || ! Vfs :: is_readable ( $path ))
2015-09-30 05:27:29 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) invalid MANAGED-ID " . array2string ( $attr ));
self :: xml_error ( self :: mkprop ( self :: CALDAV , 'valid-managed-id' , '' ));
return false ;
}
2016-04-02 12:44:17 +02:00
if ( $path == ( $link = Link :: vfs_path ( $app , $id , Vfs :: basename ( $path ))))
2015-09-30 05:27:29 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) trying to modify existing MANAGED-ID --> ignored! " . array2string ( $attr ));
continue ;
}
// reuse valid managed-id --> symlink attachment
2016-04-02 12:44:17 +02:00
if ( Vfs :: file_exists ( $link ))
2015-09-30 05:27:29 +02:00
{
2016-04-02 12:44:17 +02:00
if ( Vfs :: readlink ( $link ) === $path ) continue ; // no need to recreate identical link
Vfs :: unlink ( $link ); // symlink will fail, if $link exists
2015-09-30 05:27:29 +02:00
}
2016-04-02 12:44:17 +02:00
if ( ! Vfs :: symlink ( $path , $link ))
2015-09-30 05:27:29 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) failed to symlink( $path , $link ) --> ignored! " );
}
continue ;
}
2013-09-23 15:39:28 +02:00
if ( ! ( $to = self :: fopen_attachment ( $app , $id , $filename = $attr [ 'params' ][ 'FILENAME' ], $attr [ 'params' ][ 'FMTTYPE' ], $path )) ||
2015-09-29 09:37:15 +02:00
// Horde Icalendar does NOT decode automatic
2016-06-20 18:34:35 +02:00
( /*$copied=*/ fwrite ( $to , $attr [ 'params' ][ 'ENCODING' ] == 'BASE64' ? base64_decode ( $attr [ 'value' ]) : $attr [ 'value' ])) === false )
2013-09-23 15:39:28 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) failed to add attachment " . array2string ( $attr ) . " ) " );
continue ;
}
fclose ( $to );
2016-06-20 18:34:35 +02:00
//error_log(__METHOD__."('$app', $id, ...)) content-type={$attr['params']['FMTTYPE']}, filename=$filename: $path created $copied bytes copied");
2013-09-23 15:39:28 +02:00
}
else
{
2016-06-20 18:34:35 +02:00
//error_log(__METHOD__."('$app', $id, ...) unsupported URI attachment ".array2string($attr));
2013-09-23 15:39:28 +02:00
}
}
}
/**
* Open attachment for writing
*
* @ param string $app
* @ param int | string $id
2016-04-02 10:40:34 +02:00
* @ param string $_filename defaults to 'attachment'
* @ param string $mime = null mime - type to generate extension
* @ param string & $path = null on return path opened
2013-09-23 15:39:28 +02:00
* @ return resource
*/
2016-04-02 10:40:34 +02:00
protected static function fopen_attachment ( $app , $id , $_filename , $mime = null , & $path = null )
2013-09-23 15:39:28 +02:00
{
2016-04-02 12:44:17 +02:00
$filename = empty ( $_filename ) ? 'attachment' : Vfs :: basename ( $_filename );
2013-09-23 15:39:28 +02:00
2023-07-21 08:54:06 +02:00
if ( strpos ( $mime , ';' )) list ( $mime ) = explode ( ';' , $mime ); // in case it contains e.g. charset info
2013-09-23 15:39:28 +02:00
2016-04-02 12:44:17 +02:00
$ext = ! empty ( $mime ) ? MimeMagic :: mime2ext ( $mime ) : '' ;
2013-09-23 15:39:28 +02:00
2016-04-02 10:40:34 +02:00
$matches = null ;
2013-09-23 15:39:28 +02:00
if ( ! $ext || substr ( $filename , - strlen ( $ext ) - 1 ) == '.' . $ext ||
2016-04-02 12:44:17 +02:00
preg_match ( '/\.([^.]+)$/' , $filename , $matches ) && MimeMagic :: ext2mime ( $matches [ 1 ]) == $mime )
2013-09-23 15:39:28 +02:00
{
$parts = explode ( '.' , $filename );
$ext = '.' . array_pop ( $parts );
$filename = implode ( '.' , $parts );
}
else
{
$ext = '.' . $ext ;
}
for ( $i = 1 ; $i < 100 ; ++ $i )
{
2016-04-02 12:44:17 +02:00
$path = Link :: vfs_path ( $app , $id , $filename . ( $i > 1 ? '-' . $i : '' ) . $ext , true );
if ( ! Vfs :: stat ( $path )) break ;
2013-09-23 15:39:28 +02:00
}
if ( $i >= 100 ) return null ;
2016-06-26 19:06:54 +02:00
if ( ! ( $dir = Vfs :: dirname ( $path )) || ! Vfs :: file_exists ( $dir ) && ! Vfs :: mkdir ( $dir , 0777 , STREAM_MKDIR_RECURSIVE ))
2013-09-23 15:39:28 +02:00
{
error_log ( __METHOD__ . " (' $app ', $id , ...) failed to create entry dir $dir ! " );
return false ;
}
2016-04-02 12:44:17 +02:00
return Vfs :: fopen ( $path , 'w' );
2013-09-23 15:39:28 +02:00
}
/**
2013-09-24 14:29:17 +02:00
* Get attachment location from path
2013-09-23 15:39:28 +02:00
*
2013-09-24 14:29:17 +02:00
* @ param string $path
* @ return string
2013-09-23 15:39:28 +02:00
*/
2013-09-24 14:29:17 +02:00
protected static function path2location ( $path )
2013-09-23 15:39:28 +02:00
{
2019-11-15 13:54:34 +01:00
return Framework :: getUrl ( Framework :: link ( Vfs :: download_url ( $path )));
2013-09-24 14:29:17 +02:00
}
/**
* Add ATTACH attribute ( s ) for iCal
*
2023-07-21 08:54:06 +02:00
* @ param string $app e . g . 'calendar'
2013-09-24 14:29:17 +02:00
* @ param int | string $id
* @ param array & $attributes
* @ param array & $parameters
*/
public static function add_attach ( $app , $id , array & $attributes , array & $parameters )
{
2016-04-02 12:44:17 +02:00
foreach ( Vfs :: find ( Link :: vfs_path ( $app , $id , '' , true ), array (
2013-09-23 15:39:28 +02:00
'type' => 'F' ,
'need_mime' => true ,
2018-04-11 11:25:53 +02:00
'maxdepth' => 10 , // set a limit to not run into an infinit recursion
2013-09-23 15:39:28 +02:00
), true ) as $path => $stat )
{
2015-09-30 05:27:29 +02:00
// handle symlinks --> return target size and mime-type
2016-04-02 12:44:17 +02:00
if (( $target = Vfs :: readlink ( $path )))
2015-09-30 05:27:29 +02:00
{
2016-04-02 12:44:17 +02:00
if ( ! ( $stat = Vfs :: stat ( $target ))) continue ; // broken or inaccessible symlink
2015-09-30 05:27:29 +02:00
// check if target is in /apps, probably reused MANAGED-ID --> return it
if ( substr ( $target , 0 , 6 ) == '/apps/' )
{
$path = $target ;
}
}
2013-09-24 14:29:17 +02:00
$attributes [ 'ATTACH' ][] = self :: path2location ( $path );
2013-09-23 15:39:28 +02:00
$parameters [ 'ATTACH' ][] = array (
2016-04-02 12:44:17 +02:00
'MANAGED-ID' => self :: path2managed_id ( $path ),
2013-09-25 09:11:27 +02:00
'FMTTYPE' => $stat [ 'mime' ],
2015-09-29 09:37:15 +02:00
'SIZE' => ( string ) $stat [ 'size' ], // Horde_Icalendar renders int as empty string
2016-04-02 12:44:17 +02:00
'FILENAME' => Vfs :: basename ( $path ),
2013-09-23 15:39:28 +02:00
);
2015-09-29 09:37:15 +02:00
// if we have attachments, set X-attribute to enable deleting them by put
// (works around events synced before without ATTACH attributes)
$attributes [ 'X-EGROUPWARE-ATTACH-INCLUDED' ] = 'TRUE' ;
2013-09-23 15:39:28 +02:00
}
}
2013-09-23 12:21:31 +02:00
/**
* Return managed - id of a vfs - path
*
* @ param string $path " /apps/ $app / $id /something "
* @ return string
*/
static public function path2managed_id ( $path )
{
return base64_encode ( $path );
}
/**
* Return vfs - path of a managed - id
*
* @ param string $managed_id
2016-04-02 10:40:34 +02:00
* @ param string $app = null app - name to check against path
2023-07-21 08:54:06 +02:00
* @ param string | int $id = null id to check against path
2013-09-23 12:21:31 +02:00
* @ return string | boolean " /apps/ $app / $id /something " or false if not found or not belonging to given $app / $id
*/
2023-07-21 08:54:06 +02:00
public static function managed_id2path ( $managed_id , $app = null , $id = null )
2013-09-23 12:21:31 +02:00
{
$path = base64_decode ( $managed_id );
2016-04-02 12:44:17 +02:00
if ( ! $path || substr ( $path , 0 , 6 ) != '/apps/' || ! Vfs :: stat ( $path ))
2013-09-23 12:21:31 +02:00
{
$path = false ;
}
elseif ( ! empty ( $app ) && ! empty ( $id ))
{
list (,, $a , $i ) = explode ( '/' , $path );
if ( $a !== $app || $i !== ( string ) $id )
{
$path = false ;
}
}
error_log ( __METHOD__ . " (' $managed_id ', $app , $id ) base64_decode(' $managed_id ')= " . array2string ( base64_decode ( $managed_id )) . ' returning ' . array2string ( $path ));
return $path ;
2010-05-17 16:20:34 +02:00
}
2010-10-31 08:56:29 +01:00
2011-11-09 14:23:53 +01:00
/**
2023-07-21 08:54:06 +02:00
* Namespaces which need to be explicitly named in self :: $proppatch_props ,
2012-01-30 06:11:05 +01:00
* because we consider them protected , if not explicitly named
*
* @ var array
*/
static $ns_needs_explicit_named_props = array ( self :: DAV , self :: CALDAV , self :: CARDDAV , self :: CALENDARSERVER );
/**
2023-07-21 08:54:06 +02:00
* props modifiable via proppatch from client for name - spaces mentioned in self :: $ns_needs_explicit_named_props
2012-01-30 06:11:05 +01:00
*
2023-07-21 08:54:06 +02:00
* Props named here are stored in preferences without namespace !
2011-11-09 14:23:53 +01:00
*
* @ var array name => namespace pairs
*/
static $proppatch_props = array (
'displayname' => self :: DAV ,
'calendar-description' => self :: CALDAV ,
2012-01-30 06:11:05 +01:00
'addressbook-description' => self :: CARDDAV ,
'calendar-color' => self :: ICAL , // only mentioned that old prefs still work
2011-11-09 14:23:53 +01:00
'calendar-order' => self :: ICAL ,
2012-09-26 12:01:02 +02:00
'default-alarm-vevent-date' => self :: CALDAV ,
'default-alarm-vevent-datetime' => self :: CALDAV ,
2011-11-09 14:23:53 +01:00
);
/**
* PROPPATCH method handler
*
2012-01-30 06:11:05 +01:00
* @ param array & $options general parameter passing array
2023-07-21 08:54:06 +02:00
* @ return string with response - description or null , individual status in $options [ 'props' ][][ 'status' ]
2011-11-09 14:23:53 +01:00
*/
function PROPPATCH ( & $options )
{
2014-05-28 12:57:02 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( " . array2string ( $options ) . ')' );
2011-11-09 14:23:53 +01:00
// parse path in form [/account_lid]/app[/more]
2016-04-02 10:40:34 +02:00
$id = $app = $user = $user_prefix = null ;
2023-07-21 08:54:06 +02:00
$this -> _parse_path ( $options [ 'path' ], $id , $app , $user , $user_prefix ); // always returns false if e.g. !$id
2012-02-02 00:26:16 +01:00
if ( $app == 'principals' || $id || $options [ 'path' ] == '/' )
2011-11-09 14:23:53 +01:00
{
2014-05-28 12:57:02 +02:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " : user=' $user ', app=' $app ', id=' $id ': 404 not found! " );
2016-04-02 10:40:34 +02:00
foreach ( $options [ 'props' ] as & $prop )
{
$prop [ 'status' ] = '403 Forbidden' ;
}
2012-01-30 06:11:05 +01:00
return 'NOT allowed to PROPPATCH that resource!' ;
2011-11-09 14:23:53 +01:00
}
2023-07-21 08:54:06 +02:00
// store selected props in preferences, e.g. calendar-color, see self::$proppatch_props
2014-05-28 12:57:02 +02:00
$need_save = array ();
2012-01-30 06:11:05 +01:00
foreach ( $options [ 'props' ] as & $prop )
2011-11-09 14:23:53 +01:00
{
2012-01-30 06:11:05 +01:00
if (( isset ( self :: $proppatch_props [ $prop [ 'name' ]]) && self :: $proppatch_props [ $prop [ 'name' ]] === $prop [ 'xmlns' ] ||
! in_array ( $prop [ 'xmlns' ], self :: $ns_needs_explicit_named_props )))
2011-11-09 14:23:53 +01:00
{
2012-02-02 00:26:16 +01:00
if ( ! $app )
{
$app = 'groupdav' ;
$name = $prop [ 'name' ] . ':' . $options [ 'path' ] . ':' . $prop [ 'ns' ];
}
else
{
$name = $prop [ 'name' ] . ':' . $user . ( isset ( self :: $proppatch_props [ $prop [ 'name' ]]) &&
self :: $proppatch_props [ $prop [ 'name' ]] == $prop [ 'ns' ] ? '' : ':' . $prop [ 'ns' ]);
}
2012-01-30 06:11:05 +01:00
//error_log("preferences['user']['$app']['$name']=".array2string($GLOBALS['egw_info']['user']['preferences'][$app][$name]).($GLOBALS['egw_info']['user']['preferences'][$app][$name] !== $prop['val'] ? ' !== ':' === ')."prop['val']=".array2string($prop['val']));
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ $app ][ $name ] !== $prop [ 'val' ]) // nothing to change otherwise
{
if ( isset ( $prop [ 'val' ]))
{
$GLOBALS [ 'egw' ] -> preferences -> add ( $app , $name , $prop [ 'val' ]);
}
else
{
$GLOBALS [ 'egw' ] -> preferences -> delete ( $app , $name );
}
2014-05-28 12:57:02 +02:00
$need_save [] = $name ;
2012-01-30 06:11:05 +01:00
}
$prop [ 'status' ] = '200 OK' ;
}
else
{
$prop [ 'status' ] = '409 Conflict' ; // could also be "403 Forbidden"
2011-11-09 14:23:53 +01:00
}
}
2014-05-28 12:57:02 +02:00
if ( $need_save )
{
$GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ] = $GLOBALS [ 'egw' ] -> preferences -> save_repository ();
// call calendar-hook, if default-alarms are changed, to sync them to calendar prefs
2016-04-02 12:44:17 +02:00
if ( class_exists ( 'calendar_hooks' ))
2014-05-28 12:57:02 +02:00
{
2016-04-02 12:44:17 +02:00
foreach ( $need_save as $name )
2014-05-28 12:57:02 +02:00
{
2016-04-02 12:44:17 +02:00
list ( $name ) = explode ( ':' , $name );
if ( in_array ( $name , array ( 'default-alarm-vevent-date' , 'default-alarm-vevent-datetime' )))
{
calendar_hooks :: sync_default_alarms ();
break ;
}
2014-05-28 12:57:02 +02:00
}
}
}
2011-11-09 14:23:53 +01:00
}
2008-05-08 22:31:32 +02:00
/**
* PUT method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options parameter passing array
* @ param string $method " PUT " ( default ) or " PATCH "
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
2021-09-25 12:20:31 +02:00
function PUT ( & $options , $method = 'PUT' )
2008-05-08 22:31:32 +02:00
{
// read the content in a string, if a stream is given
if ( isset ( $options [ 'stream' ]))
{
$options [ 'content' ] = '' ;
while ( ! feof ( $options [ 'stream' ]))
{
$options [ 'content' ] .= fread ( $options [ 'stream' ], 8192 );
}
}
2010-10-31 08:56:29 +01:00
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
2016-04-02 10:40:34 +02:00
$id = $app = $user = $prefix = null ;
2010-10-20 17:47:30 +02:00
if ( ! $this -> _parse_path ( $options [ 'path' ], $id , $app , $user , $prefix ))
2008-05-08 22:31:32 +02:00
{
return '404 Not Found' ;
}
2023-07-21 17:41:37 +02:00
// REST API & PATCH only implemented for addressbook and calendar currently
2024-02-06 15:39:12 +01:00
if ( ! self :: isJSON () && $method === 'PATCH' )
2021-09-25 12:20:31 +02:00
{
return '501 Not implemented' ;
}
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2008-05-08 22:31:32 +02:00
{
2024-02-06 20:23:59 +01:00
// handle links for all apps supporting links
if ( $method === 'POST' && preg_match ( '#/' . $app . '/' . $id . '/links/#' , $options [ 'path' ]))
{
return $handler -> createLink ( $options , $id );
}
2021-09-25 12:20:31 +02:00
$status = $handler -> put ( $options , $id , $user , $prefix , $method , $_SERVER [ 'HTTP_CONTENT_TYPE' ]);
2013-09-23 12:21:31 +02:00
2008-05-08 22:31:32 +02:00
// set default stati: true --> 204 No Content, false --> should be already handled
if ( is_bool ( $status )) $status = $status ? '204 No Content' : '400 Something went wrong' ;
2013-09-23 12:21:31 +02:00
// check/handle Prefer: return-representation
2024-05-10 16:28:37 +02:00
if ((( string ) $status )[ 0 ] === '2' || $status === true )
2013-09-23 12:21:31 +02:00
{
2023-07-07 10:49:58 +02:00
// we can NOT use 204 No content (forbids a body) with return=representation, therefore we need to use 200 Ok instead!
2015-09-28 15:15:36 +02:00
if ( $handler -> check_return_representation ( $options , $id , $user ) && ( int ) $status == 204 )
{
$status = '200 Ok' ;
}
2013-09-23 12:21:31 +02:00
}
2008-05-08 22:31:32 +02:00
return $status ;
}
return '501 Not Implemented' ;
}
/**
* DELETE method handler
*
2023-07-21 08:54:06 +02:00
* @ param array $options general parameter passing array
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function DELETE ( $options )
{
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2008-05-08 22:31:32 +02:00
if ( ! $this -> _parse_path ( $options [ 'path' ], $id , $app , $user ))
{
return '404 Not Found' ;
}
2023-07-21 08:54:06 +02:00
if (( $handler = $this -> app_handler ( $app )))
2008-05-08 22:31:32 +02:00
{
2024-02-05 20:06:18 +01:00
// handle links for all apps supporting links
if ( preg_match ( '#/' . $app . '/' . $id . '/links/(-?\d+)$#' , $options [ 'path' ], $matches ))
{
return $handler -> deleteLink ( $options , $id , $matches [ 1 ]);
}
2018-10-09 13:14:36 +02:00
$status = $handler -> delete ( $options , $id , $user );
2008-05-08 22:31:32 +02:00
// set default stati: true --> 204 No Content, false --> should be already handled
if ( is_bool ( $status )) $status = $status ? '204 No Content' : '400 Something went wrong' ;
return $status ;
}
return '501 Not Implemented' ;
}
/**
* MKCOL method handler
*
2023-07-21 08:54:06 +02:00
* @ param array $options general parameter passing array
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function MKCOL ( $options )
{
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
return '501 Not Implemented' ;
}
/**
* MOVE method handler
*
2023-07-21 08:54:06 +02:00
* @ param array $options general parameter passing array
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function MOVE ( $options )
{
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
return '501 Not Implemented' ;
}
/**
* COPY method handler
*
2023-07-21 08:54:06 +02:00
* @ param array $options general parameter passing array
* @ param bool $del false : default copy , true : move
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function COPY ( $options , $del = false )
{
2016-04-02 12:44:17 +02:00
if ( $this -> debug ) error_log ( 'self::' . ( $del ? 'MOVE' : 'COPY' ) . '(' . array2string ( $options ) . ')' );
2008-05-08 22:31:32 +02:00
return '501 Not Implemented' ;
}
/**
* LOCK method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options general parameter passing array
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function LOCK ( & $options )
{
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2023-07-21 08:54:06 +02:00
$this -> _parse_path ( $options [ 'path' ], $id , $app , $user );
2016-04-02 12:44:17 +02:00
$path = Vfs :: app_entry_lock_path ( $app , $id );
2008-05-08 22:31:32 +02:00
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . " ) path= $path " );
2008-05-08 22:31:32 +02:00
// get the app handler, to check if the user has edit access to the entry (required to make locks)
2023-07-21 08:54:06 +02:00
$handler = $this -> app_handler ( $app );
2008-05-08 22:31:32 +02:00
// TODO recursive locks on directories not supported yet
2016-05-11 21:23:14 +02:00
if ( ! $id || ! empty ( $options [ 'depth' ]) || ! $handler -> check_access ( Acl :: EDIT , $id ))
2008-05-08 22:31:32 +02:00
{
return '409 Conflict' ;
}
$options [ 'timeout' ] = time () + 300 ; // 5min. hardcoded
2023-07-21 08:54:06 +02:00
// don't know why, but HTTP_WebDAV_Server passes the owner in D:href tags, which gets passed unchanged to checkLock/PROPFIND
2008-05-08 22:31:32 +02:00
// that's wrong according to the standard and cadaver does not show it on discover --> strip_tags removes eventual tags
2021-03-31 17:49:43 +02:00
$owner = strip_tags ( $options [ 'owner' ]);
if (( $ret = Vfs :: lock ( $path , $options [ 'locktoken' ], $options [ 'timeout' ], $owner ,
2008-05-08 22:31:32 +02:00
$options [ 'scope' ], $options [ 'type' ], isset ( $options [ 'update' ]), false )) && ! isset ( $options [ 'update' ])) // false = no ACL check
{
return $ret ? '200 OK' : '409 Conflict' ;
}
return $ret ;
}
/**
* UNLOCK method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options general parameter passing array
* @ return string string with HTTP status
2008-05-08 22:31:32 +02:00
*/
function UNLOCK ( & $options )
{
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2023-07-21 08:54:06 +02:00
$this -> _parse_path ( $options [ 'path' ], $id , $app , $user );
2016-04-02 12:44:17 +02:00
$path = Vfs :: app_entry_lock_path ( $app , $id );
2008-05-08 22:31:32 +02:00
2008-05-17 14:54:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . " ) path= $path " );
2016-04-02 12:44:17 +02:00
return Vfs :: unlock ( $path , $options [ 'token' ]) ? '204 No Content' : '409 Conflict' ;
2008-05-08 22:31:32 +02:00
}
/**
* checkLock () helper
*
2023-07-21 08:54:06 +02:00
* @ param string $path resource path to check for locks
2008-05-08 22:31:32 +02:00
* @ return bool true on success
*/
function checkLock ( $path )
{
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2023-07-21 08:54:06 +02:00
$this -> _parse_path ( $path , $id , $app , $user );
2008-05-08 22:31:32 +02:00
2016-04-02 12:44:17 +02:00
return Vfs :: checkLock ( Vfs :: app_entry_lock_path ( $app , $id ));
2008-05-08 22:31:32 +02:00
}
2010-03-07 00:06:43 +01:00
/**
* ACL method handler
*
2023-07-21 08:54:06 +02:00
* @ param array & $options general parameter passing array
2010-03-07 00:06:43 +01:00
* @ return string HTTP status
*/
function ACL ( & $options )
{
2016-04-02 10:40:34 +02:00
$id = $app = $user = null ;
2023-07-21 08:54:06 +02:00
$this -> _parse_path ( $options [ 'path' ], $id , $app , $user );
2010-03-07 00:06:43 +01:00
2016-04-02 10:40:34 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . " ) path= $options[path] " );
2010-03-07 00:06:43 +01:00
$options [ 'errors' ] = array ();
switch ( $app )
{
case 'calendar' :
case 'addressbook' :
case 'infolog' :
$status = '200 OK' ; // grant all
break ;
default :
$options [ 'errors' ][] = 'no-inherited-ace-conflict' ;
$status = '403 Forbidden' ;
}
return $status ;
}
2008-05-08 22:31:32 +02:00
/**
* Parse a path into it ' s id , app and user parts
*
* @ param string $path
* @ param int & $id
* @ param string & $app addressbook , calendar , infolog ( = infolog )
* @ param int & $user
2016-04-02 10:40:34 +02:00
* @ param string & $user_prefix = null
2008-05-08 22:31:32 +02:00
* @ return boolean true on success , false on error
*/
2009-10-03 12:22:14 +02:00
function _parse_path ( $path , & $id , & $app , & $user , & $user_prefix = null )
2008-05-08 22:31:32 +02:00
{
2010-02-26 12:04:01 +01:00
if ( $this -> debug )
{
error_log ( __METHOD__ . " called with (' $path ') id= $id , app=' $app ', user= $user " );
}
if ( $path [ 0 ] == '/' )
{
2011-11-23 17:34:39 +01:00
$path = substr ( $path , 1 );
2010-02-26 12:04:01 +01:00
}
2023-07-21 08:54:06 +02:00
$parts = explode ( '/' , self :: _unslashify ( $path ));
2008-05-08 22:31:32 +02:00
2012-09-27 17:46:08 +02:00
// /(resources|locations)/<resource-id>-<resource-name>/calendar
if ( $parts [ 0 ] == 'resources' || $parts [ 0 ] == 'locations' )
{
if ( ! empty ( $parts [ 1 ]))
{
$user = $parts [ 0 ] . '/' . $parts [ 1 ];
array_shift ( $parts );
$res_id = ( int ) array_shift ( $parts );
2016-04-02 12:44:17 +02:00
if ( ! Principals :: read_resource ( $res_id ))
2012-09-27 17:46:08 +02:00
{
return false ;
}
$account_id = 'r' . $res_id ;
$app = 'calendar' ;
}
}
elseif (( $account_id = $this -> accounts -> name2id ( $parts [ 0 ], 'account_lid' )) ||
2010-10-20 16:37:48 +02:00
( $account_id = $this -> accounts -> name2id ( $parts [ 0 ] = urldecode ( $parts [ 0 ]))))
2009-10-03 12:22:14 +02:00
{
2010-02-26 12:04:01 +01:00
// /$user/$app/...
$user = array_shift ( $parts );
2009-10-03 12:22:14 +02:00
}
2008-05-08 22:31:32 +02:00
2012-09-27 17:46:08 +02:00
if ( ! isset ( $app )) $app = array_shift ( $parts );
2010-02-26 12:04:01 +01:00
2012-01-30 06:11:05 +01:00
// /addressbook-accounts/
if ( ! $account_id && $app == 'addressbook-accounts' )
{
$app = 'addressbook' ;
$user = 0 ;
$user_prefix = '/' ;
}
2012-09-27 17:46:08 +02:00
// shared calendars/addressbooks at /<currentuser>/(calendar|addressbook|infolog|resource|location)-<username>
2012-01-30 06:11:05 +01:00
elseif ( $account_id == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && strpos ( $app , '-' ) !== false )
2012-01-25 04:25:42 +01:00
{
2012-09-27 17:46:08 +02:00
$user_prefix = '/' . $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ] . '/' . $app ;
2012-01-25 04:25:42 +01:00
list ( $app , $username ) = explode ( '-' , $app , 2 );
2017-12-01 14:58:44 +01:00
if ( $username == 'accounts' && $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] !== '1' )
2012-01-25 04:25:42 +01:00
{
$account_id = 0 ;
}
2012-09-27 17:46:08 +02:00
elseif ( $app == 'resource' || $app == 'location' )
{
2016-04-02 12:44:17 +02:00
if ( ! Principals :: read_resource ( $res_id = ( int ) $username ))
2012-09-27 17:46:08 +02:00
{
return false ;
}
$account_id = 'r' . $res_id ;
$app = 'calendar' ;
}
2012-01-25 04:25:42 +01:00
elseif ( ! ( $account_id = $this -> accounts -> name2id ( $username , 'account_lid' )) &&
! ( $account_id = $this -> accounts -> name2id ( $username = urldecode ( $username ))))
{
return false ;
}
$user = $account_id ;
}
elseif ( $user )
2008-05-08 22:31:32 +02:00
{
2009-10-03 12:22:14 +02:00
$user_prefix = '/' . $user ;
2010-03-22 16:04:21 +01:00
$user = $account_id ;
2013-10-31 12:29:22 +01:00
// /<currentuser>/inbox/
if ( $user == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && $app == 'inbox' )
{
$app = 'calendar' ;
}
2008-05-08 22:31:32 +02:00
}
else
{
2009-10-03 12:22:14 +02:00
$user_prefix = '' ;
2008-05-08 22:31:32 +02:00
$user = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
2010-02-26 12:04:01 +01:00
2023-07-21 08:54:06 +02:00
// Api\WebDAV\Server encodes %, # and ? again, which leads to storing e.g. '%' as '%25'
2024-02-05 20:06:18 +01:00
$id = strtr ( array_shift ( $parts ), array (
2018-10-09 18:03:17 +02:00
'%25' => '%' ,
'%23' => '#' ,
'%3F' => '?' ,
));
2010-02-26 12:04:01 +01:00
2023-07-31 16:24:58 +02:00
$ok = ( $id || isset ( $_GET [ 'add-member' ]) && $_SERVER [ 'REQUEST_METHOD' ] === 'POST' ) &&
( $user || $user === 0 ) && self :: app_handler ( $app );
2012-02-04 21:24:01 +01:00
2010-03-07 00:06:43 +01:00
if ( $this -> debug )
2008-05-08 22:31:32 +02:00
{
2010-03-07 00:06:43 +01:00
error_log ( __METHOD__ . " (' $path ') returning " . ( $ok ? 'true' : 'false' ) . " : id=' $id ', app=' $app ', user=' $user ', user_prefix=' $user_prefix ' " );
2008-05-08 22:31:32 +02:00
}
return $ok ;
}
2011-11-23 17:34:39 +01:00
2012-10-04 13:59:04 +02:00
protected static $request_starttime ;
/**
* Log level from user prefs : $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'debug_level' ])
* - 'f' files directory
* - 'r' to error - log , but only shortend requests
*
* @ var string
*/
protected static $log_level ;
2012-02-20 10:06:24 +01:00
2011-11-23 17:34:39 +01:00
/**
* Serve WebDAV HTTP request
*
2012-02-17 10:14:33 +01:00
* Reimplemented to add logging
2016-06-09 09:00:57 +02:00
*
2023-07-21 08:54:06 +02:00
* @ param $prefix = null prefix filesystem path with given path , e . g . " /webdav " for owncloud 4.5 remote . php
2011-11-23 17:34:39 +01:00
*/
2016-06-09 09:00:57 +02:00
function ServeRequest ( $prefix = null )
2011-11-23 17:34:39 +01:00
{
2012-10-04 13:59:04 +02:00
if (( self :: $log_level = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'debug_level' ]) === 'r' ||
self :: $log_level === 'f' || $this -> debug )
2011-11-23 17:34:39 +01:00
{
2012-02-20 10:06:24 +01:00
self :: $request_starttime = microtime ( true );
2013-09-23 12:21:31 +02:00
// do NOT log non-text attachments
2023-06-30 17:13:42 +02:00
$this -> store_request = $_SERVER [ 'REQUEST_METHOD' ] != 'POST' ||
! self :: isFileUpload () ||
2024-01-26 11:53:22 +01:00
substr ( $_SERVER [ 'CONTENT_TYPE' ], 0 , 5 ) == 'text/' ||
str_starts_with ( $_SERVER [ 'CONTENT_TYPE' ], 'application/json' );
2011-11-23 17:34:39 +01:00
}
2023-07-15 08:29:34 +02:00
// unconditionally start output-buffering to fix problems with huge multiget reports from TB110 AB
ob_start ();
2016-06-09 09:00:57 +02:00
parent :: ServeRequest ( $prefix );
2011-11-23 17:34:39 +01:00
2023-07-21 08:54:06 +02:00
if ( self :: $request_starttime ) $this -> log_request ();
2012-02-20 10:06:24 +01:00
}
2023-06-30 17:13:42 +02:00
/**
* Check if request is a possibly large , binary file upload :
* - CalDAV managed attachments or
* - Mail REST API attachment upload
2024-02-05 20:06:18 +01:00
* - REST API attachment upload to / $app / $id / links /
2023-06-30 17:13:42 +02:00
*
* @ return bool
*/
protected static function isFileUpload ()
{
return ( isset ( $_GET [ 'action' ]) && in_array ( $_GET [ 'action' ], array ( 'attachment-add' , 'attachment-update' ))) ||
2024-02-05 20:06:18 +01:00
strpos ( $_SERVER [ 'REQUEST_URI' ], '/mail/attachments/' ) ||
strpos ( $_SERVER [ 'REQUEST_URI' ], '/links/' ) && $_SERVER [ 'REQUEST_METHOD' ] === 'POST' && $_SERVER [ 'CONTENT_TYPE' ] !== 'application/json' ;
2023-06-30 17:13:42 +02:00
}
2017-10-30 14:59:58 +01:00
/**
2023-07-21 08:54:06 +02:00
* Sanitizing filename to gard against path traversal and / e . g . in UserAgent string
2017-10-30 14:59:58 +01:00
*
* @ param string $filename
* @ return string
*/
public static function sanitize_filename ( $filename )
{
return str_replace ( array ( '../' , '/' ), array ( '' , '!' ), $filename );
}
2012-02-20 10:06:24 +01:00
/**
* Log the request
*
2023-07-21 08:54:06 +02:00
* @ param string $extra = '' extra text to add below request - log , e . g . exception thrown
2012-02-20 10:06:24 +01:00
*/
2012-10-04 13:59:04 +02:00
protected function log_request ( $extra = '' )
2012-02-20 10:06:24 +01:00
{
if ( self :: $request_starttime )
2011-11-23 17:34:39 +01:00
{
2012-10-04 13:59:04 +02:00
if ( self :: $log_level === 'f' )
2011-11-24 13:20:13 +01:00
{
$msg_file = $GLOBALS [ 'egw_info' ][ 'server' ][ 'files_dir' ];
$msg_file .= '/groupdav' ;
2017-10-30 14:59:58 +01:00
$msg_file .= '/' . self :: sanitize_filename ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ]) . '/' ;
2023-07-21 08:54:06 +02:00
if ( ! file_exists ( $msg_file ) && ! mkdir ( $msg_file , 0700 , true ) && ! is_dir ( $msg_file ))
2011-11-24 13:20:13 +01:00
{
error_log ( __METHOD__ . " () Could NOT create directory ' $msg_file '! " );
return ;
}
2013-09-25 09:46:02 +02:00
// stop CalDAVTester from creating one log per test-step
if ( substr ( $_SERVER [ 'HTTP_USER_AGENT' ], 0 , 14 ) == 'scripts/tests/' )
{
$msg_file .= 'CalDAVTester.log' ;
}
else
{
2017-10-30 14:59:58 +01:00
$msg_file .= self :: sanitize_filename ( $_SERVER [ 'HTTP_USER_AGENT' ]) . '.log' ;
2013-09-25 09:46:02 +02:00
}
2012-02-17 10:14:33 +01:00
$content = '*** ' . $_SERVER [ 'REMOTE_ADDR' ] . ' ' . date ( 'c' ) . " \n " ;
2011-11-24 13:20:13 +01:00
}
2012-02-17 10:14:33 +01:00
$content .= $_SERVER [ 'REQUEST_METHOD' ] . ' ' . $_SERVER [ 'REQUEST_URI' ] . ' HTTP/1.1' . " \n " ;
2011-11-23 17:34:39 +01:00
// reconstruct headers
foreach ( $_SERVER as $name => $value )
{
list ( $type , $name ) = explode ( '_' , $name , 2 );
2011-11-24 13:20:13 +01:00
if ( $type == 'HTTP' || $type == 'CONTENT' )
{
2012-02-17 10:14:33 +01:00
$content .= str_replace ( ' ' , '-' , ucwords ( strtolower (( $type == 'HTTP' ? '' : $type . ' ' ) . str_replace ( '_' , ' ' , $name )))) .
': ' . ( $name == 'AUTHORIZATION' ? 'Basic ***************' : $value ) . " \n " ;
2011-11-24 13:20:13 +01:00
}
2011-11-23 17:34:39 +01:00
}
2012-02-17 10:14:33 +01:00
$content .= " \n " ;
2011-11-23 17:34:39 +01:00
if ( $this -> request )
{
2012-02-18 11:49:24 +01:00
$content .= $this -> request . " \n " ;
2011-11-23 17:34:39 +01:00
}
2012-02-17 10:14:33 +01:00
$content .= 'HTTP/1.1 ' . $this -> _http_status . " \n " ;
2013-09-25 09:11:27 +02:00
$content .= 'Date: ' . str_replace ( '+0000' , 'GMT' , gmdate ( 'r' )) . " \n " ;
$content .= 'Server: ' . $_SERVER [ 'SERVER_SOFTWARE' ] . " \n " ;
2016-04-02 10:40:34 +02:00
foreach ( headers_list () as $line )
{
$content .= $line . " \n " ;
}
2012-02-17 10:14:33 +01:00
if (( $c = ob_get_flush ())) $content .= " \n " ;
2012-10-08 13:20:29 +02:00
if ( self :: $log_level !== 'f' && strlen ( $c ) > 1536 ) $c = substr ( $c , 0 , 1536 ) . " \n *** LOG TRUNKATED \n " ;
2012-02-17 10:14:33 +01:00
$content .= $c ;
2012-02-20 10:06:24 +01:00
if ( $extra ) $content .= $extra ;
2012-02-21 21:04:45 +01:00
if ( $this -> to_log ) $content .= " \n ### " . implode ( " \n ### " , $this -> to_log ) . " \n " ;
2012-04-12 12:44:00 +02:00
$content .= $this -> _http_status [ 0 ] == '4' && substr ( $this -> _http_status , 0 , 3 ) != '412' ||
$this -> _http_status [ 0 ] == '5' ? '###' : '***' ; // mark failed requests with ###, instead of ***
$content .= sprintf ( ' %s --> "%s" took %5.3f s' , $_SERVER [ 'REQUEST_METHOD' ] . ( $_SERVER [ 'REQUEST_METHOD' ] == 'REPORT' ? ' ' . $this -> propfind_options [ 'root' ][ 'name' ] : '' ) . ' ' . $_SERVER [ 'PATH_INFO' ], $this -> _http_status , microtime ( true ) - self :: $request_starttime ) . " \n \n " ;
2012-02-21 21:04:45 +01:00
if ( $msg_file && ( $f = fopen ( $msg_file , 'a' )))
{
flock ( $f , LOCK_EX );
fwrite ( $f , $content );
flock ( $f , LOCK_UN );
fclose ( $f );
}
else
{
2016-04-02 10:40:34 +02:00
foreach ( explode ( " \n " , $content ) as $line )
{
error_log ( $line );
}
2012-02-21 21:04:45 +01:00
}
2012-02-17 10:14:33 +01:00
}
}
2013-09-25 09:11:27 +02:00
/**
* Output xml error element
*
* @ param string | array $xml_error string with name for empty element in DAV NS or array with props
2016-04-02 10:40:34 +02:00
* @ param string $human_readable = null human readable error message
2013-09-25 09:11:27 +02:00
*/
public static function xml_error ( $xml_error , $human_readable = null )
{
header ( 'Content-type: application/xml; charset=utf-8' );
2016-04-02 12:44:17 +02:00
$xml = new \XMLWriter ;
2013-09-25 09:11:27 +02:00
$xml -> openMemory ();
$xml -> setIndent ( true );
$xml -> startDocument ( '1.0' , 'utf-8' );
$xml -> startElementNs ( null , 'error' , 'DAV:' );
self :: add_prop ( $xml , $xml_error );
if ( ! empty ( $human_readable ))
{
$xml -> writeElement ( 'responsedescription' , $human_readable );
}
$xml -> endElement (); // DAV:error
$xml -> endDocument ();
echo $xml -> outputMemory ();
}
/**
2023-07-21 08:54:06 +02:00
* Recursively add properties to XMLWriter object
2013-09-25 09:11:27 +02:00
*
2016-04-02 12:44:17 +02:00
* @ param \XMLWriter $xml
2013-09-25 09:11:27 +02:00
* @ param string | array $props string with name for empty element in DAV NS or array with props
*/
2016-04-02 12:44:17 +02:00
protected static function add_prop ( \XMLWriter $xml , $props )
2013-09-25 09:11:27 +02:00
{
if ( is_string ( $props )) $props = self :: mkprop ( $props , '' );
if ( isset ( $props [ 'name' ])) $props = array ( $props );
foreach ( $props as $prop )
{
if ( isset ( $prop [ 'ns' ]) && $prop [ 'ns' ] !== 'DAV:' )
{
$xml -> startElementNs ( null , $prop [ 'name' ], $prop [ 'ns' ]);
}
else
{
$xml -> startElement ( $prop [ 'name' ]);
}
if ( is_array ( $prop [ 'val' ]))
{
self :: add_prop ( $xml , $prop [ 'val' ]);
}
else
{
$xml -> text (( string ) $prop [ 'val' ]);
}
$xml -> endElement ();
}
}
2012-02-17 10:14:33 +01:00
/**
2012-02-21 21:04:45 +01:00
* Content of log () calls , to be appended to request_log
*
* @ var array
*/
private $to_log = array ();
/**
* Log unconditional to own request - and PHP error - log
2012-02-17 10:14:33 +01:00
*
2012-02-21 21:04:45 +01:00
* @ param string $str
2012-02-17 10:14:33 +01:00
*/
2012-02-21 21:04:45 +01:00
public function log ( $str )
2012-02-17 10:14:33 +01:00
{
2012-10-29 13:14:33 +01:00
$this -> to_log [] = $str ;
2012-02-21 21:04:45 +01:00
error_log ( $str );
2011-11-23 17:34:39 +01:00
}
2012-02-20 10:06:24 +01:00
/**
* Exception handler , which additionally logs the request ( incl . a trace )
*
* Does NOT return and get installed in constructor .
*
2016-10-31 18:29:32 +01:00
* @ param \Exception | \Error $e
2012-02-20 10:06:24 +01:00
*/
2016-10-31 18:29:32 +01:00
public static function exception_handler ( $e )
2012-02-20 10:06:24 +01:00
{
// logging exception as regular egw_execption_hander does
2016-04-02 10:40:34 +02:00
$headline = null ;
2012-02-20 10:06:24 +01:00
_egw_log_exception ( $e , $headline );
2021-09-17 20:15:36 +02:00
if ( self :: isJSON ())
{
header ( 'Content-Type: application/json; charset=utf-8' );
2023-11-29 14:47:27 +01:00
if ( is_a ( $e , JsParseException :: class ))
2021-09-17 20:15:36 +02:00
{
$status = '422 Unprocessable Entity' ;
}
else
{
$status = '500 Internal Server Error' ;
}
http_response_code (( int ) $status );
echo self :: json_encode ([
'error' => $e -> getCode () ? : ( int ) $status ,
'message' => $e -> getMessage (),
] + ( $e -> getPrevious () ? [
'original' => get_class ( $e -> getPrevious ()) . ': ' . $e -> getPrevious () -> getMessage (),
] : []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
}
else
{
// exception handler sending message back to the client as basic auth message
$error = str_replace ( array ( " \r " , " \n " ), array ( '' , ' | ' ), $e -> getMessage ());
header ( 'WWW-Authenticate: Basic realm="' . $headline . ': ' . $error . '"' );
header ( 'HTTP/1.1 401 Unauthorized' );
header ( 'X-WebDAV-Status: 401 Unauthorized' , true );
}
2012-02-20 10:06:24 +01:00
// if our own logging is active, log the request plus a trace, if enabled in server-config
2012-10-04 13:59:04 +02:00
if ( self :: $request_starttime && isset ( self :: $instance ))
2012-02-20 10:06:24 +01:00
{
2021-09-17 20:15:36 +02:00
self :: $instance -> _http_status = self :: isJSON () ? $status : '401 Unauthorized' ; // to correctly log it
2012-02-20 10:06:24 +01:00
if ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'exception_show_trace' ])
{
2012-10-04 13:59:04 +02:00
self :: $instance -> log_request ( " \n " . $e -> getTraceAsString () . " \n " );
2012-02-20 10:06:24 +01:00
}
else
{
2012-10-04 13:59:04 +02:00
self :: $instance -> log_request ();
2012-02-20 10:06:24 +01:00
}
}
exit ;
}
2016-04-02 12:44:17 +02:00
/**
* Generate a unique id , which can be used for syncronisation
*
* @ param string $_appName the appname
* @ param string $_eventID the id of the content
* @ return string the unique id
*/
static function generate_uid ( $_appName , $_eventID )
{
if ( empty ( $_appName ) || empty ( $_eventID )) return false ;
return $_appName . '-' . $_eventID . '-' . $GLOBALS [ 'egw_info' ][ 'server' ][ 'install_id' ];
}
2022-11-07 20:50:45 +01:00
}