locking for eGW's WebDAV (and later on CalDAV).

no recursive (depth infinit) locks atm.
This commit is contained in:
Ralf Becker 2008-05-01 11:44:55 +00:00
parent 2e0882c868
commit f6a883713f
5 changed files with 349 additions and 227 deletions

View File

@ -72,6 +72,10 @@ class egw_vfs extends vfs_stream_wrapper
* Excecutable bit, here only use to check if user is allowed to search dirs
*/
const EXECUTABLE = 1;
/**
* Name of the lock table
*/
const LOCK_TABLE = 'egw_locks';
/**
* Current user has root rights, no access checks performed!
*
@ -96,6 +100,12 @@ class egw_vfs extends vfs_stream_wrapper
* @var int
*/
static $find_total;
/**
* Reference to the global db object
*
* @var egw_db
*/
static $db;
/**
* fopen working on just the eGW VFS
@ -947,6 +957,158 @@ class egw_vfs extends vfs_stream_wrapper
return '/filemanager/webdav.php'.$path;
}
/**
* We cache locks within a request, as HTTP_WebDAV_Server generates so many, that it can be a bottleneck
*
* @var array
*/
static protected $lock_cache;
/**
* lock a ressource/path
*
* @param string $path path or url
* @param string $token
* @param int &$timeout
* @param string &$owner
* @param string &$scope
* @param string &$type
* @param boolean $update=false
* @param boolean $check_writable=true should we check if the ressource is writable, before granting locks, default yes
* @return boolean true on success
*/
function lock($path,$token,&$timeout,&$owner,&$scope,&$type,$update=false,$check_writable=true)
{
// we require write rights to lock/unlock a resource
if (!$path || $update && !$token || $check_writable && !egw_vfs::is_writable($path))
{
return false;
}
// remove the lock info evtl. set in the cache
unset(self::$lock_cache[$path]);
if ($update) // Lock Update
{
if (($ret = (boolean)($row = self::$db->select(self::LOCK_TABLE,array('lock_owner','lock_exclusive','lock_write'),array(
'lock_path' => $path,
'lock_token' => $token,
)))))
{
$owner = $row['lock_owner'];
$scope = egw_db::from_bool($row['lock_exclusive']) ? 'exclusive' : 'shared';
$type = egw_db::from_bool($row['lock_write']) ? 'write' : 'read';
self::$db->update(self::LOCK_TABLE,array(
'lock_expires' => $timeout,
'lock_modified' => time(),
),array(
'path' => $path,
'token' => $token,
),__LINE__,__FILE__);
}
}
// HTTP_WebDAV_Server does this check before calling LOCK, but we want to be complete and usable outside WebDAV
elseif(($lock = self::checkLock($path)) && ($lock['scope'] == 'exclusive' || $scope == 'exclusive'))
{
$ret = false; // there's alread a lock
}
else
{
// HTTP_WebDAV_Server sets owner and token, but we want to be complete and usable outside WebDAV
if (!$owner || $owner == 'unknown')
{
$owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email'];
}
if (!$token)
{
$token = HTTP_WebDAV_Server::_new_locktoken();
}
try {
self::$db->insert(self::LOCK_TABLE,array(
'lock_token' => $token,
'lock_path' => $path,
'lock_created' => time(),
'lock_modified' => time(),
'lock_owner' => $owner,
'lock_expires' => $timeout,
'lock_exclusive' => $scope == 'exclusive',
'lock_write' => $type == 'write',
),false,__LINE__,__FILE__);
$ret = true;
}
catch(egw_exception_db $e) {
$ret = false; // there's already a lock
}
}
error_log(__METHOD__."($path,$token,$timeout,$owner,$scope,$type,$update,$check_writable) returns ".($ret ? 'true' : 'false'));
return $ret;
}
/**
* unlock a ressource/path
*
* @param string $path path to unlock
* @param string $token locktoken
* @param boolean $check_writable=true should we check if the ressource is writable, before granting locks, default yes
* @return boolean true on success
*/
function unlock($path,$token,$check_writable=true)
{
// we require write rights to lock/unlock a resource
if ($check_writable && !egw_vfs::is_writable($path))
{
return false;
}
if (($ret = self::$db->delete(self::LOCK_TABLE,array(
'lock_path' => $path,
'lock_token' => $token,
),__LINE__,__FILE__) && self::$db->affected_rows()))
{
// remove the lock from the cache too
unset(self::$lock_cache[$path]);
}
error_log(__METHOD__."($path,$token,$check_writable) returns ".($ret ? 'true' : 'false'));
return $ret;
}
/**
* checkLock() helper
*
* @param string resource path to check for locks
* @return array|boolean false if there's no lock, else array with lock info
*/
function checkLock($path)
{
if (isset(self::$lock_cache[$path]))
{
error_log(__METHOD__."($path) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$path],true)));
return self::$lock_cache[$path];
}
$where = 'lock_path='.self::$db->quote($path);
// ToDo: additional check parent dirs for locks and children of the requested directory
//$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%');
// ToDo: shared locks can return multiple rows
if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch()))
{
$result = egw_db::strip_array_keys($result,'lock_');
$result['type'] = egw_db::from_bool($result['write']) ? 'write' : 'read';
$result['scope'] = egw_db::from_bool($result['exclusive']) ? 'exclusive' : 'shared';
$result['depth'] = egw_db::from_bool($result['recursive']) ? 'infinite' : 0;
}
if ($result && $result['expires'] < time()) // lock is expired --> remove it
{
self::$db->delete(self::LOCK_TABLE,array(
'lock_path' => $result['path'],
'lock_token' => $result['token'],
),__LINE__,__FILE__);
error_log(__METHOD__."($path) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed");
$result = false;
}
error_log(__METHOD__."($path) returns ".($result?str_replace(array("\n",' '),'',print_r($result,true)):'false'));
return self::$lock_cache[$path] = $result;
}
/**
* Initialise our static vars
*/
@ -954,6 +1116,8 @@ class egw_vfs extends vfs_stream_wrapper
{
self::$user = (int) $GLOBALS['egw_info']['user']['account_id'];
self::$is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']);
self::$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
self::$lock_cache = array();
}
}

View File

@ -60,10 +60,10 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
{
foreach (apache_request_headers() as $key => $value)
{
if (stristr($key, "litmus"))
if (stristr($key, 'litmus'))
{
error_log("Litmus test $value");
header("X-Litmus-reply: ".$value);
header('X-Litmus-reply: '.$value);
}
}
}
@ -71,114 +71,110 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
HTTP_WebDAV_Server::ServeRequest();
}
/**
* DELETE method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function DELETE($options)
{
$path = $this->base . "/" .$options["path"];
/**
* DELETE method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function DELETE($options)
{
$path = $this->base . $options['path'];
if (!file_exists($path))
{
return "404 Not found";
}
if (!file_exists($path))
{
return '404 Not found';
}
if (is_dir($path))
{
/*$query = "DELETE FROM {$this->db_prefix}properties
WHERE path LIKE '".$this->_slashify($options["path"])."%'";
mysql_query($query); */
// recursive delete the directory
if ($dir = egw_vfs::dir_opendir($options["path"]))
{
while(($file = readdir($dir)))
{
if ($file == '.' || $file == '..') continue;
if (is_dir($path))
{
if (is_dir($path.'/'.$file))
{
// recursivly call ourself with the dir
$opts = $options;
$opts['path'] .= '/'.$file;
$this->DELETE($opts);
}
else
{
unlink($path.'/'.$file);
}
}
closedir($dir);
}
}
else
{
unlink($path);
}
/*$query = "DELETE FROM {$this->db_prefix}properties
WHERE path = '$options[path]'";
mysql_query($query);*/
/*$query = "DELETE FROM {$this->db_prefix}properties
WHERE path LIKE '".$this->_slashify($options["path"])."%'";
mysql_query($query); */
return "204 No Content";
}
// recursive delete the directory
egw_vfs::remove($options['path']);
}
else
{
unlink($path);
}
/*$query = "DELETE FROM {$this->db_prefix}properties
WHERE path = '$options[path]'";
mysql_query($query);*/
/**
* Get properties for a single file/resource
*
* @param string resource path
* @return array resource properties
*/
function fileinfo($path)
{
error_log(__METHOD__."($path)");
// map URI path to filesystem path
$fspath = $this->base . $path;
return '204 No Content';
}
// create result array
$info = array();
// TODO remove slash append code when base clase is able to do it itself
$info["path"] = is_dir($fspath) ? $this->_slashify($path) : $path;
$info["props"] = array();
/**
* Get properties for a single file/resource
*
* @param string resource path
* @return array resource properties
*/
function fileinfo($path)
{
//error_log(__METHOD__."($path)");
// map URI path to filesystem path
$fspath = $this->base . $path;
// no special beautified displayname here ...
$info["props"][] = $this->mkprop("displayname", strtoupper($path));
// create result array
$info = array();
// TODO remove slash append code when base class is able to do it itself
$info['path'] = is_dir($fspath) ? $this->_slashify($path) : $path;
$info['props'] = array();
// creation and modification time
$info["props"][] = $this->mkprop("creationdate", filectime($fspath));
$info["props"][] = $this->mkprop("getlastmodified", filemtime($fspath));
// no special beautified displayname here ...
$info['props'][] = HTTP_WebDAV_Server::mkprop ('displayname', strtoupper($path));
// type and size (caller already made sure that path exists)
if (is_dir($fspath)) {
// directory (WebDAV collection)
$info["props"][] = $this->mkprop("resourcetype", "collection");
$info["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory");
} else {
// plain file (WebDAV resource)
$info["props"][] = $this->mkprop("resourcetype", "");
if (egw_vfs::is_readable($path)) {
$info["props"][] = $this->mkprop("getcontenttype", egw_vfs::mime_content_type($path));
} else {
// creation and modification time
$info['props'][] = HTTP_WebDAV_Server::mkprop ('creationdate', filectime($fspath));
$info['props'][] = HTTP_WebDAV_Server::mkprop ('getlastmodified', filemtime($fspath));
// type and size (caller already made sure that path exists)
if (is_dir($fspath)) {
// directory (WebDAV collection)
$info['props'][] = HTTP_WebDAV_Server::mkprop ('resourcetype', 'collection');
$info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontenttype', 'httpd/unix-directory');
} else {
// plain file (WebDAV resource)
$info['props'][] = HTTP_WebDAV_Server::mkprop ('resourcetype', '');
if (egw_vfs::is_readable($path)) {
$info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontenttype', egw_vfs::mime_content_type($path));
} else {
error_log(__METHOD__."($path) $fspath is not readable!");
$info["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable");
}
$info["props"][] = $this->mkprop("getcontentlength", filesize($fspath));
}
$info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontenttype', 'application/x-non-readable');
}
$info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontentlength', filesize($fspath));
}
// supportedlock property
$info['props'][] = HTTP_WebDAV_Server::mkprop('supportedlock','
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:lockscope>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:lockscope>
</D:lockentry>');
// ToDo: etag from inode and modification time
/*
// get additional properties from database
$query = "SELECT ns, name, value
FROM {$this->db_prefix}properties
WHERE path = '$path'";
$res = mysql_query($query);
while ($row = mysql_fetch_assoc($res)) {
$info["props"][] = $this->mkprop($row["ns"], $row["name"], $row["value"]);
}
mysql_free_result($res);
// get additional properties from database
$query = "SELECT ns, name, value
FROM {$this->db_prefix}properties
WHERE path = '$path'";
$res = mysql_query($query);
while ($row = mysql_fetch_assoc($res)) {
$info["props"][] = HTTP_WebDAV_Server::mkprop ($row["ns"], $row["name"], $row["value"]);
}
mysql_free_result($res);
*/
//error_log(__METHOD__."($path) info=".print_r($info,true));
return $info;
}
return $info;
}
/**
* Used eg. by get
@ -207,7 +203,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
{
$path = $GLOBALS['egw']->translation->convert($options['path'],'utf-8');
foreach ($options["props"] as $key => $prop) {
foreach ($options['props'] as $key => $prop) {
$attributes = array();
switch($prop['ns'])
{
@ -235,12 +231,12 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
break;
// not sure why, the filesystem example of the WebDAV class does it ...
default:
$options["props"][$key]['status'] = "403 Forbidden";
$options['props'][$key]['status'] = '403 Forbidden';
break;
}
break;
}
if ($this->debug) $props[] = '('.$prop["ns"].')'.$prop['name'].'='.$prop['val'];
if ($this->debug) $props[] = '('.$prop['ns'].')'.$prop['name'].'='.$prop['val'];
}
if ($this->debug)
{
@ -248,137 +244,55 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
if ($attributes) error_log(__METHOD__.": path=$options[path], set attributes=".str_replace("\n",' ',print_r($attributes,true)));
}
return ""; // this is as the filesystem example handler does it, no true or false ...
return ''; // this is as the filesystem example handler does it, no true or false ...
}
/**
* LOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function LOCK(&$options)
{
// behaving like LOCK is not implemented
return "412 Precondition failed";
/*
// get absolute fs path to requested resource
$fspath = $this->base . $options["path"];
/**
* LOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function LOCK(&$options)
{
error_log(__METHOD__.'('.str_replace(array("\n",' '),'',print_r($options,true)).')');
// TODO recursive locks on directories not supported yet
if (is_dir($fspath) && !empty($options["depth"])) {
return "409 Conflict";
if (is_dir($this->base . $options['path']) && !empty($options['depth']))
{
return '409 Conflict';
}
$options['timeout'] = time()+300; // 5min. hardcoded
$options["timeout"] = time()+300; // 5min. hardcoded
// dont know why, but HTTP_WebDAV_Server passes the owner in D:href tags, which get's passed unchanged to checkLock/PROPFIND
// that's wrong according to the standard and cadaver does not show it on discover --> strip_tags removes eventual tags
if (($ret = egw_vfs::lock($options['path'],$options['locktoken'],$options['timeout'],strip_tags($options['owner']),
$options['scope'],$options['type'],isset($options['update']))) && !isset($options['update']))
{
return $ret ? '200 OK' : '409 Conflict';
}
return $ret;
}
if (isset($options["update"])) { // Lock Update
$where = "WHERE path = '$options[path]' AND token = '$options[update]'";
/**
* UNLOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function UNLOCK(&$options)
{
error_log(__METHOD__.'('.str_replace(array("\n",' '),'',print_r($options,true)).')');
return egw_vfs::unlock($options['path'],$options['token']) ? '204 No Content' : '409 Conflict';
}
$query = "SELECT owner, exclusivelock FROM {$this->db_prefix}locks $where";
$res = mysql_query($query);
$row = mysql_fetch_assoc($res);
mysql_free_result($res);
if (is_array($row)) {
$query = "UPDATE {$this->db_prefix}locks
SET expires = '$options[timeout]'
, modified = ".time()."
$where";
mysql_query($query);
$options['owner'] = $row['owner'];
$options['scope'] = $row["exclusivelock"] ? "exclusive" : "shared";
$options['type'] = $row["exclusivelock"] ? "write" : "read";
return true;
} else {
return false;
}
}
$query = "INSERT INTO {$this->db_prefix}locks
SET token = '$options[locktoken]'
, path = '$options[path]'
, created = ".time()."
, modified = ".time()."
, owner = '$options[owner]'
, expires = '$options[timeout]'
, exclusivelock = " .($options['scope'] === "exclusive" ? "1" : "0")
;
mysql_query($query);
return mysql_affected_rows() ? "200 OK" : "409 Conflict";*/
}
/**
* UNLOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function UNLOCK(&$options)
{
// behaving like LOCK is not implemented
return "405 Method not allowed";
/*
$query = "DELETE FROM {$this->db_prefix}locks
WHERE path = '$options[path]'
AND token = '$options[token]'";
mysql_query($query);
return mysql_affected_rows() ? "204 No Content" : "409 Conflict";*/
}
/**
* checkLock() helper
*
* @param string resource path to check for locks
* @return bool true on success
*/
function checkLock($path)
{
// behave like checkLock is not implemented
return false;
/*
$result = false;
$query = "SELECT owner, token, created, modified, expires, exclusivelock
FROM {$this->db_prefix}locks
WHERE path = '$path'
";
$res = mysql_query($query);
if ($res) {
$row = mysql_fetch_array($res);
mysql_free_result($res);
if ($row) {
$result = array( "type" => "write",
"scope" => $row["exclusivelock"] ? "exclusive" : "shared",
"depth" => 0,
"owner" => $row['owner'],
"token" => $row['token'],
"created" => $row['created'],
"modified" => $row['modified'],
"expires" => $row['expires']
);
}
}
return $result;*/
}
/**
* Remove not (yet) implemented LOCK methods, so we can use the mostly unchanged HTTP_WebDAV_Server_Filesystem class
*
* @return array
*/
function _allow()
{
$allow = parent::_allow();
unset($allow['LOCK']);
unset($allow['UNLOCK']);
return $allow;
}
/**
* checkLock() helper
*
* @param string resource path to check for locks
* @return bool true on success
*/
function checkLock($path)
{
return egw_vfs::checkLock($path);
}
}

View File

@ -12,7 +12,7 @@
/* Basic information about this app */
$setup_info['phpgwapi']['name'] = 'phpgwapi';
$setup_info['phpgwapi']['title'] = 'eGroupWare API';
$setup_info['phpgwapi']['version'] = '1.5.009';
$setup_info['phpgwapi']['version'] = '1.5.010';
$setup_info['phpgwapi']['versions']['current_header'] = '1.28';
$setup_info['phpgwapi']['enable'] = 3;
$setup_info['phpgwapi']['app_order'] = 1;
@ -47,6 +47,7 @@ $setup_info['phpgwapi']['tables'][] = 'egw_sqlfs';
$setup_info['phpgwapi']['tables'][] = 'egw_index_keywords';
$setup_info['phpgwapi']['tables'][] = 'egw_index';
$setup_info['phpgwapi']['tables'][] = 'egw_cat2entry';
$setup_info['phpgwapi']['tables'][] = 'egw_locks';
// hooks used by vfs_home_hooks to manage user- and group-directories for the new stream based VFS
$setup_info['phpgwapi']['hooks']['addaccount'] = 'phpgwapi.vfs_home_hooks.addAccount';
@ -65,3 +66,5 @@ $setup_info['notifywindow']['app_order'] = 1;
$setup_info['notifywindow']['tables'] = '';
$setup_info['notifywindow']['hooks'][] = 'home';

View File

@ -486,5 +486,22 @@ $phpgw_baseline = array(
'fk' => array(),
'ix' => array('cat_id'),
'uc' => array()
),
'egw_locks' => array(
'fd' => array(
'lock_token' => array('type' => 'varchar','precision' => '255','nullable' => False),
'lock_path' => array('type' => 'varchar','precision' => '255','nullable' => False),
'lock_expires' => array('type' => 'int','precision' => '8','nullable' => False),
'lock_owner' => array('type' => 'varchar','precision' => '255'),
'lock_recursive' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_write' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_exclusive' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_created' => array('type' => 'int','precision' => '8','default' => '0'),
'lock_modified' => array('type' => 'int','precision' => '8','default' => '0')
),
'pk' => array('lock_token'),
'fk' => array(),
'ix' => array('lock_path','lock_expires'),
'uc' => array()
)
);

View File

@ -389,3 +389,27 @@ function phpgwapi_upgrade1_5_008()
return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '1.5.009';
}
$test[] = '1.5.009';
function phpgwapi_upgrade1_5_009()
{
$GLOBALS['egw_setup']->oProc->CreateTable('egw_locks',array(
'fd' => array(
'lock_token' => array('type' => 'varchar','precision' => '255','nullable' => False),
'lock_path' => array('type' => 'varchar','precision' => '255','nullable' => False),
'lock_expires' => array('type' => 'int','precision' => '8','nullable' => False),
'lock_owner' => array('type' => 'varchar','precision' => '255'),
'lock_recursive' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_write' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_exclusive' => array('type' => 'bool','nullable' => False,'default' => '0'),
'lock_created' => array('type' => 'int','precision' => '8','default' => '0'),
'lock_modified' => array('type' => 'int','precision' => '8','default' => '0')
),
'pk' => array('lock_token'),
'fk' => array(),
'ix' => array('lock_path','lock_expires'),
'uc' => array()
));
return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '1.5.010';
}