diff --git a/phpgwapi/inc/class.egw_vfs.inc.php b/phpgwapi/inc/class.egw_vfs.inc.php index fe1d8992d0..2fe6733ca2 100644 --- a/phpgwapi/inc/class.egw_vfs.inc.php +++ b/phpgwapi/inc/class.egw_vfs.inc.php @@ -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(); } } diff --git a/phpgwapi/inc/class.vfs_webdav_server.inc.php b/phpgwapi/inc/class.vfs_webdav_server.inc.php index bcea0fa34d..86e5b302ea 100644 --- a/phpgwapi/inc/class.vfs_webdav_server.inc.php +++ b/phpgwapi/inc/class.vfs_webdav_server.inc.php @@ -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',' + + + + + + + + '); + + // 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); + } } \ No newline at end of file diff --git a/phpgwapi/setup/setup.inc.php b/phpgwapi/setup/setup.inc.php index b843527e26..ac6fbf8682 100755 --- a/phpgwapi/setup/setup.inc.php +++ b/phpgwapi/setup/setup.inc.php @@ -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'; + + diff --git a/phpgwapi/setup/tables_current.inc.php b/phpgwapi/setup/tables_current.inc.php index 76f20c8326..4e76da6d5c 100644 --- a/phpgwapi/setup/tables_current.inc.php +++ b/phpgwapi/setup/tables_current.inc.php @@ -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() ) ); diff --git a/phpgwapi/setup/tables_update.inc.php b/phpgwapi/setup/tables_update.inc.php index 03508156fc..2c7c9994d4 100644 --- a/phpgwapi/setup/tables_update.inc.php +++ b/phpgwapi/setup/tables_update.inc.php @@ -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'; +}