CalDAV::managed_id2path()   B
last analyzed

Complexity

Conditions 8
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 9
nc 4
nop 3
dl 0
loc 18
rs 8.4444
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware: CalDAV/CardDAV/GroupDAV access
4
 *
5
 * @link http://www.egroupware.org
6
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
7
 * @package api
8
 * @subpackage caldav
9
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @copyright (c) 2007-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api;
15
16
use EGroupware\Api\CalDAV\Handler;
17
use EGroupware\Api\CalDAV\Principals;
18
19
// explicit import non-namespaced classes
20
require_once(__DIR__.'/WebDAV/Server.php');
21
use HTTP_WebDAV_Server;
22
use calendar_hooks;
23
24
/**
25
 * EGroupware: GroupDAV access
26
 *
27
 * Using a modified PEAR HTTP/WebDAV/Server class from API!
28
 *
29
 * One can use the following url's releative (!) to http://domain.com/egroupware/groupdav.php
30
 *
31
 * - /                        base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here
32
 * - /principals/             principal-collection-set for WebDAV ACL
33
 * - /principals/users/<username>/
34
 * - /principals/groups/<groupname>/
35
 * - /<username>/             users home-set with
36
 * - /<username>/addressbook/ addressbook of user or group <username> given the user has rights to view it
37
 * - /<current-username>/addressbook-<other-username>/ shared addressbooks from other user or group
38
 * - /<current-username>/addressbook-accounts/ all accounts current user has rights to see
39
 * - /<username>/calendar/    calendar of user <username> given the user has rights to view it
40
 * - /<username>/calendar/?download download whole calendar as .ics file (GET request!)
41
 * - /<current-username>/calendar-<other-username>/ shared calendar from other user or group (only current <username>!)
42
 * - /<username>/inbox/       scheduling inbox of user <username>
43
 * - /<username>/outbox/      scheduling outbox of user <username>
44
 * - /<username>/infolog/     InfoLog's of user <username> given the user has rights to view it
45
 * - /addressbook/ all addressbooks current user has rights to, announced as directory-gateway now
46
 * - /addressbook-accounts/ all accounts current user has rights to see
47
 * - /calendar/    calendar of current user
48
 * - /infolog/     infologs of current user
49
 * - /(resources|locations)/<resource-name>/calendar calendar of a resource/location, if user has rights to view
50
 * - /<current-username>/(resource|location)-<resource-name> shared calendar from a resource/location
51
 *
52
 * Shared addressbooks or calendars are only shown in in users home-set, if he subscribed to it via his CalDAV preferences!
53
 *
54
 * Calling one of the above collections with a GET request / regular browser generates an automatic index
55
 * from the data of a allprop PROPFIND, allow to browse CalDAV/CardDAV/GroupDAV tree with a regular browser.
56
 *
57
 * Permanent error_log() calls should use groupdav->log($str) instead, to be send to PHP error_log()
58
 * and our request-log (prefixed with "### " after request and response, like exceptions).
59
 *
60
 * @link http://www.groupdav.org/ GroupDAV spec
61
 * @link http://caldav.calconnect.org/ CalDAV resources
62
 * @link http://carddav.calconnect.org/ CardDAV resources
63
 * @link http://calendarserver.org/ Apple calendar and contacts server
64
 */
65
class CalDAV extends HTTP_WebDAV_Server
66
{
67
	/**
68
	 * DAV namespace
69
	 */
70
	const DAV = 'DAV:';
71
	/**
72
	 * GroupDAV namespace
73
	 */
74
	const GROUPDAV = 'http://groupdav.org/';
75
	/**
76
	 * CalDAV namespace
77
	 */
78
	const CALDAV = 'urn:ietf:params:xml:ns:caldav';
79
	/**
80
	 * CardDAV namespace
81
	 */
82
	const CARDDAV = 'urn:ietf:params:xml:ns:carddav';
83
	/**
84
	 * Apple Calendarserver namespace (eg. for ctag)
85
	 */
86
	const CALENDARSERVER = 'http://calendarserver.org/ns/';
87
	/**
88
	 * Apple Addressbookserver namespace (eg. for ctag)
89
	 */
90
	const ADDRESSBOOKSERVER = 'http://addressbookserver.org/ns/';
91
	/**
92
	 * Apple iCal namespace (eg. for calendar color)
93
	 */
94
	const ICAL = 'http://apple.com/ns/ical/';
95
	/**
96
	 * Realm and powered by string
97
	 */
98
	const REALM = 'EGroupware CalDAV/CardDAV/GroupDAV server';
99
100
	var $dav_powered_by = self::REALM;
101
	var $http_auth_realm = self::REALM;
102
103
	/**
104
	 * Folders in root or user home
105
	 *
106
	 * @var array
107
	 */
108
	var $root = array(
109
		'addressbook' => array(
110
			'resourcetype' => array(self::GROUPDAV => 'vcard-collection', self::CARDDAV => 'addressbook'),
111
			'component-set' => array(self::GROUPDAV => 'VCARD'),
112
		),
113
		'calendar' => array(
114
			'resourcetype' => array(self::GROUPDAV => 'vevent-collection', self::CALDAV => 'calendar'),
115
			'component-set' => array(self::GROUPDAV => 'VEVENT'),
116
		),
117
		'inbox' => array(
118
			'resourcetype' => array(self::CALDAV => 'schedule-inbox'),
119
			'app' => 'calendar',
120
			'user-only' => true,	// display just in user home
121
		),
122
		'outbox' => array(
123
			'resourcetype' => array(self::CALDAV => 'schedule-outbox'),
124
			'app' => 'calendar',
125
			'user-only' => true,	// display just in user home
126
		),
127
		'infolog' => array(
128
			'resourcetype' => array(self::GROUPDAV => 'vtodo-collection', self::CALDAV => 'calendar'),
129
			'component-set' => array(self::GROUPDAV => 'VTODO'),
130
		),
131
	);
132
	/**
133
	 * Debug level: 0 = nothing, 1 = function calls, 2 = more info, 3 = complete $_SERVER array
134
	 *
135
	 * Can now be enabled on a per user basis in GroupDAV prefs, if it is set here to 0!
136
	 *
137
	 * The debug messages are send to the apache error_log
138
	 *
139
	 * @var integer
140
	 */
141
	var $debug = 0;
142
143
	/**
144
	 * eGW's charset
145
	 *
146
	 * @var string
147
	 */
148
	var $egw_charset;
149
	/**
150
	 * Instance of our application specific handler
151
	 *
152
	 * @var Handler
153
	 */
154
	var $handler;
155
	/**
156
	 * current-user-principal URL
157
	 *
158
	 * @var string
159
	 */
160
	var $current_user_principal;
161
	/**
162
	 * Reference to the accounts class
163
	 *
164
	 * @var accounts
165
	 */
166
	var $accounts;
167
	/**
168
	 * Supported privileges with name and description
169
	 *
170
	 * privileges are hierarchical
171
	 *
172
	 * @var array
173
	 */
174
	var $supported_privileges = array(
175
		'all' => array(
176
			'*description*' => 'all privileges',
177
			'read' => array(
178
				'*description*' => 'read resource',
179
				'read-free-busy' => array(
180
					'*ns*' => self::CALDAV,
181
					'*description*' => 'allow free busy report query',
182
					'*only*' => '/calendar/',
183
				),
184
			),
185
			'write' => array(
186
				'*description*' => 'write resource',
187
				'write-properties' => 'write resource properties',
188
				'write-content' => 'write resource content',
189
				'bind' => 'add child resource',
190
				'unbind' => 'remove child resource',
191
			),
192
			'unlock' => 'unlock resource without ownership of lock',
193
			'read-acl' => 'read resource access control list',
194
			'write-acl' => 'write resource access control list',
195
			'read-current-user-privilege-set' => 'read privileges for current principal',
196
			'schedule-deliver' => array(
197
				'*ns*' => self::CALDAV,
198
				'*description*' => 'schedule privileges for current principal',
199
				'*only*' => '/inbox/',
200
			),
201
			'schedule-send' => array(
202
				'*ns*' => self::CALDAV,
203
				'*description*' => 'schedule privileges for current principal',
204
				'*only*' => '/outbox/',
205
			),
206
		),
207
	);
208
	/**
209
	 * $options parameter to PROPFIND request, eg. to check what props are requested
210
	 *
211
	 * @var array
212
	 */
213
	var $propfind_options;
214
215
	/**
216
	 * Reference to active instance, used by exception handler
217
	 *
218
	 * @var groupdav
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\groupdav was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
219
	 */
220
	protected static $instance;
221
222
	function __construct()
223
	{
224
		// log which CalDAVTester test is currently running, set as User-Agent header
225
		if (substr($_SERVER['HTTP_USER_AGENT'], 0, 14) == 'scripts/tests/') error_log('****** '.$_SERVER['HTTP_USER_AGENT']);
226
227
		if (!$this->debug) $this->debug = (int)$GLOBALS['egw_info']['user']['preferences']['groupdav']['debug_level'];
228
229
		if ($this->debug > 2) error_log('groupdav: $_SERVER='.array2string($_SERVER));
230
231
		// setting our own exception handler, to be able to still log the requests
232
		set_exception_handler(array(__CLASS__,'exception_handler'));
233
234
		// crrnd: client refuses redundand namespace declarations
235
		// setting redundand namespaces as the default for (Cal|Card|Group)DAV, as the majority of the clients either require or can live with it
236
		$this->crrnd = false;
237
238
		// identify clients, which do NOT support path AND full url in <D:href> of PROPFIND request
239
		switch(($agent = Handler::get_agent()))
240
		{
241
			case 'kde':	// KAddressbook (at least in 3.5 can NOT subscribe / does NOT find addressbook)
242
				$this->client_require_href_as_url = true;
243
				break;
244
			case 'cfnetwork':	// Apple addressbook app
245
			case 'dataaccess':	// iPhone addressbook
246
				$this->client_require_href_as_url = false;
247
				break;
248
			case 'davkit':	// iCal app in OS X 10.6 created wrong request, if full url given
249
			case 'coredav':	// iCal app in OS X 10.7
250
			case 'calendarstore':	// Apple iCal 5.0.1 under OS X 10.7.2
251
				$this->client_require_href_as_url = false;
252
				break;
253
			case 'cfnetwork_old':
254
				$this->crrnd = true; // Older Apple Addressbook.app does not cope with namespace redundancy
255
				break;
256
		}
257
		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");
258
259
		// adding EGroupware version to X-Dav-Powered-By header eg. "EGroupware 1.8.001 CalDAV/CardDAV/GroupDAV server"
260
		$this->dav_powered_by = str_replace('EGroupware','EGroupware '.$GLOBALS['egw_info']['server']['versions']['phpgwapi'],
261
			$this->dav_powered_by);
262
263
		parent::__construct();
264
		// hack to allow to use query parameters in WebDAV, which HTTP_WebDAV_Server interprets as part of the path
265
		list($this->_SERVER['REQUEST_URI']) = explode('?',$this->_SERVER['REQUEST_URI']);
266
		// OSX Addressbook sends ?add-member url-encoded
267
		if (substr($this->_SERVER['REQUEST_URI'], -14) == '/%3Fadd-member')
268
		{
269
			$_GET['add-member'] = '';
270
			$this->_SERVER['REQUEST_URI'] = substr($this->_SERVER['REQUEST_URI'], 0, -14);
271
		}
272
		//error_log($_SERVER['REQUEST_URI']." --> ".$this->_SERVER['REQUEST_URI']);
273
274
		$this->egw_charset = Translation::charset();
275
		if (strpos($this->base_uri, 'http') === 0)
276
		{
277
			$this->current_user_principal = $this->_slashify($this->base_uri);
278
		}
279
		else
280
		{
281
			$this->current_user_principal = Framework::getUrl($_SERVER['SCRIPT_NAME']) . '/';
282
		}
283
		$this->current_user_principal .= 'principals/users/'.$GLOBALS['egw_info']['user']['account_lid'].'/';
284
285
		// if client requires pathes instead of URLs
286
		if (!$this->client_require_href_as_url)
287
		{
288
			$this->current_user_principal = parse_url($this->current_user_principal,PHP_URL_PATH);
289
		}
290
		$this->accounts = $GLOBALS['egw']->accounts;
291
292
		self::$instance = $this;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this of type EGroupware\Api\CalDAV is incompatible with the declared type EGroupware\Api\groupdav of property $instance.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
293
	}
294
295
	/**
296
	 * get the handler for $app
297
	 *
298
	 * @param string $app
299
	 * @return Handler
300
	 */
301
	function app_handler($app)
302
	{
303
		if (isset($this->root[$app]['app'])) $app = $this->root[$app]['app'];
304
305
		return Handler::app_handler($app,$this);
306
	}
307
308
	/**
309
	 * OPTIONS request, allow to modify the standard responses from the pear-class
310
	 *
311
	 * @param string $path
312
	 * @param array &$dav
313
	 * @param array &$allow
314
	 */
315
	function OPTIONS($path, &$dav, &$allow)
316
	{
317
		unset($allow);	// not used, but required by function signature
318
319
		// locking support
320
		if (!in_array('2', $dav)) $dav[] = '2';
321
322
		if (preg_match('#/(calendar(-[^/]+)?|inbox|outbox)/#', $path))	// eg. /<username>/calendar-<otheruser>/
323
		{
324
			$app = 'calendar';
325
		}
326
		elseif (preg_match('#/addressbook(-[^/]+)?/#', $path))	// eg. /<username>/addressbook-<otheruser>/
327
		{
328
			$app = 'addressbook';
329
		}
330
		// CalDAV and CardDAV
331
		$dav[] = 'access-control';
332
333
		if ($app !== 'addressbook')	// CalDAV
334
		{
335
			$dav[] = 'calendar-access';
336
			$dav[] = 'calendar-auto-schedule';
337
			$dav[] = 'calendar-proxy';
338
			// required by iOS iCal to use principal-property-search to autocomplete participants (and locations)
339
			$dav[] = 'calendarserver-principal-property-search';
340
			// required by iOS & OS X iCal to show private checkbox (X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALENDAR)
341
			$dav[] = 'calendarserver-private-events';
342
			// managed attachments
343
			$dav[] = 'calendar-managed-attachments';
344
			// other capabilities calendarserver announces
345
			//$dav[] = 'calendar-schedule';
346
			//$dav[] = 'calendar-availability';
347
			//$dav[] = 'inbox-availability';
348
			//$dav[] = 'calendarserver-private-comments';
349
			//$dav[] = 'calendarserver-sharing';
350
			//$dav[] = 'calendarserver-sharing-no-scheduling';
351
		}
352
		if ($app !== 'calendar')	// CardDAV
353
		{
354
			$dav[] = 'addressbook';	// CardDAV uses "addressbook" NOT "addressbook-access"
355
		}
356
		//error_log(__METHOD__."('$path') --> app='$app' --> DAV: ".implode(', ', $dav));
357
	}
358
359
	/**
360
	 * PROPFIND and REPORT method handler
361
	 *
362
	 * @param  array  general parameter passing array
363
	 * @param  array  return array for file properties
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\return was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
364
	 * @return bool   true on success
365
	 */
366
	function PROPFIND(&$options, &$files, $method='PROPFIND')
367
	{
368
		if ($this->debug) error_log(__CLASS__."::$method(".array2string($options).')');
369
370
		// make options (readonly) available to all class methods, eg. prop_requested
371
		$this->propfind_options = $options;
372
373
		// parse path in form [/account_lid]/app[/more]
374
		$id = $app = $user = $user_prefix = null;
375
		if (!self::_parse_path($options['path'],$id,$app,$user,$user_prefix) && $app && !$user && $user !== 0)
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

375
		if (!self::/** @scrutinizer ignore-call */ _parse_path($options['path'],$id,$app,$user,$user_prefix) && $app && !$user && $user !== 0)
Loading history...
376
		{
377
			if ($this->debug > 1) error_log(__CLASS__."::$method: user='$user', app='$app', id='$id': 404 not found!");
378
			return '404 Not Found';
379
		}
380
		if ($this->debug > 1) error_log(__CLASS__."::$method(path='$options[path]'): user='$user', user_prefix='$user_prefix', app='$app', id='$id'");
381
382
		$files = array('files' => array());
383
		$path = $user_prefix = $this->_slashify($user_prefix);
0 ignored issues
show
Unused Code introduced by
The assignment to $user_prefix is dead and can be removed.
Loading history...
384
385
		if (!$app)	// user root folder containing apps
386
		{
387
			// add root with current users apps
388
			$this->add_home($files, $path, $user, $options['depth']);
389
390
			if ($path == '/')
391
			{
392
				Hooks::process(array(
393
					'location' => 'groupdav_root_props',
394
					'props' => &$files['files'][0]['props'],
395
					'options' => $options,
396
					'caldav' => $this,
397
				));
398
			}
399
400
			// add principals and user-homes
401
			if ($path == '/' && $options['depth'])
402
			{
403
				// principals collection
404
				$files['files'][] = $this->add_collection('/principals/', array(
405
					'displayname' => lang('Accounts'),
406
				));
407
				foreach($this->accounts->search(array('type' => 'both','order'=>'account_lid')) as $account)
408
				{
409
					$this->add_home($files, $path.$account['account_lid'].'/', $account['account_id'], $options['depth'] == 'infinity' ? 'infinity' : $options['depth']-1);
410
				}
411
			}
412
			return true;
413
		}
414
		if ($path == '/' && ($app == 'resources' || $app == 'locations'))
415
		{
416
			return $this->add_resources_collection($files, '/'.$app.'/', $options['depth']);
417
		}
418
		if ($app != 'principals' && !isset($GLOBALS['egw_info']['user']['apps'][$this->root[$app]['app'] ? $this->root[$app]['app'] : $app]))
419
		{
420
			if ($this->debug) error_log(__CLASS__."::$method(path=$options[path]) 403 Forbidden: no app rights for '$app'");
421
			return "403 Forbidden: no app rights for '$app'";	// no rights for the given app
422
		}
423
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

423
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
424
		{
425
			if ($method != 'REPORT' && !$id)	// no self URL for REPORT requests (only PROPFIND) or propfinds on an id
426
			{
427
				// KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes
428
				$files['files'][0] = $this->add_app($app,$app=='addressbook'&&$handler->get_agent()=='kde',$user,
429
					$this->_slashify($options['path']));
430
431
				// Hack for iOS 5.0.1 addressbook to stop asking directory gateway permissions with depth != 0
432
				// values for depth are 0, 1, "infinit" or not set which has to be interpreted as "infinit"
433
				if ($method == 'PROPFIND' && $options['path'] == '/addressbook/' &&
434
					(!isset($options['depth']) || $options['depth']) && $handler->get_agent() == 'dataaccess')
435
				{
436
					$this->log(__CLASS__."::$method(".array2string($options).') Enabling hack for iOS 5.0.1 addressbook: force Depth: 0 on PROPFIND for directory gateway!');
437
					return true;
438
				}
439
				if (!$options['depth']) return true;	// depth 0 --> show only the self url
440
			}
441
			return $handler->propfind($this->_slashify($options['path']),$options,$files,$user,$id);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\CalDAV\Handler::propfind() has too many arguments starting with $id. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

441
			return $handler->/** @scrutinizer ignore-call */ propfind($this->_slashify($options['path']),$options,$files,$user,$id);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
442
		}
443
		return '501 Not Implemented';
444
	}
445
446
	/**
447
	 * Add a collection to a PROPFIND request
448
	 *
449
	 * @param string $path
450
	 * @param array $props =array() extra properties 'resourcetype' is added anyway, name => value pairs or name => HTTP_WebDAV_Server([namespace,]name,value)
451
	 * @param array $privileges =array('read') values for current-user-privilege-set
452
	 * @param array $supported_privileges =null default $this->supported_privileges
453
	 * @return array with values for keys 'path' and 'props'
454
	 */
455
	public function add_collection($path, array $props = array(), array $privileges=array('read','read-acl','read-current-user-privilege-set'), array $supported_privileges=null)
456
	{
457
		// resourcetype: collection
458
		$props['resourcetype'][] = self::mkprop('collection','');
459
460
		if (!isset($props['getcontenttype'])) $props['getcontenttype'] = 'httpd/unix-directory';
461
462
		return $this->add_resource($path, $props, $privileges, $supported_privileges);
463
	}
464
465
	/**
466
	 * Add a resource to a PROPFIND request
467
	 *
468
	 * @param string $path
469
	 * @param array $props =array() extra properties 'resourcetype' is added anyway, name => value pairs or name => HTTP_WebDAV_Server([namespace,]name,value)
470
	 * @param array $privileges =array('read') values for current-user-privilege-set
471
	 * @param array $supported_privileges =null default $this->supported_privileges
472
	 * @return array with values for keys 'path' and 'props'
473
	 */
474
	public function add_resource($path, array $props = array(), array $privileges=array('read','read-current-user-privilege-set'), array $supported_privileges=null)
475
	{
476
		// props for all collections: current-user-principal and principal-collection-set
477
		$props['current-user-principal'] = array(
478
			self::mkprop('href',$this->current_user_principal));
479
		$props['principal-collection-set'] = array(
480
			self::mkprop('href',$this->base_uri.'/principals/'));
481
482
		// required props per WebDAV standard
483
		foreach(array(
484
			'displayname'      => basename($path),
485
			'getetag'          => 'none',
486
			'getcontentlength' => '',
487
			'getlastmodified'  => '',
488
			'getcontenttype'   => '',
489
			'resourcetype'     => '',
490
		) as $name => $default)
491
		{
492
			if (!isset($props[$name])) $props[$name] = $default;
493
		}
494
495
		// if requested add privileges
496
		if (is_null($supported_privileges)) $supported_privileges = $this->supported_privileges;
497
		if ($this->prop_requested('current-user-privilege-set') === true)
498
		{
499
			foreach($privileges as $name)
500
			{
501
				$props['current-user-privilege-set'][] = self::mkprop('privilege', array(
502
					is_array($name) ? self::mkprop($name['ns'], $name['name'], '') : self::mkprop($name, '')));
503
			}
504
		}
505
		if ($this->prop_requested('supported-privilege-set') === true)
506
		{
507
			foreach($supported_privileges as $name => $data)
508
			{
509
				$props['supported-privilege-set'][] = $this->supported_privilege($name, $data, $path);
510
			}
511
		}
512
		if (!isset($props['owner']) && $this->prop_requested('owner') === true)
513
		{
514
			$props['owner'] = '';
515
		}
516
517
		if ($this->debug > 1) error_log(__METHOD__."(path='$path', props=".array2string($props).')');
518
519
		// convert simple associative properties to HTTP_WebDAV_Server ones
520
		foreach($props as $name => &$prop)
521
		{
522
			if (!is_array($prop) || !isset($prop['name']))
523
			{
524
				$prop = self::mkprop($name, $prop);
525
			}
526
			// add quotes around etag, if they are not already there
527
			if ($prop['name'] == 'getetag' && $prop['val'][0] != '"')
528
			{
529
				$prop['val'] = '"'.$prop['val'].'"';
530
			}
531
		}
532
533
		return array(
534
			'path' => $path,
535
			'props' => $props,
536
		);
537
	}
538
539
	/**
540
	 * Generate (hierachical) supported-privilege property
541
	 *
542
	 * @param string $name name of privilege
543
	 * @param string|array $data string with describtion or array with agregated privileges plus value for key '*description*', '*ns*', '*only*'
544
	 * @param string $path =null path to match with $data['*only*']
545
	 * @return array of self::mkprop() arrays
546
	 */
547
	protected function supported_privilege($name, $data, $path=null)
548
	{
549
		$props = array();
550
		$props[] = self::mkprop('privilege', array(is_array($data) && $data['*ns*'] ?
551
			self::mkprop($data['*ns*'], $name, '') : self::mkprop($name, '')));
552
		$props[] = self::mkprop('description', is_array($data) ? $data['*description*'] : $data);
553
		if (is_array($data))
554
		{
555
			foreach($data as $name => $data)
0 ignored issues
show
introduced by
$name is overwriting one of the parameters of this function.
Loading history...
introduced by
$data is overwriting one of the parameters of this function.
Loading history...
556
			{
557
				if ($name[0] == '*') continue;
558
				if (is_array($data) && $data['*only*'] && strpos($path, $data['*only*']) === false)
559
				{
560
					continue;	// wrong path
561
				}
562
				$props[] = $this->supported_privilege($name, $data, $path);
563
			}
564
		}
565
		return self::mkprop('supported-privilege', $props);
566
	}
567
568
	/**
569
	 * Checks if a given property was requested in propfind request
570
	 *
571
	 * @param string $name property name
572
	 * @param string $ns =null namespace, if that is to be checked too
573
	 * @param boolean $return_prop =false if true return the property array with values for 'name', 'xmlns', 'attrs', 'children'
574
	 * @return boolean|string|array true: $name explicitly requested (or autoindex), "all": allprop or "names": propname requested, false: $name was not requested
575
	 */
576
	function prop_requested($name, $ns=null, $return_prop=false)
577
	{
578
		if (!is_array($this->propfind_options) || !isset($this->propfind_options['props']))
0 ignored issues
show
introduced by
The condition is_array($this->propfind_options) is always true.
Loading history...
579
		{
580
			$ret = true;	// no props set, should happen only in autoindex, we return true to show all available props
581
		}
582
		elseif (!is_array($this->propfind_options['props']))
583
		{
584
			$ret = $this->propfind_options['props'];	// "all": allprop or "names": propname
585
		}
586
		else
587
		{
588
			$ret = false;
589
			foreach($this->propfind_options['props'] as $prop)
590
			{
591
				if ($prop['name'] == $name && (is_null($ns) || $prop['xmlns'] == $ns))
592
				{
593
					$ret = $return_prop ? $prop : true;
594
					break;
595
				}
596
			}
597
		}
598
		//error_log(__METHOD__."('$name', '$ns', $return_prop) propfind_options=".array2string($this->propfind_options));
599
		return $ret;
600
	}
601
602
	/**
603
	 * Add user home with addressbook, calendar, infolog
604
	 *
605
	 * @param array $files
606
	 * @param string $path / or /<username>/
607
	 * @param int $user
608
	 * @param int $depth
609
	 * @return string|boolean http status or true|false
610
	 */
611
	protected function add_home(array &$files, $path, $user, $depth)
612
	{
613
		if ($user)
614
		{
615
			$account_lid = $this->accounts->id2name($user);
616
		}
617
		else
618
		{
619
			$account_lid = $GLOBALS['egw_info']['user']['account_lid'];
620
		}
621
		$account = $this->accounts->read($account_lid);
622
623
		$calendar_user_address_set = array(
624
			self::mkprop('href','urn:uuid:'.$account['account_lid']),
625
		);
626
		if ($user < 0)
627
		{
628
			$principalType = 'groups';
629
			$displayname = lang('Group').' '.$account['account_lid'];
630
		}
631
		else
632
		{
633
			$principalType = 'users';
634
			$displayname = $account['account_fullname'];
635
			$calendar_user_address_set[] = self::mkprop('href','mailto:'.$account['account_email']);
636
		}
637
		$calendar_user_address_set[] = self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account['account_lid'].'/');
638
639
		if ($depth && $path == '/')
640
		{
641
			$displayname = 'EGroupware (Cal|Card|Group)DAV server';
642
		}
643
644
		$displayname = Translation::convert($displayname, Translation::charset(),'utf-8');
645
		// self url
646
		$props = array(
647
			'displayname' => $displayname,
648
			'owner' => $path == '/' ? '' : array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')),
649
		);
650
651
		if ($path != '/')
652
		{
653
			// add props modifyable via proppatch from client, eg. jqcalendar stores it's preferences there
654
			foreach((array)$GLOBALS['egw_info']['user']['preferences']['groupdav'] as $name => $value)
655
			{
656
				list($prop,$prop4path,$ns) = explode(':', $name, 3);
657
				if ($prop4path == $path && (!in_array($ns,self::$ns_needs_explicit_named_props) ||
658
					isset(self::$proppatch_props[$prop]) && self::$proppatch_props[$prop] === $ns))
659
				{
660
					$props[] = self::mkprop($ns, $prop, $value);
661
					//error_log(__METHOD__."() arbitrary $ns:$prop=".array2string($value));
662
				}
663
			}
664
		}
665
		$files['files'][] = $this->add_collection($path, $props);
666
667
		if ($depth)
668
		{
669
			foreach($this->root as $app => $data)
670
			{
671
				if (!$GLOBALS['egw_info']['user']['apps'][$data['app'] ? $data['app'] : $app]) continue;	// no rights for the given app
672
				if (!empty($data['user-only']) && ($path == '/' || $user < 0)) continue;
673
674
				$files['files'][] = $this->add_app($app,false,$user,$path.$app.'/');
675
676
				// only add global /addressbook-accounts/ as the one in home-set is added (and controled) by add_shared
677
				if ($path == '/' && $app == 'addressbook' &&
678
					$GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1')
679
				{
680
					$file = $this->add_app($app,false,0,$path.$app.'-accounts/');
681
					$file['props']['resourcetype']['val'][] = self::mkprop(self::CALENDARSERVER,'shared','');
682
					$files['files'][] = $file;
683
				}
684
				// added shared calendars or addressbooks
685
				$this->add_shared($files['files'], $path, $app, $user);
686
			}
687
			if ($path == '/' && $GLOBALS['egw_info']['user']['apps']['resources'])
688
			{
689
				$this->add_resources_collection($files, $path.'resources/');
690
				$this->add_resources_collection($files, $path.'locations/');
691
			}
692
		}
693
		return true;
694
	}
695
696
	/**
697
	 * Add collection with available resources or locations calendar-home-sets
698
	 *
699
	 * @param array &$files
700
	 * @param string $path / or /<username>/
701
	 * @param int $depth =0
702
	 * @return string|boolean http status or true|false
703
	 */
704
	protected function add_resources_collection(array &$files, $path, $depth=0)
705
	{
706
		if (!isset($GLOBALS['egw_info']['user']['apps']['resources']))
707
		{
708
			if ($this->debug) error_log(__METHOD__."(path=$path) 403 Forbidden: no app rights for 'resources'");
709
			return "403 Forbidden: no app rights for 'resources'";	// no rights for the given app
710
		}
711
		list(,$what) = explode('/', $path);
712
		if (($is_location = ($what == 'locations')))
713
		{
714
			$files['files'][] = $this->add_collection('/locations/', array('displayname' => lang('Location calendars')));
715
		}
716
		else
717
		{
718
			$files['files'][] = $this->add_collection('/resources/', array('displayname' => lang('Resource calendars')));
719
		}
720
		if ($depth)
721
		{
722
			foreach(Principals::get_resources() as $resource)
723
			{
724
				if ($is_location == Principals::resource_is_location($resource))
725
				{
726
					$files['files'][] = $this->add_app('calendar', false, 'r'.$resource['res_id'],
727
						'/'.Principals::resource2name($resource, $is_location).'/');
728
				}
729
			}
730
		}
731
		return true;
732
	}
733
734
	/**
735
	 * Add shared addressbook, calendar, infolog to user home
736
	 *
737
	 * @param array &$files
738
	 * @param string $path /<username>/
739
	 * @param int $app
740
	 * @param int $user
741
	 * @return string|boolean http status or true|false
742
	 */
743
	protected function add_shared(array &$files, $path, $app, $user)
744
	{
745
		// currently only show shared calendars/addressbooks for current user and not in the root
746
		if ($path == '/' || $user != $GLOBALS['egw_info']['user']['account_id'] ||
747
			!isset($GLOBALS['egw_info']['user']['apps'][$app]))	// also avoids principals, inbox and outbox
748
		{
749
			return true;
750
		}
751
		$handler = $this->app_handler($app);
752
		if (($shared = $handler->get_shared()))
753
		{
754
			foreach($shared as $id => $owner)
755
			{
756
				$file = $this->add_app($app,false,$id,$path.$owner.'/');
757
				// mark other users calendar as shared (iOS 5.0.1 AB does NOT display AB marked as shared!)
758
				if ($app == 'calendar') $file['props']['resourcetype']['val'][] = self::mkprop(self::CALENDARSERVER,'shared','');
759
				$files[] = $file;
760
			}
761
		}
762
		return true;
763
	}
764
765
	/**
766
	 * Format an account-name for use in displayname
767
	 *
768
	 * @param int|array $account
769
	 * @return string
770
	 */
771
	public function account_name($account)
772
	{
773
		if (is_array($account))
774
		{
775
			if ($account['account_id'] < 0)
776
			{
777
				$name = lang('Group').' '.$account['account_lid'];
778
			}
779
			else
780
			{
781
				$name = $account['account_fullname'];
782
			}
783
		}
784
		else
785
		{
786
			if ($account < 0)
787
			{
788
				$name = lang('Group').' '.$this->accounts->id2name($account,'account_lid');
789
			}
790
			else
791
			{
792
				$name = $this->accounts->id2name($account,'account_fullname');
793
			}
794
			if (empty($name)) $name = '#'.$account;
795
		}
796
		return $name;
797
	}
798
799
	/**
800
	 * Add an application collection to a user home or the root
801
	 *
802
	 * @param string $app
803
	 * @param boolean $no_extra_types =false should the GroupDAV and CalDAV types be added (KAddressbook has problems with it in self URL)
804
	 * @param int $user =null owner of the collection, default current user
805
	 * @param string $path ='/'
806
	 * @return array with values for keys 'path' and 'props'
807
	 */
808
	protected function add_app($app,$no_extra_types=false,$user=null,$path='/')
809
	{
810
		if ($this->debug) error_log(__METHOD__."(app='$app', no_extra_types=$no_extra_types, user='$user', path='$path')");
811
		$user_preferences = $GLOBALS['egw_info']['user']['preferences'];
0 ignored issues
show
Unused Code introduced by
The assignment to $user_preferences is dead and can be removed.
Loading history...
812
		if (is_string($user) && $user[0] == 'r' && ($resource = Principals::read_resource(substr($user, 1))))
0 ignored issues
show
introduced by
The condition is_string($user) is always false.
Loading history...
813
		{
814
			$is_location = Principals::resource_is_location($resource);
815
			$displayname = null;
816
			list($principalType, $account_lid) = explode('/', Principals::resource2name($resource, $is_location, $displayname));
817
		}
818
		elseif ($user)
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
819
		{
820
			$account_lid = $this->accounts->id2name($user);
821
			if ($user >= 0 && $GLOBALS['egw']->preferences->account_id != $user)
822
			{
823
				$GLOBALS['egw']->preferences->__construct($user);
824
				$user_preferences = $GLOBALS['egw']->preferences->read_repository();
825
				$GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_lid']);
826
			}
827
			$principalType = $user < 0 ? 'groups' : 'users';
828
		}
829
		else
830
		{
831
			$account_lid = $GLOBALS['egw_info']['user']['account_lid'];
832
			$principalType = 'users';
833
		}
834
		if (!isset($displayname)) $displayname = $this->account_name($user);
835
836
		$props = array(
837
			'owner' => array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')),
838
		);
839
840
		switch ($app)
841
		{
842
			case 'inbox':
843
				$props['displayname'] = lang('Scheduling inbox').' '.$displayname;
844
				break;
845
			case 'outbox':
846
				$props['displayname'] = lang('Scheduling outbox').' '.$displayname;
847
				break;
848
			case 'addressbook':
849
				if ($path == '/addressbook/')
850
				{
851
					$props['displayname'] = lang('All addressbooks');
852
					break;
853
				}
854
				elseif(!$user && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1')
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
855
				{
856
					unset($props['owner']);
857
					$props['displayname'] = lang($app).' '.lang('Accounts');
858
					break;
859
				}
860
				// fall through
861
			default:
862
				$props['displayname'] = Translation::convert(lang($app).' '.$displayname, $this->egw_charset, 'utf-8');
863
		}
864
865
		// rfc 5995 (Use POST to add members to WebDAV collections): we use collection path with add-member query param
866
		// 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
867
		if ($app && !in_array($app,array('inbox','outbox','principals')))	// not on inbox, outbox or principals
868
		{
869
			$props['add-member'][] = self::mkprop('href',$this->base_uri.$path.'?add-member');
870
		}
871
872
		// add props modifyable via proppatch from client, eg. calendar-color, see self::$proppatch_props
873
		$ns = null;
874
		foreach((array)$GLOBALS['egw_info']['user']['preferences'][$app] as $name => $value)
875
		{
876
			unset($ns);
877
			list($prop,$prop4user,$ns) = explode(':', $name, 3);
878
			if ($prop4user == (string)$user && isset(self::$proppatch_props[$prop]) && !isset($ns))
879
			{
880
				$props[$prop] = self::mkprop(self::$proppatch_props[$prop], $prop, $value);
881
				//error_log(__METHOD__."() explicit ".self::$proppatch_props[$prop].":$prop=".array2string($value));
882
			}
883
			// props in arbitrary namespaces not mentioned in self::$ns_needs_explicit_named_props
884
			elseif(isset($ns) && !in_array($ns,self::$ns_needs_explicit_named_props))
885
			{
886
				$props[] = self::mkprop($ns, $prop, $value);
887
				//error_log(__METHOD__."() arbitrary $ns:$prop=".array2string($value));
888
			}
889
		}
890
891
		foreach((array)$this->root[$app] as $prop => $values)
892
		{
893
			switch($prop)
894
			{
895
				case 'resourcetype';
896
					if (!$no_extra_types)
897
					{
898
						foreach($this->root[$app]['resourcetype'] as $ns => $type)
899
						{
900
							$props['resourcetype'][] = self::mkprop($ns,$type,'');
901
						}
902
						// add /addressbook/ as directory gateway
903
						if ($path == '/addressbook/')
904
						{
905
							$props['resourcetype'][] = self::mkprop(self::CARDDAV, 'directory', '');
906
						}
907
					}
908
					break;
909
				case 'app':
910
				case 'user-only':
911
					break;	// no props, already handled
912
				default:
913
					if (is_array($values))
914
					{
915
						foreach($values as $ns => $value)
916
						{
917
							$props[$prop] = self::mkprop($ns,$prop,$value);
918
						}
919
					}
920
					else
921
					{
922
						$props[$prop] = $values;
923
					}
924
					break;
925
			}
926
		}
927
		// add other handler specific properties
928
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

928
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
929
		{
930
			if (method_exists($handler,'extra_properties'))
931
			{
932
				$props = $handler->extra_properties($props, $displayname, $this->base_uri, $user, $path);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\CalDAV\Handler::extra_properties() has too many arguments starting with $path. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

932
				/** @scrutinizer ignore-call */ 
933
    $props = $handler->extra_properties($props, $displayname, $this->base_uri, $user, $path);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
933
			}
934
			// add ctag if handler implements it
935
			if (method_exists($handler,'getctag') && $this->prop_requested('getctag') === true)
936
			{
937
				$props['getctag'] = self::mkprop(
938
					self::CALENDARSERVER,'getctag',$handler->getctag($path,$user));
939
			}
940
			// add sync-token url if handler supports sync-collection report
941
			if (isset($props['supported-report-set']['sync-collection']) && $this->prop_requested('sync-token') === true)
942
			{
943
				$props['sync-token'] = $handler->get_sync_token($path,$user);
944
			}
945
		}
946
		if ($handler && !is_null($user))
947
		{
948
			return $this->add_collection($path, $props, $handler->current_user_privileges($path, $user));
949
		}
950
		return $this->add_collection($path, $props);
951
	}
952
953
	/**
954
	 * CalDAV/CardDAV REPORT method handler
955
	 *
956
	 * just calls PROPFIND()
957
	 *
958
	 * @param  array  general parameter passing array
959
	 * @param  array  return array for file properties
960
	 * @return bool   true on success
961
	 */
962
	function REPORT(&$options, &$files)
963
	{
964
		if ($this->debug > 1) error_log(__METHOD__.'('.array2string($options).')');
965
966
		return $this->PROPFIND($options,$files,'REPORT');
967
	}
968
969
	/**
970
	 * CalDAV/CardDAV REPORT method handler to get HTTP_WebDAV_Server to process REPORT requests
971
	 *
972
	 * Just calls http_PROPFIND()
973
	 */
974
	function http_REPORT()
975
	{
976
		parent::http_PROPFIND('REPORT');
977
	}
978
979
	/**
980
	 * GET method handler
981
	 *
982
	 * @param  array $options parameter passing array
983
	 * @return bool   true on success
984
	 */
985
	function GET(&$options)
986
	{
987
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
988
989
		$id = $app = $user = null;
990
		if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals')
991
		{
992
			return $this->autoindex($options);
993
		}
994
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

994
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
995
		{
996
			return $handler->get($options,$id,$user);
997
		}
998
		error_log(__METHOD__."(".array2string($options).") 501 Not Implemented");
999
		return '501 Not Implemented';
1000
	}
1001
1002
	/**
1003
	 * Display an automatic index (listing and properties) for a collection
1004
	 *
1005
	 * @param array $options parameter passing array, index "path" contains requested path
1006
	 */
1007
	protected function autoindex($options)
1008
	{
1009
		$propfind_options = array(
1010
			'path'  => $options['path'],
1011
			'depth' => 1,
1012
		);
1013
		$files = array();
1014
		if (($ret = $this->PROPFIND($propfind_options,$files)) !== true)
1015
		{
1016
			return $ret;	// no collection
1017
		}
1018
		header('Content-type: text/html; charset='.Translation::charset());
1019
		echo "<html>\n<head>\n\t<title>".'EGroupware (Cal|Card|Group)DAV server '.htmlspecialchars($options['path'])."</title>\n";
1020
		echo "\t<meta http-equiv='content-type' content='text/html; charset=utf-8' />\n";
1021
		echo "\t<style type='text/css'>\n.th { background-color: #e0e0e0; }\n.row_on { background-color: #F1F1F1; vertical-align: top; }\n".
1022
			".row_off { background-color: #ffffff; vertical-align: top; }\ntd { padding-left: 5px; }\nth { padding-left: 5px; text-align: left; }\n\t</style>\n";
1023
		echo "</head>\n<body>\n";
1024
1025
		echo '<h1>(Cal|Card|Group)DAV ';
1026
		$path = '/groupdav.php';
1027
		foreach(explode('/',$this->_unslashify($options['path'])) as $n => $name)
1028
		{
1029
			$path .= ($n != 1 ? '/' : '').$name;
1030
			echo Html::a_href(htmlspecialchars($name.'/'),$path);
1031
		}
1032
		echo "</h1>\n";
1033
1034
		static $props2show = array(
1035
			'DAV:displayname'      => 'Displayname',
1036
			'DAV:getlastmodified'  => 'Last modified',
1037
			'DAV:getetag'          => 'ETag',
1038
			//'CalDAV:schedule-tag'  => 'Schedule-Tag',
1039
			'DAV:getcontenttype'   => 'Content type',
1040
			'DAV:resourcetype'     => 'Resource type',
1041
			//'http://calendarserver.org/ns/:created-by' => 'Created by',
1042
			//'http://calendarserver.org/ns/:updated-by' => 'Updated by',
1043
			//'DAV:owner'            => 'Owner',
1044
			//'DAV:current-user-privilege-set' => 'current-user-privilege-set',
1045
			//'DAV:getcontentlength' => 'Size',
1046
			//'DAV:sync-token' => 'sync-token',
1047
		);
1048
		$n = 0;
1049
		$collection_props = null;
1050
		foreach($files['files'] as $file)
1051
		{
1052
			if (!isset($collection_props))
1053
			{
1054
				$collection_props = $this->props2array($file['props']);
1055
				echo '<h3>'.lang('Collection listing').': '.htmlspecialchars($collection_props['DAV:displayname'])."</h3>\n";
1056
				continue;	// own entry --> displaying properies later
1057
			}
1058
			if(!$n++)
1059
			{
1060
				echo "<table>\n\t<tr class='th'>\n\t\t<th>#</th>\n\t\t<th>".lang('Name')."</th>";
1061
				foreach($props2show as $label)
1062
				{
1063
					echo "\t\t<th>".lang($label)."</th>\n";
1064
				}
1065
				echo "\t</tr>\n";
1066
			}
1067
			$props = $this->props2array($file['props']);
1068
			//echo $file['path']; _debug_array($props);
1069
			$class = $class == 'row_on' ? 'row_off' : 'row_on';
1070
1071
			if (substr($file['path'],-1) == '/')
1072
			{
1073
				$name = basename(substr($file['path'],0,-1)).'/';
1074
			}
1075
			else
1076
			{
1077
				$name = basename($file['path']);
1078
			}
1079
1080
			echo "\t<tr class='$class'>\n\t\t<td>$n</td>\n\t\t<td>".
1081
				Html::a_href(htmlspecialchars($name),'/groupdav.php'.strtr($file['path'], array(
1082
					'%' => '%25',
1083
					'#' => '%23',
1084
					'?' => '%3F',
1085
				)))."</td>\n";
1086
			foreach($props2show as $prop => $label)
1087
			{
1088
				echo "\t\t<td>".($prop=='DAV:getlastmodified'&&!empty($props[$prop])?date('Y-m-d H:i:s',$props[$prop]):$props[$prop])."</td>\n";
1089
			}
1090
			echo "\t</tr>\n";
1091
		}
1092
		if (!$n)
1093
		{
1094
			echo '<p>'.lang('Collection empty.')."</p>\n";
1095
		}
1096
		else
1097
		{
1098
			echo "</table>\n";
1099
		}
1100
		echo '<h3>'.lang('Properties')."</h3>\n";
1101
		echo "<table>\n\t<tr class='th'><th>".lang('Namespace')."</th><th>".lang('Name')."</th><th>".lang('Value')."</th></tr>\n";
1102
		foreach($collection_props as $name => $value)
1103
		{
1104
			$class = $class == 'row_on' ? 'row_off' : 'row_on';
1105
			$parts = explode(':', $name);
1106
			$name = array_pop($parts);
1107
			$ns = implode(':', $parts);
1108
			echo "\t<tr class='$class'>\n\t\t<td>".htmlspecialchars($ns)."</td><td style='white-space: nowrap'>".htmlspecialchars($name)."</td>\n";
1109
			echo "\t\t<td>".$value."</td>\n\t</tr>\n";
1110
		}
1111
		echo "</table>\n";
1112
		$dav = array(1);
1113
		$allow = false;
1114
		$this->OPTIONS($options['path'], $dav, $allow);
1115
		echo "<p>DAV: ".implode(', ', $dav)."</p>\n";
1116
1117
		echo "</body>\n</html>\n";
1118
1119
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1120
	}
1121
1122
	/**
1123
	 * Format a property value for output
1124
	 *
1125
	 * @param mixed $value
1126
	 * @return string
1127
	 */
1128
	protected function prop_value($value)
1129
	{
1130
		if (is_array($value))
1131
		{
1132
			if (isset($value[0]['ns']))
1133
			{
1134
				$ns_defs = '';
1135
				$ns_hash = array();
1136
				$value = $this->_hierarchical_prop_encode($value, '', $ns_defs, $ns_hash);
1137
			}
1138
			$value = array2string($value);
1139
		}
1140
		if ($value[0] == '<' && function_exists('tidy_repair_string'))
1141
		{
1142
			$value = tidy_repair_string($value, array(
1143
				'indent'          => true,
1144
				'show-body-only'  => true,
1145
				'output-encoding' => 'utf-8',
1146
				'input-encoding'  => 'utf-8',
1147
				'input-xml'       => true,
1148
				'output-xml'      => true,
1149
				'wrap'            => 0,
1150
			));
1151
		}
1152
		if (($href=preg_match('/\<(D:)?href\>[^<]+\<\/(D:)?href\>/i',$value)))
1153
		{
1154
			$value = preg_replace('/\<(D:)?href\>('.preg_quote($this->base_uri.'/','/').')?([^<]+)\<\/(D:)?href\>/i','<\\1href><a href="\\2\\3">\\3</a></\\4href>',$value);
1155
		}
1156
		$ret = $value[0] == '<'  || strpos($value, "\n") !== false ? '<pre>'.htmlspecialchars($value).'</pre>' : htmlspecialchars($value);
1157
1158
		if ($href)
1159
		{
1160
			$ret = str_replace('&lt;/a&gt;', '</a>', preg_replace('/&lt;a href=&quot;(.+)&quot;&gt;/', '<a href="\\1">', $ret));
1161
		}
1162
		return $ret;
1163
	}
1164
1165
	/**
1166
	 * Return numeric indexed array with values for keys 'ns', 'name' and 'val' as array 'ns:name' => 'val'
1167
	 *
1168
	 * @param array $props
1169
	 * @return array
1170
	 */
1171
	protected function props2array(array $props)
1172
	{
1173
		$arr = array();
1174
		foreach($props as $prop)
1175
		{
1176
			$ns_hash = array('DAV:' => 'D');
1177
			switch($prop['ns'])
1178
			{
1179
				case 'DAV:';
1180
					$ns = 'DAV';
1181
					break;
1182
				case self::CALDAV:
1183
					$ns = $ns_hash[$prop['ns']] = 'CalDAV';
1184
					break;
1185
				case self::CARDDAV:
1186
					$ns = $ns_hash[$prop['ns']] = 'CardDAV';
1187
					break;
1188
				case self::GROUPDAV:
1189
					$ns = $ns_hash[$prop['ns']] = 'GroupDAV';
1190
					break;
1191
				default:
1192
					$ns = $prop['ns'];
1193
			}
1194
			if (is_array($prop['val']))
1195
			{
1196
				$prop['val'] = $this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs='', $ns_hash);
1197
				// hack to show real namespaces instead of not (visibly) defined shortcuts
1198
				unset($ns_hash['DAV:']);
1199
				$value = strtr($v=$this->prop_value($prop['val']),array_flip($ns_hash));
1200
			}
1201
			else
1202
			{
1203
				$value = $this->prop_value($prop['val']);
1204
			}
1205
			$arr[$ns.':'.$prop['name']] = $value;
1206
		}
1207
		return $arr;
1208
	}
1209
1210
	/**
1211
	 * POST method handler
1212
	 *
1213
	 * @param  array  parameter passing array
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\parameter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1214
	 * @return bool   true on success
1215
	 */
1216
	function POST(&$options)
1217
	{
1218
		// for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member")
1219
		// POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys
1220
		if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork')
1221
		{
1222
			$_GET['add-member'] = '';	// otherwise we give no Location header
1223
			return $this->PUT($options);
1224
		}
1225
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
1226
1227
		$id = $app = $user = null;
1228
		$this->_parse_path($options['path'],$id,$app,$user);
1229
1230
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1230
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
1231
		{
1232
			// managed attachments
1233
			if (isset($_GET['action']) && substr($_GET['action'], 0, 11) === 'attachment-')
1234
			{
1235
				return $this->managed_attachements($options, $id, $handler, $_GET['action']);
1236
			}
1237
1238
			if (method_exists($handler, 'post'))
1239
			{
1240
				// read the content in a string, if a stream is given
1241
				if (isset($options['stream']))
1242
				{
1243
					$options['content'] = '';
1244
					while(!feof($options['stream']))
1245
					{
1246
						$options['content'] .= fread($options['stream'],8192);
1247
					}
1248
				}
1249
				return $handler->post($options,$id,$user);
1250
			}
1251
		}
1252
		return '501 Not Implemented';
1253
	}
1254
1255
	/**
1256
	 * HTTP header containing managed id
1257
	 */
1258
	const MANAGED_ID_HEADER = 'Cal-Managed-ID';
1259
1260
	/**
1261
	 * Add, update or remove attachments
1262
	 *
1263
	 * @param array &$options
1264
	 * @param string|int $id
1265
	 * @param Handler $handler
1266
	 * @param string $action 'attachment-add', 'attachment-update', 'attachment-remove'
1267
	 * @return string http status
1268
	 *
1269
	 * @todo support for rid parameter
1270
	 * @todo managed-id does NOT change on update
1271
	 * @todo updates of attachments through vfs need to call $handler->update_tags($id) too
1272
	 */
1273
	protected function managed_attachements(&$options, $id, Handler $handler, $action)
1274
	{
1275
		error_log(__METHOD__."(path=$options[path], id=$id, ..., action=$action) _GET=".array2string($_GET));
1276
		$entry = $handler->_common_get_put_delete('GET', $options, $id);
1277
1278
		if (!is_array($entry))
0 ignored issues
show
introduced by
The condition is_array($entry) is always false.
Loading history...
1279
		{
1280
			return $entry ? $entry : "404 Not found";
1281
		}
1282
1283
		if (!Link::file_access($handler->app, $entry['id'], Acl::EDIT))
1284
		{
1285
			return '403 Forbidden';
1286
		}
1287
1288
		switch($action)
1289
		{
1290
			case 'attachment-add':
1291
				$matches = null;
1292
				if (isset($this->_SERVER['HTTP_CONTENT_DISPOSITION']) &&
1293
					substr($this->_SERVER['HTTP_CONTENT_DISPOSITION'], 0, 10) === 'attachment' &&
1294
					preg_match('/filename="?([^";]+)/', $this->_SERVER['HTTP_CONTENT_DISPOSITION'], $matches))
1295
				{
1296
					$filename = Vfs::basename($matches[1]);
1297
				}
1298
				$path = null;
1299
				if (!($to = self::fopen_attachment($handler->app, $handler->get_id($entry), $filename, $this->_SERVER['CONTENT_TYPE'], $path)) ||
1300
					isset($options['stream']) && ($copied=stream_copy_to_stream($options['stream'], $to)) === false ||
1301
					isset($options['content']) && ($copied=fwrite($to, $options['content'])) === false)
1302
				{
1303
					return '403 Forbidden';
1304
				}
1305
				fclose($to);
1306
				error_log(__METHOD__."() content-type=$options[content_type], filename=$filename: $path created $copied bytes copied");
1307
				$ret = '201 Created';
1308
				header(self::MANAGED_ID_HEADER.': '.self::path2managed_id($path));
1309
				header('Location: '.self::path2location($path));
1310
				break;
1311
1312
			case 'attachment-remove':
1313
			case 'attachment-update':
1314
				if (empty($_GET['managed-id']) || !($path = self::managed_id2path($_GET['managed-id'], $handler->app, $entry['id'])))
1315
				{
1316
					self::xml_error(self::mkprop(self::CALDAV, 'valid-managed-id-parameter', ''));
1317
					return '403 Forbidden';
1318
				}
1319
				if ($action == 'attachment-remove')
1320
				{
1321
					if (!Vfs::unlink($path))
1322
					{
1323
						self::xml_error(self::mkprop(self::CALDAV, 'valid-managed-id-parameter', ''));
1324
						return '403 Forbidden';
1325
					}
1326
					$ret = '204 No content';
1327
				}
1328
				else
1329
				{
1330
					// check for rename of attachment via Content-Disposition:filename=
1331
					if (isset($this->_SERVER['HTTP_CONTENT_DISPOSITION']) &&
1332
						substr($this->_SERVER['HTTP_CONTENT_DISPOSITION'], 0, 10) === 'attachment' &&
1333
						preg_match('/filename="?([^";]+)/', $this->_SERVER['HTTP_CONTENT_DISPOSITION'], $matches) &&
1334
						($filename = Vfs::basename($matches[1])) != Vfs::basename($path))
1335
					{
1336
						$old_path = $path;
1337
						if (!($dir = Vfs::dirname($path)) || !Vfs::rename($old_path, $path = Vfs::concat($dir, $filename)))
1338
						{
1339
							self::xml_error(self::mkprop(self::CALDAV, 'valid-managed-id-parameter', ''));
1340
							return '403 Forbidden';
1341
						}
1342
					}
1343
					if (!($to = Vfs::fopen($path, 'w')) ||
1344
						isset($options['stream']) && ($copied=stream_copy_to_stream($options['stream'], $to)) === false ||
1345
						isset($options['content']) && ($copied=fwrite($to, $options['content'])) === false)
1346
					{
1347
						self::xml_error(self::mkprop(self::CALDAV, 'valid-managed-id-parameter', ''));
1348
						return '403 Forbidden';
1349
					}
1350
					fclose($to);
1351
					error_log(__METHOD__."() content-type=$options[content_type], filename=$filename: $path updated $copied bytes copied");
1352
					$ret = '200 Ok';
1353
					header(self::MANAGED_ID_HEADER.': '.self::path2managed_id($path));
1354
					header('Location: '.self::path2location($path));
1355
				}
1356
				break;
1357
1358
			default:
1359
				return '501 Unknown action parameter '.$action;
1360
		}
1361
		// update etag/ctag/sync-token by updating modification time
1362
		$handler->update_tags($entry);
1363
1364
		// check/handle Prefer: return-representation
1365
		// we can NOT use 204 No content (forbidds a body) with return=representation, therefore we need to use 200 Ok instead!
1366
		if ($handler->check_return_representation($options, $id) && (int)$ret == 204)
1367
		{
1368
			$ret = '200 Ok';
1369
		}
1370
1371
		return $ret;
1372
	}
1373
1374
	/**
1375
	 * Handle ATTACH attribute on importing iCals
1376
	 *
1377
	 * - turn inline attachments into managed attachments
1378
	 * - delete NOT included attachments, $delete_via_put is true
1379
	 * @todo: store URLs not from our managed attachments
1380
	 *
1381
	 * @param string $app eg. 'calendar'
1382
	 * @param int|string $id
1383
	 * @param array $attach array of array with values for keys 'name', 'params', 'value'
1384
	 * @param boolean $delete_via_put
1385
	 * @return boolean false on error, eg. invalid managed id, for false an xml-error body has been send
1386
	 */
1387
	public static function handle_attach($app, $id, $attach, $delete_via_put=false)
1388
	{
1389
		//error_log(__METHOD__."('$app', $id, attach=".array2string($attach).", delete_via_put=".array2string($delete_via_put).')');
1390
1391
		if (!Link::file_access($app, $id, Acl::EDIT))
1392
		{
1393
			error_log(__METHOD__."('$app', $id, ...) no rights to update attachments");
1394
			return;	// no rights --> nothing to do
1395
		}
1396
		if (!is_array($attach)) $attach = array();	// could be PEAR_Error if not set
0 ignored issues
show
introduced by
The condition is_array($attach) is always true.
Loading history...
1397
1398
		if ($delete_via_put)
1399
		{
1400
			foreach(Vfs::find(Link::vfs_path($app, $id, '', true), array('type' => 'F')) as $path)
1401
			{
1402
				$found = false;
1403
				foreach($attach as $key => $attr)
1404
				{
1405
					if ($attr['params']['MANAGED-ID'] === self::path2managed_id($path))
1406
					{
1407
						$found = true;
1408
						unset($attach[$key]);
1409
						break;
1410
					}
1411
				}
1412
				if (!$found)
1413
				{
1414
					$ok = Vfs::unlink($path);
1415
					error_log(__METHOD__."('$app', $id, ...) Vfs::unlink('$path') returned ".array2string($ok));
1416
				}
1417
			}
1418
		}
1419
		// turn inline attachments into managed ones
1420
		foreach($attach as $key => $attr)
1421
		{
1422
			if (!empty($attr['params']['FMTTYPE']))
1423
			{
1424
				if (isset($attr['params']['MANAGED-ID']))
1425
				{
1426
					// invalid managed-id
1427
					if (!($path = self::managed_id2path($attr['params']['MANAGED-ID'])) || !Vfs::is_readable($path))
1428
					{
1429
						error_log(__METHOD__."('$app', $id, ...) invalid MANAGED-ID ".array2string($attr));
1430
						self::xml_error(self::mkprop(self::CALDAV, 'valid-managed-id', ''));
1431
						return false;
1432
					}
1433
					if($path == ($link = Link::vfs_path($app, $id, Vfs::basename($path))))
1434
					{
1435
						error_log(__METHOD__."('$app', $id, ...) trying to modify existing MANAGED-ID --> ignored! ".array2string($attr));
1436
						continue;
1437
					}
1438
					// reuse valid managed-id --> symlink attachment
1439
					if (Vfs::file_exists($link))
1440
					{
1441
						if (Vfs::readlink($link) === $path) continue;	// no need to recreate identical link
1442
						Vfs::unlink($link);		// symlink will fail, if $link exists
1443
					}
1444
					if (!Vfs::symlink($path, $link))
1445
					{
1446
						error_log(__METHOD__."('$app', $id, ...) failed to symlink($path, $link) --> ignored!");
1447
					}
1448
					continue;
1449
				}
1450
				if (!($to = self::fopen_attachment($app, $id, $filename=$attr['params']['FILENAME'], $attr['params']['FMTTYPE'], $path)) ||
1451
					// Horde Icalendar does NOT decode automatic
1452
					(/*$copied=*/fwrite($to, $attr['params']['ENCODING'] == 'BASE64' ? base64_decode($attr['value']) : $attr['value'])) === false)
1453
				{
1454
					error_log(__METHOD__."('$app', $id, ...) failed to add attachment ".array2string($attr).") ");
1455
					continue;
1456
				}
1457
				fclose($to);
1458
				//error_log(__METHOD__."('$app', $id, ...)) content-type={$attr['params']['FMTTYPE']}, filename=$filename: $path created $copied bytes copied");
1459
			}
1460
			else
1461
			{
1462
				//error_log(__METHOD__."('$app', $id, ...) unsupported URI attachment ".array2string($attr));
1463
			}
1464
		}
1465
	}
1466
1467
	/**
1468
	 * Open attachment for writing
1469
	 *
1470
	 * @param string $app
1471
	 * @param int|string $id
1472
	 * @param string $_filename defaults to 'attachment'
1473
	 * @param string $mime =null mime-type to generate extension
1474
	 * @param string &$path =null on return path opened
1475
	 * @return resource
1476
	 */
1477
	protected static function fopen_attachment($app, $id, $_filename, $mime=null, &$path=null)
1478
	{
1479
		$filename = empty($_filename) ? 'attachment' : Vfs::basename($_filename);
1480
1481
		if (strpos($mime, ';')) list($mime) = explode(';', $mime);	// in case it contains eg. charset info
1482
1483
		$ext = !empty($mime) ? MimeMagic::mime2ext($mime) : '';
1484
1485
		$matches = null;
1486
		if (!$ext || substr($filename, -strlen($ext)-1) == '.'.$ext ||
1487
			preg_match('/\.([^.]+)$/', $filename, $matches) && MimeMagic::ext2mime($matches[1]) == $mime)
1488
		{
1489
			$parts = explode('.', $filename);
1490
			$ext = '.'.array_pop($parts);
1491
			$filename = implode('.', $parts);
1492
		}
1493
		else
1494
		{
1495
			$ext = '.'.$ext;
1496
		}
1497
		for($i = 1; $i < 100; ++$i)
1498
		{
1499
			$path = Link::vfs_path($app, $id, $filename.($i > 1 ? '-'.$i : '').$ext, true);
1500
			if (!Vfs::stat($path)) break;
1501
		}
1502
		if ($i >= 100) return null;
1503
1504
		if (!($dir = Vfs::dirname($path)) || !Vfs::file_exists($dir) && !Vfs::mkdir($dir, 0777, STREAM_MKDIR_RECURSIVE))
1505
		{
1506
			error_log(__METHOD__."('$app', $id, ...) failed to create entry dir $dir!");
1507
			return false;
1508
		}
1509
1510
		return Vfs::fopen($path, 'w');
1511
	}
1512
1513
	/**
1514
	 * Get attachment location from path
1515
	 *
1516
	 * @param string $path
1517
	 * @return string
1518
	 */
1519
	protected static function path2location($path)
1520
	{
1521
		return Framework::getUrl(Framework::link(Vfs::download_url($path)));
1522
	}
1523
1524
	/**
1525
	 * Add ATTACH attribute(s) for iCal
1526
	 *
1527
	 * @param string $app eg. 'calendar'
1528
	 * @param int|string $id
1529
	 * @param array &$attributes
1530
	 * @param array &$parameters
1531
	 */
1532
	public static function add_attach($app, $id, array &$attributes, array &$parameters)
1533
	{
1534
		foreach(Vfs::find(Link::vfs_path($app, $id, '', true), array(
1535
			'type' => 'F',
1536
			'need_mime' => true,
1537
			'maxdepth' => 10,	// set a limit to not run into an infinit recursion
1538
		), true) as $path => $stat)
1539
		{
1540
			// handle symlinks --> return target size and mime-type
1541
			if (($target = Vfs::readlink($path)))
1542
			{
1543
				if (!($stat = Vfs::stat($target))) continue;	// broken or inaccessible symlink
1544
1545
				// check if target is in /apps, probably reused MANAGED-ID --> return it
1546
				if (substr($target, 0, 6) == '/apps/')
1547
				{
1548
					$path = $target;
1549
				}
1550
			}
1551
			$attributes['ATTACH'][] = self::path2location($path);
1552
			$parameters['ATTACH'][] = array(
1553
				'MANAGED-ID' => self::path2managed_id($path),
1554
				'FMTTYPE'    => $stat['mime'],
1555
				'SIZE'       => (string)$stat['size'],	// Horde_Icalendar renders int as empty string
1556
				'FILENAME'   => Vfs::basename($path),
1557
			);
1558
			// if we have attachments, set X-attribute to enable deleting them by put
1559
			// (works around events synced before without ATTACH attributes)
1560
			$attributes['X-EGROUPWARE-ATTACH-INCLUDED'] = 'TRUE';
1561
		}
1562
	}
1563
1564
	/**
1565
	 * Return managed-id of a vfs-path
1566
	 *
1567
	 * @param string $path "/apps/$app/$id/something"
1568
	 * @return string
1569
	 */
1570
	static public function path2managed_id($path)
1571
	{
1572
		return base64_encode($path);
1573
	}
1574
1575
	/**
1576
	 * Return vfs-path of a managed-id
1577
	 *
1578
	 * @param string $managed_id
1579
	 * @param string $app =null app-name to check against path
1580
	 * @param string|int $id =null id to check agains path
1581
	 * @return string|boolean "/apps/$app/$id/something" or false if not found or not belonging to given $app/$id
1582
	 */
1583
	static public function managed_id2path($managed_id, $app=null, $id=null)
1584
	{
1585
		$path = base64_decode($managed_id);
1586
1587
		if (!$path || substr($path, 0, 6) != '/apps/' || !Vfs::stat($path))
1588
		{
1589
			$path = false;
1590
		}
1591
		elseif (!empty($app) && !empty($id))
1592
		{
1593
			list(,,$a,$i) = explode('/', $path);
1594
			if ($a !== $app || $i !== (string)$id)
1595
			{
1596
				$path = false;
1597
			}
1598
		}
1599
		error_log(__METHOD__."('$managed_id', $app, $id) base64_decode('$managed_id')=".array2string(base64_decode($managed_id)).' returning '.array2string($path));
1600
		return $path;
1601
	}
1602
1603
	/**
1604
	 * Namespaces which need to be eplicitly named in self::$proppatch_props,
1605
	 * because we consider them protected, if not explicitly named
1606
	 *
1607
	 * @var array
1608
	 */
1609
	static $ns_needs_explicit_named_props = array(self::DAV, self::CALDAV, self::CARDDAV, self::CALENDARSERVER);
1610
	/**
1611
	 * props modifyable via proppatch from client for name-spaces mentioned in self::$ns_needs_explicit_named_props
1612
	 *
1613
	 * Props named here are stored in prefs without namespace!
1614
	 *
1615
	 * @var array name => namespace pairs
1616
	 */
1617
	static $proppatch_props = array(
1618
		'displayname' => self::DAV,
1619
		'calendar-description' => self::CALDAV,
1620
		'addressbook-description' => self::CARDDAV,
1621
		'calendar-color' => self::ICAL,	// only mentioned that old prefs still work
1622
		'calendar-order' => self::ICAL,
1623
		'default-alarm-vevent-date' => self::CALDAV,
1624
		'default-alarm-vevent-datetime' => self::CALDAV,
1625
	);
1626
1627
	/**
1628
	 * PROPPATCH method handler
1629
	 *
1630
	 * @param array &$options general parameter passing array
1631
	 * @return string with responsedescription or null, individual status in $options['props'][]['status']
1632
	 */
1633
	function PROPPATCH(&$options)
1634
	{
1635
		if ($this->debug) error_log(__METHOD__."(".array2string($options).')');
1636
1637
		// parse path in form [/account_lid]/app[/more]
1638
		$id = $app = $user = $user_prefix = null;
1639
		self::_parse_path($options['path'],$id,$app,$user,$user_prefix);	// allways returns false if eg. !$id
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1639
		self::/** @scrutinizer ignore-call */ 
1640
        _parse_path($options['path'],$id,$app,$user,$user_prefix);	// allways returns false if eg. !$id
Loading history...
1640
		if ($app == 'principals' || $id || $options['path'] == '/')
1641
		{
1642
			if ($this->debug > 1) error_log(__METHOD__.": user='$user', app='$app', id='$id': 404 not found!");
1643
			foreach($options['props'] as &$prop)
1644
			{
1645
				$prop['status'] = '403 Forbidden';
1646
			}
1647
			return 'NOT allowed to PROPPATCH that resource!';
1648
		}
1649
		// store selected props in preferences, eg. calendar-color, see self::$proppatch_props
1650
		$need_save = array();
1651
		foreach($options['props'] as &$prop)
1652
		{
1653
			if ((isset(self::$proppatch_props[$prop['name']]) && self::$proppatch_props[$prop['name']] === $prop['xmlns'] ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && self::prop...s_explicit_named_props), Probably Intended Meaning: IssetNode && (self::prop..._explicit_named_props))
Loading history...
1654
				!in_array($prop['xmlns'],self::$ns_needs_explicit_named_props)))
1655
			{
1656
				if (!$app)
1657
				{
1658
					$app = 'groupdav';
1659
					$name = $prop['name'].':'.$options['path'].':'.$prop['ns'];
1660
				}
1661
				else
1662
				{
1663
					$name = $prop['name'].':'.$user.(isset(self::$proppatch_props[$prop['name']]) &&
1664
						self::$proppatch_props[$prop['name']] == $prop['ns'] ? '' : ':'.$prop['ns']);
1665
				}
1666
				//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']));
1667
				if ($GLOBALS['egw_info']['user']['preferences'][$app][$name] !== $prop['val'])	// nothing to change otherwise
1668
				{
1669
					if (isset($prop['val']))
1670
					{
1671
						$GLOBALS['egw']->preferences->add($app, $name, $prop['val']);
1672
					}
1673
					else
1674
					{
1675
						$GLOBALS['egw']->preferences->delete($app, $name);
1676
					}
1677
					$need_save[] = $name;
1678
				}
1679
				$prop['status'] = '200 OK';
1680
			}
1681
			else
1682
			{
1683
				$prop['status'] = '409 Conflict';	// could also be "403 Forbidden"
1684
			}
1685
		}
1686
		if ($need_save)
1687
		{
1688
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->save_repository();
1689
			// call calendar-hook, if default-alarms are changed, to sync them to calendar prefs
1690
			if (class_exists('calendar_hooks'))
1691
			{
1692
				foreach($need_save as $name)
1693
				{
1694
					list($name) = explode(':', $name);
1695
					if (in_array($name, array('default-alarm-vevent-date', 'default-alarm-vevent-datetime')))
1696
					{
1697
						calendar_hooks::sync_default_alarms();
1698
						break;
1699
					}
1700
				}
1701
			}
1702
		}
1703
	}
1704
1705
	/**
1706
	 * PUT method handler
1707
	 *
1708
	 * @param  array  parameter passing array
1709
	 * @return bool   true on success
1710
	 */
1711
	function PUT(&$options)
1712
	{
1713
		// read the content in a string, if a stream is given
1714
		if (isset($options['stream']))
1715
		{
1716
			$options['content'] = '';
1717
			while(!feof($options['stream']))
1718
			{
1719
				$options['content'] .= fread($options['stream'],8192);
1720
			}
1721
		}
1722
1723
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
1724
1725
		$id = $app = $user = $prefix = null;
1726
		if (!$this->_parse_path($options['path'],$id,$app,$user,$prefix))
1727
		{
1728
			return '404 Not Found';
1729
		}
1730
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1730
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
1731
		{
1732
			$status = $handler->put($options,$id,$user,$prefix);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\CalDAV\Handler::put() has too many arguments starting with $prefix. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1732
			/** @scrutinizer ignore-call */ 
1733
   $status = $handler->put($options,$id,$user,$prefix);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1733
1734
			// set default stati: true --> 204 No Content, false --> should be already handled
1735
			if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong';
1736
1737
			// check/handle Prefer: return-representation
1738
			if ($status[0] === '2' || $status === true)
1739
			{
1740
				// we can NOT use 204 No content (forbidds a body) with return=representation, therefore we need to use 200 Ok instead!
1741
				if ($handler->check_return_representation($options, $id, $user) && (int)$status == 204)
1742
				{
1743
					$status = '200 Ok';
1744
				}
1745
			}
1746
			return $status;
1747
		}
1748
		return '501 Not Implemented';
1749
	}
1750
1751
	/**
1752
	 * DELETE method handler
1753
	 *
1754
	 * @param  array  general parameter passing array
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\general was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1755
	 * @return bool   true on success
1756
	 */
1757
	function DELETE($options)
1758
	{
1759
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
1760
1761
		$id = $app = $user = null;
1762
		if (!$this->_parse_path($options['path'],$id,$app,$user))
1763
		{
1764
			return '404 Not Found';
1765
		}
1766
		if (($handler = self::app_handler($app)))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1766
		if (($handler = self::/** @scrutinizer ignore-call */ app_handler($app)))
Loading history...
1767
		{
1768
			$status = $handler->delete($options,$id,$user);
1769
			// set default stati: true --> 204 No Content, false --> should be already handled
1770
			if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong';
1771
			return $status;
1772
		}
1773
		return '501 Not Implemented';
1774
	}
1775
1776
	/**
1777
	 * MKCOL method handler
1778
	 *
1779
	 * @param  array  general parameter passing array
1780
	 * @return bool   true on success
1781
	 */
1782
	function MKCOL($options)
1783
	{
1784
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
1785
1786
		return '501 Not Implemented';
1787
	}
1788
1789
	/**
1790
	 * MOVE method handler
1791
	 *
1792
	 * @param  array  general parameter passing array
1793
	 * @return bool   true on success
1794
	 */
1795
	function MOVE($options)
1796
	{
1797
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
1798
1799
		return '501 Not Implemented';
1800
	}
1801
1802
	/**
1803
	 * COPY method handler
1804
	 *
1805
	 * @param  array  general parameter passing array
1806
	 * @return bool   true on success
1807
	 */
1808
	function COPY($options, $del=false)
1809
	{
1810
		if ($this->debug) error_log('self::'.($del ? 'MOVE' : 'COPY').'('.array2string($options).')');
1811
1812
		return '501 Not Implemented';
1813
	}
1814
1815
	/**
1816
	 * LOCK method handler
1817
	 *
1818
	 * @param  array  general parameter passing array
1819
	 * @return bool   true on success
1820
	 */
1821
	function LOCK(&$options)
1822
	{
1823
		$id = $app = $user = null;
1824
		self::_parse_path($options['path'],$id,$app,$user);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1824
		self::/** @scrutinizer ignore-call */ 
1825
        _parse_path($options['path'],$id,$app,$user);
Loading history...
1825
		$path = Vfs::app_entry_lock_path($app,$id);
1826
1827
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$path");
1828
1829
		// get the app handler, to check if the user has edit access to the entry (required to make locks)
1830
		$handler = self::app_handler($app);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::app_handler() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1830
		/** @scrutinizer ignore-call */ 
1831
  $handler = self::app_handler($app);
Loading history...
1831
1832
		// TODO recursive locks on directories not supported yet
1833
		if (!$id || !empty($options['depth']) || !$handler->check_access(Acl::EDIT,$id))
1834
		{
1835
			return '409 Conflict';
1836
		}
1837
		$options['timeout'] = time()+300; // 5min. hardcoded
1838
1839
		// dont know why, but HTTP_WebDAV_Server passes the owner in D:href tags, which get's passed unchanged to checkLock/PROPFIND
1840
		// that's wrong according to the standard and cadaver does not show it on discover --> strip_tags removes eventual tags
1841
		if (($ret = Vfs::lock($path,$options['locktoken'],$options['timeout'],strip_tags($options['owner']),
0 ignored issues
show
Bug introduced by
strip_tags($options['owner']) cannot be passed to EGroupware\Api\Vfs::lock() as the parameter $owner expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1841
		if (($ret = Vfs::lock($path,$options['locktoken'],$options['timeout'],/** @scrutinizer ignore-type */ strip_tags($options['owner']),
Loading history...
1842
			$options['scope'],$options['type'],isset($options['update']),false)) && !isset($options['update']))		// false = no ACL check
1843
		{
1844
			return $ret ? '200 OK' : '409 Conflict';
0 ignored issues
show
introduced by
The condition $ret is always true.
Loading history...
1845
		}
1846
		return $ret;
1847
	}
1848
1849
	/**
1850
	 * UNLOCK method handler
1851
	 *
1852
	 * @param  array  general parameter passing array
1853
	 * @return bool   true on success
1854
	 */
1855
	function UNLOCK(&$options)
1856
	{
1857
		$id = $app = $user = null;
1858
		self::_parse_path($options['path'],$id,$app,$user);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1858
		self::/** @scrutinizer ignore-call */ 
1859
        _parse_path($options['path'],$id,$app,$user);
Loading history...
1859
		$path = Vfs::app_entry_lock_path($app,$id);
1860
1861
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$path");
1862
		return Vfs::unlock($path,$options['token']) ? '204 No Content' : '409 Conflict';
1863
	}
1864
1865
	/**
1866
	 * checkLock() helper
1867
	 *
1868
	 * @param  string resource path to check for locks
1869
	 * @return bool   true on success
1870
	 */
1871
	function checkLock($path)
1872
	{
1873
		$id = $app = $user = null;
1874
		self::_parse_path($path,$id,$app,$user);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1874
		self::/** @scrutinizer ignore-call */ 
1875
        _parse_path($path,$id,$app,$user);
Loading history...
1875
1876
		return Vfs::checkLock(Vfs::app_entry_lock_path($app, $id));
1877
	}
1878
1879
	/**
1880
	 * ACL method handler
1881
	 *
1882
	 * @param  array  general parameter passing array
1883
	 * @return string HTTP status
1884
	 */
1885
	function ACL(&$options)
1886
	{
1887
		$id = $app = $user = null;
1888
		self::_parse_path($options['path'],$id,$app,$user);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::_parse_path() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1888
		self::/** @scrutinizer ignore-call */ 
1889
        _parse_path($options['path'],$id,$app,$user);
Loading history...
1889
1890
		if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$options[path]");
1891
1892
		$options['errors'] = array();
1893
		switch ($app)
1894
		{
1895
			case 'calendar':
1896
			case 'addressbook':
1897
			case 'infolog':
1898
				$status = '200 OK'; // grant all
1899
				break;
1900
			default:
1901
				$options['errors'][] = 'no-inherited-ace-conflict';
1902
				$status = '403 Forbidden';
1903
		}
1904
1905
		return $status;
1906
	}
1907
1908
	/**
1909
	 * Parse a path into it's id, app and user parts
1910
	 *
1911
	 * @param string $path
1912
	 * @param int &$id
1913
	 * @param string &$app addressbook, calendar, infolog (=infolog)
1914
	 * @param int &$user
1915
	 * @param string &$user_prefix =null
1916
	 * @return boolean true on success, false on error
1917
	 */
1918
	function _parse_path($path,&$id,&$app,&$user,&$user_prefix=null)
1919
	{
1920
		if ($this->debug)
1921
		{
1922
			error_log(__METHOD__." called with ('$path') id=$id, app='$app', user=$user");
1923
		}
1924
		if ($path[0] == '/')
1925
		{
1926
			$path = substr($path, 1);
1927
		}
1928
		$parts = explode('/', $this->_unslashify($path));
1929
1930
		// /(resources|locations)/<resource-id>-<resource-name>/calendar
1931
		if ($parts[0] == 'resources' || $parts[0] == 'locations')
1932
		{
1933
			if (!empty($parts[1]))
1934
			{
1935
				$user = $parts[0].'/'.$parts[1];
1936
				array_shift($parts);
1937
				$res_id = (int)array_shift($parts);
1938
				if (!Principals::read_resource($res_id))
1939
				{
1940
					return false;
1941
				}
1942
				$account_id = 'r'.$res_id;
1943
				$app = 'calendar';
1944
			}
1945
		}
1946
		elseif (($account_id = $this->accounts->name2id($parts[0], 'account_lid')) ||
1947
			($account_id = $this->accounts->name2id($parts[0]=urldecode($parts[0]))))
1948
		{
1949
			// /$user/$app/...
1950
			$user = array_shift($parts);
1951
		}
1952
1953
		if (!isset($app)) $app = array_shift($parts);
1954
1955
		// /addressbook-accounts/
1956
		if (!$account_id && $app == 'addressbook-accounts')
1957
		{
1958
			$app = 'addressbook';
1959
			$user = 0;
1960
			$user_prefix = '/';
1961
		}
1962
		// shared calendars/addressbooks at /<currentuser>/(calendar|addressbook|infolog|resource|location)-<username>
1963
		elseif ($account_id == $GLOBALS['egw_info']['user']['account_id'] && strpos($app, '-') !== false)
1964
		{
1965
			$user_prefix = '/'.$GLOBALS['egw_info']['user']['account_lid'].'/'.$app;
1966
			list($app, $username) = explode('-', $app, 2);
1967
			if ($username == 'accounts' && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1')
1968
			{
1969
				$account_id = 0;
1970
			}
1971
			elseif($app == 'resource' || $app == 'location')
1972
			{
1973
				if (!Principals::read_resource($res_id = (int)$username))
1974
				{
1975
					return false;
1976
				}
1977
				$account_id = 'r'.$res_id;
1978
				$app = 'calendar';
1979
			}
1980
			elseif (!($account_id = $this->accounts->name2id($username, 'account_lid')) &&
1981
					!($account_id = $this->accounts->name2id($username=urldecode($username))))
1982
			{
1983
				return false;
1984
			}
1985
			$user = $account_id;
1986
		}
1987
		elseif ($user)
1988
		{
1989
			$user_prefix = '/'.$user;
1990
			$user = $account_id;
1991
			// /<currentuser>/inbox/
1992
			if ($user == $GLOBALS['egw_info']['user']['account_id'] && $app == 'inbox')
1993
			{
1994
				$app = 'calendar';
1995
			}
1996
		}
1997
		else
1998
		{
1999
			$user_prefix = '';
2000
			$user = $GLOBALS['egw_info']['user']['account_id'];
2001
		}
2002
2003
		// Api\WebDAV\Server encodes %, # and ? again, which leads to storing eg. '%' as '%25'
2004
		$id = strtr(array_pop($parts), array(
2005
			'%25' => '%',
2006
			'%23' => '#',
2007
			'%3F' => '?',
2008
		));
2009
2010
		$ok = ($id || isset($_GET['add-member']) && $_SERVER['REQUEST_METHOD'] == 'POST') &&
2011
			($user || $user === 0) && in_array($app,array('addressbook','calendar','infolog','principals'));
2012
2013
		if ($this->debug)
2014
		{
2015
			error_log(__METHOD__."('$path') returning " . ($ok ? 'true' : 'false') . ": id='$id', app='$app', user='$user', user_prefix='$user_prefix'");
2016
		}
2017
		return $ok;
2018
	}
2019
2020
	protected static $request_starttime;
2021
	/**
2022
	 * Log level from user prefs: $GLOBALS['egw_info']['user']['preferences']['groupdav']['debug_level'])
2023
	 * - 'f' files directory
2024
	 * - 'r' to error-log, but only shortend requests
2025
	 *
2026
	 * @var string
2027
	 */
2028
	protected static $log_level;
2029
2030
	/**
2031
	 * Serve WebDAV HTTP request
2032
	 *
2033
	 * Reimplemented to add logging
2034
	 *
2035
     * @param  $prefix =null prefix filesystem path with given path, eg. "/webdav" for owncloud 4.5 remote.php
0 ignored issues
show
Documentation Bug introduced by
The doc comment =null at position 0 could not be parsed: Unknown type name '=null' at position 0 in =null.
Loading history...
2036
	 */
2037
	function ServeRequest($prefix=null)
2038
	{
2039
		if ((self::$log_level=$GLOBALS['egw_info']['user']['preferences']['groupdav']['debug_level']) === 'r' ||
2040
			self::$log_level === 'f' || $this->debug)
2041
		{
2042
			self::$request_starttime = microtime(true);
2043
			// do NOT log non-text attachments
2044
			$this->store_request = $_SERVER['REQUEST_METHOD'] != 'POST' || !isset($_GET['action']) ||
2045
				!in_array($_GET['action'], array('attachment-add', 'attachment-update')) ||
2046
				substr($_SERVER['CONTENT_TYPE'], 0, 5) == 'text/';
2047
			ob_start();
2048
		}
2049
		parent::ServeRequest($prefix);
2050
2051
		if (self::$request_starttime) self::log_request();
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\CalDAV::log_request() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2051
		if (self::$request_starttime) self::/** @scrutinizer ignore-call */ log_request();
Loading history...
2052
	}
2053
2054
	/**
2055
	 * Sanitizing filename to gard agains path traversal and / eg. in UserAgent string
2056
	 *
2057
	 * @param string $filename
2058
	 * @return string
2059
	 */
2060
	public static function sanitize_filename($filename)
2061
	{
2062
		return str_replace(array('../', '/'), array('', '!'), $filename);
2063
	}
2064
2065
	/**
2066
	 * Log the request
2067
	 *
2068
	 * @param string $extra ='' extra text to add below request-log, eg. exception thrown
2069
	 */
2070
	protected function log_request($extra='')
2071
	{
2072
		if (self::$request_starttime)
2073
		{
2074
			if (self::$log_level === 'f')
2075
			{
2076
				$msg_file = $GLOBALS['egw_info']['server']['files_dir'];
2077
				$msg_file .= '/groupdav';
2078
				$msg_file .= '/'.self::sanitize_filename($GLOBALS['egw_info']['user']['account_lid']).'/';
2079
				if (!file_exists($msg_file) && !mkdir($msg_file, 0700, true))
2080
				{
2081
					error_log(__METHOD__."() Could NOT create directory '$msg_file'!");
2082
					return;
2083
				}
2084
				// stop CalDAVTester from creating one log per test-step
2085
				if (substr($_SERVER['HTTP_USER_AGENT'], 0, 14) == 'scripts/tests/')
2086
				{
2087
					$msg_file .= 'CalDAVTester.log';
2088
				}
2089
				else
2090
				{
2091
					$msg_file .= self::sanitize_filename($_SERVER['HTTP_USER_AGENT']).'.log';
2092
				}
2093
				$content = '*** '.$_SERVER['REMOTE_ADDR'].' '.date('c')."\n";
2094
			}
2095
			$content .= $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI'].' HTTP/1.1'."\n";
2096
			// reconstruct headers
2097
			foreach($_SERVER as $name => $value)
2098
			{
2099
				list($type,$name) = explode('_',$name,2);
2100
				if ($type == 'HTTP' || $type == 'CONTENT')
2101
				{
2102
					$content .= str_replace(' ','-',ucwords(strtolower(($type=='HTTP'?'':$type.' ').str_replace('_',' ',$name)))).
2103
						': '.($name=='AUTHORIZATION'?'Basic ***************':$value)."\n";
2104
				}
2105
			}
2106
			$content .= "\n";
2107
			if ($this->request)
2108
			{
2109
				$content .= $this->request."\n";
2110
			}
2111
			$content .= 'HTTP/1.1 '.$this->_http_status."\n";
2112
			$content .= 'Date: '.str_replace('+0000', 'GMT', gmdate('r'))."\n";
2113
			$content .= 'Server: '.$_SERVER['SERVER_SOFTWARE']."\n";
2114
			foreach(headers_list() as $line)
2115
			{
2116
				$content .= $line."\n";
2117
			}
2118
			if (($c = ob_get_flush())) $content .= "\n";
2119
			if (self::$log_level !== 'f' && strlen($c) > 1536) $c = substr($c,0,1536)."\n*** LOG TRUNKATED\n";
2120
			$content .= $c;
2121
			if ($extra) $content .= $extra;
2122
			if ($this->to_log) $content .= "\n### ".implode("\n### ", $this->to_log)."\n";
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->to_log of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2123
			$content .= $this->_http_status[0] == '4' && substr($this->_http_status,0,3) != '412' ||
2124
				$this->_http_status[0] == '5' ? '###' : '***';	// mark failed requests with ###, instead of ***
2125
			$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";
2126
2127
			if ($msg_file && ($f = fopen($msg_file,'a')))
2128
			{
2129
				flock($f,LOCK_EX);
2130
				fwrite($f,$content);
2131
				flock($f,LOCK_UN);
2132
				fclose($f);
2133
			}
2134
			else
2135
			{
2136
				foreach(explode("\n",$content) as $line)
2137
				{
2138
					error_log($line);
2139
				}
2140
			}
2141
		}
2142
	}
2143
2144
	/**
2145
	 * Output xml error element
2146
	 *
2147
	 * @param string|array $xml_error string with name for empty element in DAV NS or array with props
2148
	 * @param string $human_readable =null human readable error message
2149
	 */
2150
	public static function xml_error($xml_error, $human_readable=null)
2151
	{
2152
		header('Content-type: application/xml; charset=utf-8');
2153
2154
		$xml = new \XMLWriter;
2155
		$xml->openMemory();
2156
		$xml->setIndent(true);
2157
		$xml->startDocument('1.0', 'utf-8');
2158
		$xml->startElementNs(null, 'error', 'DAV:');
2159
2160
		self::add_prop($xml, $xml_error);
2161
2162
		if (!empty($human_readable))
2163
		{
2164
			$xml->writeElement('responsedescription', $human_readable);
2165
		}
2166
2167
		$xml->endElement();	// DAV:error
2168
		$xml->endDocument();
2169
		echo $xml->outputMemory();
2170
	}
2171
2172
	/**
2173
	 * Recursivly add properties to XMLWriter object
2174
	 *
2175
	 * @param \XMLWriter $xml
2176
	 * @param string|array $props string with name for empty element in DAV NS or array with props
2177
	 */
2178
	protected static function add_prop(\XMLWriter $xml, $props)
2179
	{
2180
		if (is_string($props)) $props = self::mkprop($props, '');
2181
		if (isset($props['name'])) $props = array($props);
2182
2183
		foreach($props as $prop)
2184
		{
2185
			if (isset($prop['ns']) && $prop['ns'] !== 'DAV:')
2186
			{
2187
				$xml->startElementNs(null, $prop['name'], $prop['ns']);
2188
			}
2189
			else
2190
			{
2191
				$xml->startElement($prop['name']);
2192
			}
2193
			if (is_array($prop['val']))
2194
			{
2195
				self::add_prop($xml, $prop['val']);
2196
			}
2197
			else
2198
			{
2199
				$xml->text((string)$prop['val']);
2200
			}
2201
			$xml->endElement();
2202
		}
2203
	}
2204
2205
	/**
2206
	 * Content of log() calls, to be appended to request_log
2207
	 *
2208
	 * @var array
2209
	 */
2210
	private $to_log = array();
2211
2212
	/**
2213
	 * Log unconditional to own request- and PHP error-log
2214
	 *
2215
	 * @param string $str
2216
	 */
2217
	public function log($str)
2218
	{
2219
		$this->to_log[] = $str;
2220
2221
		error_log($str);
2222
	}
2223
2224
	/**
2225
	 * Exception handler, which additionally logs the request (incl. a trace)
2226
	 *
2227
	 * Does NOT return and get installed in constructor.
2228
	 *
2229
	 * @param \Exception|\Error $e
2230
	 */
2231
	public static function exception_handler($e)
2232
	{
2233
		// logging exception as regular egw_execption_hander does
2234
		$headline = null;
2235
		_egw_log_exception($e,$headline);
2236
2237
		// exception handler sending message back to the client as basic auth message
2238
		$error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage());
2239
		header('WWW-Authenticate: Basic realm="'.$headline.': '.$error.'"');
2240
		header('HTTP/1.1 401 Unauthorized');
2241
		header('X-WebDAV-Status: 401 Unauthorized', true);
2242
2243
		// if our own logging is active, log the request plus a trace, if enabled in server-config
2244
		if (self::$request_starttime && isset(self::$instance))
2245
		{
2246
			self::$instance->_http_status = '401 Unauthorized';	// to correctly log it
2247
			if ($GLOBALS['egw_info']['server']['exception_show_trace'])
2248
			{
2249
				self::$instance->log_request("\n".$e->getTraceAsString()."\n");
2250
			}
2251
			else
2252
			{
2253
				self::$instance->log_request();
2254
			}
2255
		}
2256
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2257
	}
2258
2259
	/**
2260
	 * Generate a unique id, which can be used for syncronisation
2261
	 *
2262
	 * @param string $_appName the appname
2263
	 * @param string $_eventID the id of the content
2264
	 * @return string the unique id
2265
	 */
2266
	static function generate_uid($_appName, $_eventID)
2267
	{
2268
		if(empty($_appName) || empty($_eventID)) return false;
2269
2270
		return $_appName.'-'.$_eventID.'-'.$GLOBALS['egw_info']['server']['install_id'];
2271
	}
2272
}
2273