mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-16 13:03:16 +01:00
447c8b618a
allow to do propfinds on hugh addressbooks independent of memory_limit: - regular groupdav_handler::profind() method gets split in a method just computing a filter and a callback to run that filter on the backend - groupdav_propfind_iterator class is returned from profind method instead of an array with information about the files - iterator calls groupdav_hander::propfind_callback if there are no more entries from the previous call - constructor of groupdav_propfind_iterator allows to pass an extra array with files to return, to simplify modifying existing implementation (were eg. information about the current path, get's supplied from calling groupdav class).
450 lines
12 KiB
PHP
450 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* EGroupware: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @subpackage groupdav
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @copyright (c) 2007-9 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @version $Id$
|
|
*/
|
|
|
|
/**
|
|
* EGroupware: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers
|
|
*/
|
|
abstract class groupdav_handler
|
|
{
|
|
/**
|
|
* Debug level: 0 = nothing, 1 = function calls, 2 = more info, eg. complete $_SERVER array
|
|
*
|
|
* The debug messages are send to the apache error_log
|
|
*
|
|
* @var integer
|
|
*/
|
|
var $debug = 0;
|
|
|
|
/**
|
|
* eGW's charset
|
|
*
|
|
* @var string
|
|
*/
|
|
var $egw_charset;
|
|
/**
|
|
* Reference to the translation class
|
|
*
|
|
* @var translation
|
|
*/
|
|
var $translation;
|
|
/**
|
|
* Translates method names into ACL bits
|
|
*
|
|
* @var array
|
|
*/
|
|
var $method2acl = array(
|
|
'GET' => EGW_ACL_READ,
|
|
'PUT' => EGW_ACL_EDIT,
|
|
'DELETE' => EGW_ACL_DELETE,
|
|
);
|
|
/**
|
|
* eGW application responsible for the handler
|
|
*
|
|
* @var string
|
|
*/
|
|
var $app;
|
|
/**
|
|
* Base url of handler, need to prefix all pathes not automatic handled by HTTP_WebDAV_Server
|
|
*
|
|
* @var string
|
|
*/
|
|
var $base_uri;
|
|
/**
|
|
* HTTP_IF_MATCH / etag of current request / last call to _common_get_put_delete() method
|
|
*
|
|
* @var string
|
|
*/
|
|
var $http_if_match;
|
|
/**
|
|
* Identified user agent
|
|
*
|
|
* @var string
|
|
*/
|
|
var $agent;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $app 'calendar', 'addressbook' or 'infolog'
|
|
* @param int $debug=null debug-level to set
|
|
* @param string $base_uri=null base url of handler
|
|
*/
|
|
function __construct($app,$debug=null,$base_uri=null)
|
|
{
|
|
//error_log(__METHOD__." called");
|
|
$this->app = $app;
|
|
#if (!is_null($debug)) $this->debug = $debug = 3;
|
|
$this->base_uri = is_null($base_uri) ? $base_uri : $_SERVER['SCRIPT_NAME'];
|
|
$this->agent = self::get_agent();
|
|
|
|
$this->translation =& $GLOBALS['egw']->translation;
|
|
$this->egw_charset = $this->translation->charset();
|
|
}
|
|
|
|
/**
|
|
* Handle propfind request for an application folder
|
|
*
|
|
* @param string $path
|
|
* @param array $options
|
|
* @param array &$files
|
|
* @param int $user account_id
|
|
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
|
|
*/
|
|
abstract function propfind($path,$options,&$files,$user);
|
|
|
|
/**
|
|
* Propfind callback, if interator is used
|
|
*
|
|
* @param array $filter
|
|
* @param array|boolean $start false=return all or array(start,num)
|
|
* @param int &$total
|
|
* @return array with "files" array with values for keys path and props
|
|
*/
|
|
function &propfind_callback(array $filter,$start,&$total) { }
|
|
|
|
/**
|
|
* Handle get request for an applications entry
|
|
*
|
|
* @param array &$options
|
|
* @param int $id
|
|
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
|
|
*/
|
|
abstract function get(&$options,$id);
|
|
|
|
/**
|
|
* Handle get request for an applications entry
|
|
*
|
|
* @param array &$options
|
|
* @param int $id
|
|
* @param int $user=null account_id of owner, default null
|
|
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
|
|
*/
|
|
abstract function put(&$options,$id,$user=null);
|
|
|
|
/**
|
|
* Handle get request for an applications entry
|
|
*
|
|
* @param array &$options
|
|
* @param int $id
|
|
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
|
|
*/
|
|
abstract function delete(&$options,$id);
|
|
|
|
/**
|
|
* Read an entry
|
|
*
|
|
* @param string/int $id
|
|
* @return array/boolean array with entry, false if no read rights, null if $id does not exist
|
|
*/
|
|
abstract function read($id);
|
|
|
|
/**
|
|
* Check if user has the neccessary rights on an entry
|
|
*
|
|
* @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE
|
|
* @param array/int $entry entry-array or id
|
|
* @return boolean null if entry does not exist, false if no access, true if access permitted
|
|
*/
|
|
abstract function check_access($acl,$entry);
|
|
|
|
/**
|
|
* Add extra properties for collections
|
|
*
|
|
* @param array $props=array() regular props by the groupdav handler
|
|
* @return array
|
|
*/
|
|
static function extra_properties(array $props=array())
|
|
{
|
|
return $props;
|
|
}
|
|
|
|
/**
|
|
* Get the etag for an entry, can be reimplemented for other algorithm or field names
|
|
*
|
|
* @param array/int $event array with event or cal_id
|
|
* @return string/boolean string with etag or false
|
|
*/
|
|
function get_etag($entry)
|
|
{
|
|
if (!is_array($entry))
|
|
{
|
|
$entry = $this->read($entry);
|
|
}
|
|
if (!is_array($entry) || !isset($entry['id']) || !(isset($entry['modified']) || isset($entry['etag'])))
|
|
{
|
|
// error_log(__METHOD__."(".array2string($entry).") Cant create etag!");
|
|
return false;
|
|
}
|
|
return '"'.$entry['id'].':'.(isset($entry['etag']) ? $entry['etag'] : $entry['modified']).'"';
|
|
}
|
|
|
|
/**
|
|
* Convert etag to the raw etag column value (without quotes, double colon and id)
|
|
*
|
|
* @param string $etag
|
|
* @return int
|
|
*/
|
|
static function etag2value($etag)
|
|
{
|
|
list(,$val) = explode(':',substr($etag,1,-1),2);
|
|
|
|
return $val;
|
|
}
|
|
|
|
/**
|
|
* Handle common stuff for get, put and delete requests:
|
|
* - application rights
|
|
* - entry level acl, incl. edit and delete rights
|
|
* - etag handling for precondition failed and not modified
|
|
*
|
|
* @param string $method GET, PUT, DELETE
|
|
* @param array &$options
|
|
* @param int $id
|
|
* @param boolean &$return_no_access=false if set to true on call, instead of '403 Forbidden' the entry is returned and $return_no_access===false
|
|
* @return array/string entry on success, string with http-error-code on failure, null for PUT on an unknown id
|
|
*/
|
|
function _common_get_put_delete($method,&$options,$id,&$return_no_access=false)
|
|
{
|
|
if (!in_array($this->app,array('principals','groups')) && !$GLOBALS['egw_info']['user']['apps'][$this->app])
|
|
{
|
|
if ($this->debug) error_log(__METHOD__."($method,,$id) 403 Forbidden: no app rights for '$this->app'");
|
|
return '403 Forbidden'; // no app rights
|
|
}
|
|
$extra_acl = $this->method2acl[$method];
|
|
if (!($entry = $this->read($id)) && ($method != 'PUT' || $entry === false) ||
|
|
($extra_acl != EGW_ACL_READ && $this->check_access($extra_acl,$entry) === false))
|
|
{
|
|
if ($return_no_access && !is_null($entry))
|
|
{
|
|
if ($this->debug) error_log(__METHOD__."($method,,$id,$return_no_access) is_null(\$entry)=".(int)is_null($entry).", set to false");
|
|
$return_no_access = false;
|
|
}
|
|
else
|
|
{
|
|
if ($this->debug) error_log(__METHOD__."($method,,$id) 403 Forbidden/404 Not Found: read($id)==".($entry===false?'false':'null'));
|
|
return !is_null($entry) ? '403 Forbidden' : '404 Not Found';
|
|
}
|
|
}
|
|
if ($entry)
|
|
{
|
|
$etag = $this->get_etag($entry);
|
|
// If the clients sends an "If-Match" header ($_SERVER['HTTP_IF_MATCH']) we check with the current etag
|
|
// of the calendar --> on failure we return 412 Precondition failed, to not overwrite the modifications
|
|
if (isset($_SERVER['HTTP_IF_MATCH']) && ($this->http_if_match = $_SERVER['HTTP_IF_MATCH']) != $etag)
|
|
{
|
|
if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_MATCH='$_SERVER[HTTP_IF_MATCH]', etag='$etag': 412 Precondition failed");
|
|
return '412 Precondition Failed';
|
|
}
|
|
// if an IF_NONE_MATCH is given, check if we need to send a new export, or the current one is still up-to-date
|
|
if ($method == 'GET' && isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag)
|
|
{
|
|
if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_NONE_MATCH='$_SERVER[HTTP_IF_NONE_MATCH]', etag='$etag': 304 Not Modified");
|
|
return '304 Not Modified';
|
|
}
|
|
}
|
|
return $entry;
|
|
}
|
|
|
|
/**
|
|
* Get the handler for the given app
|
|
*
|
|
* @static
|
|
* @param string $app 'calendar', 'addressbook' or 'infolog'
|
|
* @param int $debug=null debug-level to set
|
|
* @param string $base_uri=null base url of handler
|
|
* @return groupdav_handler
|
|
*/
|
|
static function &app_handler($app,$debug=null,$base_uri=null)
|
|
{
|
|
static $handler_cache = array();
|
|
|
|
if (!array_key_exists($app,$handler_cache))
|
|
{
|
|
$class = $app.'_groupdav';
|
|
if (!class_exists($class) && !class_exists($class = 'groupdav_'.$app)) return null;
|
|
|
|
$handler_cache[$app] = new $class($app,$debug,$base_uri);
|
|
}
|
|
return $handler_cache[$app];
|
|
}
|
|
|
|
/**
|
|
* Identify know GroupDAV agents by HTTP_USER_AGENT header
|
|
*
|
|
* @return string|boolean agent name or false
|
|
*/
|
|
static function get_agent()
|
|
{
|
|
static $agent;
|
|
|
|
if (is_null($agent))
|
|
{
|
|
$agent = false;
|
|
// identify the agent (GroupDAV client) from the HTTP_USER_AGENT header
|
|
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
|
|
foreach(array(
|
|
'davkit' => 'davkit', // Apple iCal
|
|
'bionicmessage.net' => 'funambol', // funambol GroupDAV connector from bionicmessage.net
|
|
'zideone' => 'zideone', // zideone outlook plugin
|
|
'lightning' => 'lightning', // Lighting (SOGo connector for addressbook)
|
|
'khtml' => 'kde', // KDE clients
|
|
'cadaver' => 'cadaver',
|
|
) as $pattern => $name)
|
|
{
|
|
if (strpos($user_agent,$pattern) !== false)
|
|
{
|
|
$agent = $name;
|
|
break;
|
|
}
|
|
}
|
|
if (!$agent)
|
|
{
|
|
error_log("Unrecogniced GroupDAV client: HTTP_USER_AGENT='$_SERVER[HTTP_USER_AGENT]'!");
|
|
}
|
|
}
|
|
return $agent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterator for propfinds using propfind callback of a groupdav_handler to query results in chunks
|
|
*
|
|
* The propfind method just computes a filter and then returns an instance of this iterator instead of the files:
|
|
*
|
|
* function propfind($path,$options,&$files,$user,$id='')
|
|
* {
|
|
* $filter = array();
|
|
* // compute filter from path, options, ...
|
|
*
|
|
* $files['files'] = new groupdav_propfind_iterator($this,$filter,$files['files']);
|
|
*
|
|
* return true;
|
|
* }
|
|
*/
|
|
class groupdav_propfind_iterator implements Iterator
|
|
{
|
|
/**
|
|
* Handler to call for entries
|
|
*
|
|
* @var groupdav_handler
|
|
*/
|
|
protected $handler;
|
|
|
|
/**
|
|
* Filter of propfind call
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $filter;
|
|
|
|
/**
|
|
* Extra responses to return too
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $files;
|
|
|
|
/**
|
|
* Start value for callback
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $start=0;
|
|
|
|
/**
|
|
* Number of entries queried from callback in one call
|
|
*
|
|
*/
|
|
const CHUNK_SIZE = 100;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param groupdav_handler $handler
|
|
* @param array $filter filter for propfind call
|
|
* @param array $files=null extra files/responses to return too
|
|
*/
|
|
public function __construct(groupdav_handler $handler,array $filter,array &$files=null)
|
|
{
|
|
$this->handler = $handler;
|
|
$this->filter = $filter;
|
|
$this->files = $files;
|
|
reset($this->files);
|
|
}
|
|
|
|
/**
|
|
* Return the current element
|
|
*
|
|
* @return array
|
|
*/
|
|
public function current()
|
|
{
|
|
return current($this->files);
|
|
}
|
|
|
|
/**
|
|
* Return the key of the current element
|
|
*
|
|
* @return int|string
|
|
*/
|
|
public function key()
|
|
{
|
|
$current = $this->current();
|
|
|
|
return $current['path']; // we return path as key
|
|
}
|
|
|
|
/**
|
|
* Move forward to next element (called after each foreach loop)
|
|
*/
|
|
public function next()
|
|
{
|
|
if (next($this->files) !== false)
|
|
{
|
|
return true;
|
|
}
|
|
if (!$this->handler)
|
|
{
|
|
return false; // no further entries
|
|
}
|
|
// try query further files via propfind callback of handler and store result in $this->files
|
|
$this->files = $this->handler->propfind_callback($this->filter,array($this->start,self::CHUNK_SIZE));
|
|
$this->start += self::CHUNK_SIZE;
|
|
reset($this->files);
|
|
|
|
if (count($this->files) < self::CHUNK_SIZE) // less entries then asked --> no further available
|
|
{
|
|
unset($this->handler);
|
|
}
|
|
return current($this->files) !== false;
|
|
}
|
|
|
|
/**
|
|
* Rewind the Iterator to the first element (called at beginning of foreach loop)
|
|
*/
|
|
public function rewind()
|
|
{
|
|
|
|
}
|
|
|
|
/**
|
|
* Checks if current position is valid
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function valid ()
|
|
{
|
|
return current($this->files) !== false;
|
|
}
|
|
}
|