From 67cb60b972e192d34d38dd0ee2769938df1936dd Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 20 Mar 2016 16:19:53 +0000 Subject: [PATCH] moving egw_digest_auth, vfs_webdav_server and egw_sharing to new api --- api/src/Header/Authenticate.php | 274 ++++++++++++++++++ api/src/Header/Content.php | 4 +- api/src/Session.php | 3 +- .../src/Vfs/Sharing.php | 126 ++++---- .../src/Vfs/WebDAV.php | 114 ++++---- .../inc/HTTP => api/src}/WebDAV/Server.php | 6 +- .../src}/WebDAV/Server/Filesystem.php | 3 +- .../src}/WebDAV/Tools/_parse_lockinfo.php | 0 .../src}/WebDAV/Tools/_parse_propfind.php | 0 .../src}/WebDAV/Tools/_parse_proppatch.php | 0 .../inc/class.filemanager_shares.inc.php | 29 +- files/webdav.php | 24 +- groupdav.php | 5 +- phpgwapi/inc/class.egw_digest_auth.inc.php | 253 +--------------- phpgwapi/inc/class.groupdav.inc.php | 8 +- remote.php | 24 +- share.php | 10 +- webdav.php | 14 +- 18 files changed, 477 insertions(+), 420 deletions(-) create mode 100644 api/src/Header/Authenticate.php rename phpgwapi/inc/class.egw_sharing.inc.php => api/src/Vfs/Sharing.php (83%) rename phpgwapi/inc/class.vfs_webdav_server.inc.php => api/src/Vfs/WebDAV.php (86%) rename {phpgwapi/inc/HTTP => api/src}/WebDAV/Server.php (99%) rename {phpgwapi/inc/HTTP => api/src}/WebDAV/Server/Filesystem.php (99%) rename {phpgwapi/inc/HTTP => api/src}/WebDAV/Tools/_parse_lockinfo.php (100%) rename {phpgwapi/inc/HTTP => api/src}/WebDAV/Tools/_parse_propfind.php (100%) rename {phpgwapi/inc/HTTP => api/src}/WebDAV/Tools/_parse_proppatch.php (100%) diff --git a/api/src/Header/Authenticate.php b/api/src/Header/Authenticate.php new file mode 100644 index 0000000000..f2fe4c39ae --- /dev/null +++ b/api/src/Header/Authenticate.php @@ -0,0 +1,274 @@ + + * @copyright (c) 2010-16 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api\Header; + +use EGroupware\Api; + +/** + * Class to authenticate via basic or digest auth + * + * The more secure digest auth requires: + * a) cleartext passwords in SQL table + * b) md5 hashes of username, realm, password stored somewhere (NOT yet implemented) + * Otherwise digest auth is not possible and therefore not offered to the client. + * + * Usage example: + * + * $GLOBALS['egw_info']['flags'] = array( + * 'noheader' => True, + * 'currentapp' => 'someapp', + * 'no_exception_handler' => 'basic_auth', // we use a basic auth exception handler (sends exception message as basic auth realm) + * 'autocreate_session_callback' => 'EGroupware\\Api\\Header\\Authenticate::autocreate_session_callback', + * 'auth_realm' => 'EGroupware', + * ); + * include(dirname(__FILE__).'/header.inc.php'); + * + * @link http://www.php.net/manual/en/features.http-auth.php + * @ToDo check if we have to check if returned nonce matches our challange (not done in above link, but why would it be there) + * @link http://en.wikipedia.org/wiki/Digest_access_authentication + * @link http://tools.ietf.org/html/rfc2617 + * + * Commented out is accept-charset parameter from (seems not supported by any client I tested with) + * @link https://tools.ietf.org/id/draft-reschke-basicauth-enc-06.html + * + * Implemented support for clients sending credentials in iso-8859-1 instead of our utf-8: + * - Firefox 19.0 + * - Thunderbird 17.0.3 with Lightning 1.8 + * - IE 8 + * - Netdrive + * (Chrome 24 or Safari 6 sends credentials in charset of webpage.) + */ +class Authenticate +{ + /** + * Log to error_log: + * 0 = dont + * 1 = no cleartext passwords + * 2 = all + */ + const ERROR_LOG = 0; + + /** + * Callback to be used to create session via header include authenticated via basic or digest auth + * + * @param array $account NOT used! + * @return string valid session-id or does NOT return at all! + */ + static public function autocreate_session_callback(&$account) + { + unset($account); // not used, but required by function signature + if (self::ERROR_LOG) + { + $pw = self::ERROR_LOG > 1 ? $_SERVER['PHP_AUTH_PW'] : '**********'; + error_log(__METHOD__.'() PHP_AUTH_USER='.array2string($_SERVER['PHP_AUTH_USER']).', PHP_AUTH_PW='.array2string($pw).', PHP_AUTH_DIGEST='.array2string($_SERVER['PHP_AUTH_DIGEST'])); + } + $realm = $GLOBALS['egw_info']['flags']['auth_realm']; + if (empty($realm)) $realm = 'EGroupware'; + + $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; + // Support for basic auth when using PHP CGI (what about digest auth?) + if (!isset($username) && !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && strpos($_SERVER['REDIRECT_HTTP_AUTHORIZATION'],'Basic ') === 0) + { + $hash = base64_decode(substr($_SERVER['REDIRECT_HTTP_AUTHORIZATION'],6)); + if (strpos($hash, ':') !== false) + { + list($username, $password) = explode(':', $hash, 2); + } + } + elseif (isset($_SERVER['PHP_AUTH_DIGEST']) && !self::is_valid($realm,$_SERVER['PHP_AUTH_DIGEST'],$username,$password)) + { + unset($password); + } + // if given password contains non-ascii chars AND we can not authenticate with it + if (isset($username) && isset($password) && + (preg_match('/[^\x20-\x7F]/', $password) || strpos($password, '\\x') !== false) && + !$GLOBALS['egw']->auth->authenticate($username, $password, 'text')) + { + self::decode_password($password); + } + // create session without session cookie (session->create(..., true), as we use pseudo sessionid from credentials + if (!isset($username) || !($sessionid = $GLOBALS['egw']->session->create($username, $password, 'text', true))) + { + // if the session class gives a reason why the login failed --> append it to the REALM + if ($GLOBALS['egw']->session->reason) $realm .= ': '.$GLOBALS['egw']->session->reason; + + header('WWW-Authenticate: Basic realm="'.$realm.'"');// draft-reschke-basicauth-enc-06 adds, accept-charset="'.translation::charset().'"'); + self::digest_header($realm); + header('HTTP/1.1 401 Unauthorized'); + header('X-WebDAV-Status: 401 Unauthorized', true); + echo "\n\n401 Unauthorized\n\nAuthorization failed.\n\n\n"; + exit; + } + return $sessionid; + } + + /** + * Decode password containing non-ascii chars + * + * @param string &$password + * @return boolean true if conversation happend, false if there was no need for a conversation + */ + public static function decode_password(&$password) + { + // if given password contains non-ascii chars AND we can not authenticate with it + if (preg_match('/[^\x20-\x7F]/', $password) || strpos($password, '\\x') !== false) + { + // replace \x encoded non-ascii chars in password, as they are used eg. by Thunderbird for German umlauts + if (strpos($password, '\\x') !== false) + { + $password = preg_replace_callback('/\\\\x([0-9A-F]{2})/i', function($matches){ + return chr(hexdec($matches[1])); + }, $password); + } + // try translating the password from iso-8859-1 to utf-8 + $password = Api\Translation::convert($password, 'iso-8859-1'); + //error_log(__METHOD__."() Fixed non-ascii password of user '$username' from '$_SERVER[PHP_AUTH_PW]' to '$password'"); + return true; + } + return false; + } + + /** + * Check if digest auth is available for a given realm (and user): do we use cleartext passwords + * + * If no user is given, check is NOT authoritative, as we can only check if cleartext passwords are generally used + * + * @param string $realm + * @param string $username =null username or null to only check if we auth agains sql and use plaintext passwords + * @param string &$user_pw =null stored cleartext password, if $username given AND function returns true + * @return boolean true if digest auth is available, false otherwise + */ + static public function digest_auth_available($realm,$username=null,&$user_pw=null) + { + // we currently require plaintext passwords! + if (!($GLOBALS['egw_info']['server']['auth_type'] == 'sql' && $GLOBALS['egw_info']['server']['sql_encryption_type'] == 'plain') || + $GLOBALS['egw_info']['server']['auth_type'] == 'ldap' && $GLOBALS['egw_info']['server']['ldap_encryption_type'] == 'plain') + { + if (self::ERROR_LOG) error_log(__METHOD__."('$username') return false (no plaintext passwords used)"); + return false; // no plain-text passwords used + } + // check for specific user, if given + if (!is_null($username) && !(($user_pw = $GLOBALS['egw']->accounts->id2name($username,'account_pwd','u')) || + $GLOBALS['egw_info']['server']['auth_type'] == 'sql' && substr($user_pw,0,7) != '{PLAIN}')) + { + unset($user_pw); + if (self::ERROR_LOG) error_log(__METHOD__."('$realm','$username') return false (unknown user or NO plaintext password for user)"); + return false; // user does NOT exist, or has no plaintext passwords (ldap server requires real root_dn or special ACL!) + } + if (substr($user_pw,0,7) == '{PLAIN}') $user_pw = substr($user_pw,7); + + if (self::ERROR_LOG) + { + $pw = self::ERROR_LOG > 1 ? $user_pw : '**********'; + error_log(__METHOD__."('$realm','$username','$pw') return true"); + } + return true; + } + + /** + * Send header offering digest auth, if it's generally available + * + * @param string $realm + * @param string &$nonce=null on return + */ + static public function digest_header($realm,&$nonce=null) + { + if (self::digest_auth_available($realm)) + { + $nonce = uniqid(); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.$nonce.'",opaque="'.md5($realm).'"'); + if (self::ERROR_LOG) error_log(__METHOD__."() offering digest auth for realm '$realm' using nonce='$nonce'"); + } + } + + /** + * Check digest + * + * @param string $realm + * @param string $auth_digest =null default to $_SERVER['PHP_AUTH_DIGEST'] + * @param string &$username on return username + * @param string &$password on return cleartext password + * @return boolean true if digest is correct, false otherwise + */ + static public function is_valid($realm,$auth_digest=null,&$username=null,&$password=null) + { + if (is_null($auth_digest)) $auth_digest = $_SERVER['PHP_AUTH_DIGEST']; + + $data = self::parse_digest($auth_digest); + + if (!$data || !($A1 = self::get_digest_A1($realm,$username=$data['username'],$password=null))) + { + error_log(__METHOD__."('$realm','$auth_digest','$username') returning FALSE"); + return false; + } + $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); + + $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); + + if (self::ERROR_LOG) error_log(__METHOD__."('$realm','$auth_digest','$username') response='$data[response]', valid_response='$valid_response' returning ".array2string($data['response'] === $valid_response)); + return $data['response'] === $valid_response; + } + + /** + * Calculate the A1 digest hash + * + * @param string $realm + * @param string $username + * @param string &$password=null password to use or if null, on return stored password + * @return string|boolean false if $password not given and can NOT be read + */ + static private function get_digest_A1($realm,$username,&$password=null) + { + $user_pw = null; + if (empty($username) || empty($realm) || !self::digest_auth_available($realm,$username,$user_pw)) + { + return false; + } + if (is_null($password)) $password = $user_pw; + + $A1 = md5($username . ':' . $realm . ':' . $password); + if (self::ERROR_LOG > 1) error_log(__METHOD__."('$realm','$username','$password') returning ".array2string($A1)); + return $A1; + } + + /** + * Parse the http auth header + */ + static public function parse_digest($txt) + { + // protect against missing data + $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1); + $data = array(); + $keys = implode('|', array_keys($needed_parts)); + + $matches = null; + preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER); + + foreach ($matches as $m) + { + $data[$m[1]] = $m[3] ? $m[3] : $m[4]; + unset($needed_parts[$m[1]]); + } + //error_log(__METHOD__."('$txt') returning ".array2string($needed_parts ? false : $data)); + return $needed_parts ? false : $data; + } +} diff --git a/api/src/Header/Content.php b/api/src/Header/Content.php index 22e4fa8b58..3c70f6467b 100644 --- a/api/src/Header/Content.php +++ b/api/src/Header/Content.php @@ -28,9 +28,9 @@ class Content * * Mitigate risk of html downloads by using CSP or force download for IE * - * @param resource|string &$content content might be changed by this call + * @param resource|string& $content content might be changed by this call * @param string $path filename or path for content-disposition header - * @param string &$mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() + * @param string& $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() * on return used, maybe changed, mime-type * @param int $length =0 content length, default 0 = skip that header * on return changed size diff --git a/api/src/Session.php b/api/src/Session.php index dd6d2fc363..ca8a5c75e0 100644 --- a/api/src/Session.php +++ b/api/src/Session.php @@ -24,7 +24,6 @@ namespace EGroupware\Api; // explicitly reference classes still in phpgwapi use egw_mailer; -use egw_digest_auth; // egw_digest_auth::parse_digest /** * Create, verifies or destroys an EGroupware session @@ -831,7 +830,7 @@ class Session { // we generate a pseudo-sessionid from the digest username, realm and nounce // can't use full $_SERVER['PHP_AUTH_DIGEST'], as it changes (contains eg. the url) - $data = egw_digest_auth::parse_digest($_SERVER['PHP_AUTH_DIGEST']); + $data = Header\Authenticate::parse_digest($_SERVER['PHP_AUTH_DIGEST']); $sessionid = md5($data['username'].':'.$data['realm'].':'.$data['nonce'].':'.$_SERVER['HTTP_HOST']. EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/phpgwapi/setup/setup.inc.php'). ':'.$_SERVER['HTTP_USER_AGENT']); diff --git a/phpgwapi/inc/class.egw_sharing.inc.php b/api/src/Vfs/Sharing.php similarity index 83% rename from phpgwapi/inc/class.egw_sharing.inc.php rename to api/src/Vfs/Sharing.php index 248aebd1c9..2f1e9aba4c 100644 --- a/phpgwapi/inc/class.egw_sharing.inc.php +++ b/api/src/Vfs/Sharing.php @@ -5,16 +5,30 @@ * @link http://www.egroupware.org * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api + * @subpackage Vfs * @author Ralf Becker - * @copyright (c) 2014/15 by Ralf Becker + * @copyright (c) 2014-16 by Ralf Becker * @version $Id$ */ +namespace EGroupware\Api\Vfs; + +use EGroupware\Api; +use EGroupware\Api\Vfs; + +// explicitly list old, not yet ported api classes +use common; // egw_exist +use egw_framework; +use asyncservice; +use egw; // link + +use filemanager_ui; + /** * VFS sharing * * Token generation uses openssl_random_pseudo_bytes, if available, otherwise - * mt_rand based auth::randomstring is used. + * mt_rand based Api\Auth::randomstring is used. * * Existing user sessions are kept whenever possible by an additional mount into regular VFS: * - share owner is current user (no problems with rights, they simply match) @@ -26,7 +40,7 @@ * @todo handle mounts inside shared directory (they get currently lost) * @todo handle absolute symlinks (wont work as we use share as root) */ -class egw_sharing +class Sharing { /** * Length of base64 encoded token (real length is only 3/4 of it) @@ -184,9 +198,9 @@ class egw_sharing // check password, if required if ($share['share_passwd'] && (empty($_SERVER['PHP_AUTH_PW']) || - !(auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt') || - egw_digest_auth::decode_password($_SERVER['PHP_AUTH_PW']) && - auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt')))) + !(Api\Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt') || + Api\Header\Authenticate::decode_password($_SERVER['PHP_AUTH_PW']) && + Api\Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt')))) { $realm = 'EGroupware share '.$share['share_token']; header('WWW-Authenticate: Basic realm="'.$realm.'"'); @@ -201,10 +215,10 @@ class egw_sharing if (count($GLOBALS['egw_info']['server']['vfs_fstab']) <= 1) { unset($GLOBALS['egw_info']['server']['vfs_fstab']); // triggers reset of fstab in mount() - $GLOBALS['egw_info']['server']['vfs_fstab'] = egw_vfs::mount(); - egw_vfs::clearstatcache(); + $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); + Vfs::clearstatcache(); } - $share['resolve_url'] = egw_vfs::resolve_url($share['share_path'], true, true, true, true); // true = fix evtl. contained url parameter + $share['resolve_url'] = Vfs::resolve_url($share['share_path'], true, true, true, true); // true = fix evtl. contained url parameter // if share not writable append ro=1 to mount url to make it readonly if (!self::$db->from_bool($share['share_writable'])) { @@ -217,7 +231,7 @@ class egw_sharing $share['share_root'] = '/'.$share['share_token']; // if current user is not the share owner, we cant just mount share - if (egw_vfs::$user != $share['share_owner']) + if (Vfs::$user != $share['share_owner']) { $keep_session = false; } @@ -230,12 +244,12 @@ class egw_sharing ); $share['share_root'] = '/'; - egw_vfs::$user = $share['share_owner']; + Vfs::$user = $share['share_owner']; } // mounting share - egw_vfs::$is_root = true; - if (!egw_vfs::mount($share['resolve_url'], $share['share_root'], false, false, !$keep_session)) + Vfs::$is_root = true; + if (!Vfs::mount($share['resolve_url'], $share['share_root'], false, false, !$keep_session)) { sleep(1); $status = '404 Not Found'; @@ -244,8 +258,8 @@ class egw_sharing echo "Requested resource '/".htmlspecialchars($token)."' does NOT exist!\n"; common::egw_exit(); } - egw_vfs::$is_root = false; - egw_vfs::clearstatcache(); + Vfs::$is_root = false; + Vfs::clearstatcache(); // update accessed timestamp self::$db->update(self::TABLE, array( @@ -255,7 +269,7 @@ class egw_sharing ), __LINE__, __FILE__); // store sharing object in egw object and therefore in session - $GLOBALS['egw']->sharing = new egw_sharing($share); + $GLOBALS['egw']->sharing = new Sharing($share); // we have a session we want to keep, but share owner is different from current user and we need filemanager UI, or no session // --> create a new anon session @@ -284,16 +298,16 @@ class egw_sharing $GLOBALS['egw']->session->commit_session(); } // need to store new fstab and vfs_user in session to allow GET requests / downloads via WebDAV - $GLOBALS['egw_info']['user']['vfs_user'] = egw_vfs::$user; - $GLOBALS['egw_info']['server']['vfs_fstab'] = egw_vfs::mount(); + $GLOBALS['egw_info']['user']['vfs_user'] = Vfs::$user; + $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); // update modified egw and egw_info again in session, if neccessary if ($keep_session || $sessionid) { - $_SESSION[egw_session::EGW_INFO_CACHE] = $GLOBALS['egw_info']; - unset($_SESSION[egw_session::EGW_INFO_CACHE]['flags']); // dont save the flags, they change on each request + $_SESSION[Api\Session::EGW_INFO_CACHE] = $GLOBALS['egw_info']; + unset($_SESSION[Api\Session::EGW_INFO_CACHE]['flags']); // dont save the flags, they change on each request - $_SESSION[egw_session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']); + $_SESSION[Api\Session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']); } return $sessionid; @@ -308,9 +322,9 @@ class egw_sharing */ public function use_filemanager() { - return !(!egw_vfs::is_dir($this->share['share_root']) || $_SERVER['REQUEST_METHOD'] != 'GET' || + return !(!Vfs::is_dir($this->share['share_root']) || $_SERVER['REQUEST_METHOD'] != 'GET' || // or unsupported browsers like ie < 10 - html::$user_agent == 'msie' && html::$ua_version < 10.0 || + Api\Header\UserAgent::type() == 'msie' && Api\Header\UserAgent::version() < 10.0 || // or if no filemanager installed (WebDAV has own autoindex) !file_exists(__DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php')); } @@ -331,13 +345,13 @@ class egw_sharing if (!$this->use_filemanager()) { // send a content-disposition header, so browser knows how to name downloaded file - if (!egw_vfs::is_dir($this->share['share_root'])) + if (!Vfs::is_dir($this->share['share_root'])) { - html::content_disposition_header(egw_vfs::basename($this->share['share_path']), false); + Api\Header\Content::disposition(Vfs::basename($this->share['share_path']), false); } //$GLOBALS['egw']->session->commit_session(); - $webdav_server = new vfs_webdav_server(); - $webdav_server->ServeRequest(egw_vfs::concat($this->share['share_root'], $this->share['share_token'])); + $webdav_server = new Vfs\WebDAV(); + $webdav_server->ServeRequest(Vfs::concat($this->share['share_root'], $this->share['share_token'])); return; } // run full eTemplate2 UI for directories @@ -346,7 +360,7 @@ class egw_sharing $_GET['cd'] = 'no'; $GLOBALS['egw_info']['flags']['js_link_registry'] = true; egw_framework::includeCSS('filemanager', 'sharing'); - $ui = new egw_sharing_filemanager(); + $ui = new SharingUi(); $ui->index(); } @@ -357,13 +371,13 @@ class egw_sharing */ public static function token() { - // generate random token (using oppenssl if available otherwise mt_rand based auth::randomstring) + // generate random token (using oppenssl if available otherwise mt_rand based Api\Auth::randomstring) do { $token = function_exists('openssl_random_pseudo_bytes') ? base64_encode(openssl_random_pseudo_bytes(3*self::TOKEN_LENGTH/4)) : - auth::randomstring(self::TOKEN_LENGTH); + Api\Auth::randomstring(self::TOKEN_LENGTH); // base64 can contain chars not allowed in our vfs-urls eg. / or # - } while ($token != egw_vfs::encodePathComponent($token)); + } while ($token != Vfs::encodePathComponent($token)); return $token; } @@ -387,7 +401,7 @@ class egw_sharing if (empty($name)) $name = $path; - $path2tmp =& egw_cache::getSession(__CLASS__, 'path2tmp'); + $path2tmp =& Api\Cache::getSession(__CLASS__, 'path2tmp'); // allow filesystem path only for temp_dir $temp_dir = $GLOBALS['egw_info']['server']['temp_dir'].'/'; @@ -402,13 +416,13 @@ class egw_sharing { $path = 'vfs://default'.($path[0] == '/' ? '' : '/').$path; } - $vfs_path = egw_vfs::parse_url($path, PHP_URL_PATH); - $exists = egw_vfs::file_exists($vfs_path) && egw_vfs::is_readable($vfs_path); + $vfs_path = Vfs::parse_url($path, PHP_URL_PATH); + $exists = Vfs::file_exists($vfs_path) && Vfs::is_readable($vfs_path); } // check if file exists and is readable if (!$exists) { - throw new egw_exception_not_found("'$path' NOT found!"); + throw new Api\Exception\NotFound("'$path' NOT found!"); } // check if file has been shared before, with identical attributes if (($mode != self::LINK || isset($path2tmp[$path])) && @@ -447,29 +461,29 @@ class egw_sharing if ($mode == 'link') { $user_tmp = '/home/'.$GLOBALS['egw_info']['user']['account_lid'].'/.tmp'; - if (!egw_vfs::file_exists($user_tmp) && !egw_vfs::mkdir($user_tmp)) + if (!Vfs::file_exists($user_tmp) && !Vfs::mkdir($user_tmp)) { - throw new egw_exception_assertion_failed("Could NOT create temp. directory '$user_tmp'!"); + throw new Api\Exception\AssertionFailed("Could NOT create temp. directory '$user_tmp'!"); } $n = 0; do { - $tmp_file = egw_vfs::concat($user_tmp, ($n?$n.'.':'').egw_vfs::basename($name)); + $tmp_file = Vfs::concat($user_tmp, ($n?$n.'.':'').Vfs::basename($name)); } - while(!(is_dir($path) && egw_vfs::mkdir($tmp_file) || - !is_dir($path) && (!egw_vfs::file_exists($tmp_file) && ($fp = egw_vfs::fopen($tmp_file, 'x')) || + while(!(is_dir($path) && Vfs::mkdir($tmp_file) || + !is_dir($path) && (!Vfs::file_exists($tmp_file) && ($fp = Vfs::fopen($tmp_file, 'x')) || // do not copy identical files again to users tmp dir, just re-use them - egw_vfs::file_exists($tmp_file) && egw_vfs::compare(egw_vfs::PREFIX.$tmp_file, $path))) && $n++ < 100); + Vfs::file_exists($tmp_file) && Vfs::compare(Vfs::PREFIX.$tmp_file, $path))) && $n++ < 100); if ($n >= 100) { - throw new egw_exception_assertion_failed("Could NOT create temp. file '$tmp_file'!"); + throw new Api\Exception\AssertionFailed("Could NOT create temp. file '$tmp_file'!"); } if ($fp) fclose($fp); - if (is_dir($path) && !egw_vfs::copy_files(array($path), $tmp_file) || - !is_dir($path) && !copy($path, egw_vfs::PREFIX.$tmp_file)) + if (is_dir($path) && !Vfs::copy_files(array($path), $tmp_file) || + !is_dir($path) && !copy($path, Vfs::PREFIX.$tmp_file)) { - throw new egw_exception_assertion_failed("Could NOT create temp. file '$tmp_file'!"); + throw new Api\Exception\AssertionFailed("Could NOT create temp. file '$tmp_file'!"); } // store temp. path in session, to be able to add more recipients $path2tmp[$path] = $tmp_file; @@ -490,7 +504,7 @@ class egw_sharing try { self::$db->insert(self::TABLE, $share = array( 'share_token' => self::token(), - 'share_path' => egw_vfs::parse_url($path, PHP_URL_PATH), + 'share_path' => Vfs::parse_url($path, PHP_URL_PATH), 'share_owner' => $GLOBALS['egw_info']['user']['account_id'], 'share_with' => implode(',', (array)$recipients), 'share_created' => time(), @@ -499,7 +513,7 @@ class egw_sharing $share['share_id'] = self::$db->get_last_insert_id(self::TABLE, 'share_id'); break; } - catch(egw_exception_db $e) { + catch(Api\Db\Exception $e) { if ($i++ > 3) throw $e; unset($e); } @@ -509,9 +523,9 @@ class egw_sharing } /** - * so_sql instance for egw_sharing table + * Api\Storage\Base instance for egw_sharing table * - * @var so_sql + * @var Api\Storage\Base */ protected static $so; @@ -522,7 +536,7 @@ class egw_sharing { if (!isset(self::$so)) { - self::$so = new so_sql('phpgwapi', self::TABLE, null, '', true); + self::$so = new Api\Storage\Base('phpgwapi', self::TABLE, null, '', true); self::$so->set_times('string'); } return self::$so; @@ -567,7 +581,7 @@ class egw_sharing // if not delete them foreach($tmp_paths as $path) { - egw_vfs::remove($path); + Vfs::remove($path); } } return $deleted; @@ -586,7 +600,7 @@ class egw_sharing public static function tmp_cleanup() { if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db; - egw_vfs::$is_root = true; + Vfs::$is_root = true; try { $cols = array( @@ -607,7 +621,7 @@ class egw_sharing "share_path LIKE '/home/%/.tmp/%'", ), __LINE__, __FILE__, false, 'GROUP BY share_path '.$having) as $row) { - egw_vfs::remove($row['share_path']); + Vfs::remove($row['share_path']); if ($group_concat) { @@ -629,10 +643,10 @@ class egw_sharing } } } - catch (Exception $e) { + catch (\Exception $e) { unset($e); } - egw_vfs::$is_root = false; + Vfs::$is_root = false; } /** @@ -661,7 +675,7 @@ if (file_exists(__DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php')) { require_once __DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php'; - class egw_sharing_filemanager extends filemanager_ui + class SharingUi extends filemanager_ui { /** * Get the configured start directory for the current user diff --git a/phpgwapi/inc/class.vfs_webdav_server.inc.php b/api/src/Vfs/WebDAV.php similarity index 86% rename from phpgwapi/inc/class.vfs_webdav_server.inc.php rename to api/src/Vfs/WebDAV.php index b32fc55441..4e3fdc5c97 100644 --- a/phpgwapi/inc/class.vfs_webdav_server.inc.php +++ b/api/src/Vfs/WebDAV.php @@ -7,24 +7,30 @@ * @link http://www.egroupware.org * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api - * @subpackage vfs + * @subpackage webdav * @author Ralf Becker * @author Hartmut Holzgraefe original HTTP/WebDAV/Server/Filesystem class, of which some code is used * @version $Id$ */ -if (strpos(ini_get('include_path'), EGW_API_INC) === false) -{ - ini_set('include_path', EGW_API_INC.PATH_SEPARATOR.ini_get('include_path')); -} -require_once('HTTP/WebDAV/Server/Filesystem.php'); +namespace EGroupware\Api\Vfs; + +require_once dirname(__DIR__).'/WebDAV/Server/Filesystem.php'; + +use HTTP_WebDAV_Server_Filesystem; +use HTTP_WebDAV_Server; +use EGroupware\Api\Vfs; +use EGroupware\Api; + +// old, not yet ported api classes +use common; // egw_exit /** * FileManger - WebDAV access using the new stream wrapper VFS interface * * Using modified PEAR HTTP/WebDAV/Server/Filesystem class in API dir */ -class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem +class WebDAV extends HTTP_WebDAV_Server_Filesystem { /** * Realm of eGW's WebDAV server @@ -39,7 +45,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem * * @var string */ - var $base = egw_vfs::PREFIX; + var $base = Vfs::PREFIX; /** * Debug level: 0 = nothing, 1 = function calls, 2 = more info, eg. complete $_SERVER array @@ -90,11 +96,11 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem { // recursive delete the directory try { - $deleted = egw_vfs::remove($options['path']); + $deleted = Vfs::remove($options['path']); $ret = !empty($deleted[$options['path']]); - //error_log(__METHOD__."() egw_vfs::remove($options[path]) returned ".array2string($deleted)." --> ".array2string($ret)); + //error_log(__METHOD__."() Vfs::remove($options[path]) returned ".array2string($deleted)." --> ".array2string($ret)); } - catch (Exception $e) { + catch (\Exception $e) { return '403 Forbidden: '.$e->getMessage(); } } @@ -120,7 +126,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem function MKCOL($options) { $path = $this->_unslashify($this->base .$options["path"]); - $parent = egw_vfs::dirname($path); + $parent = Vfs::dirname($path); if (!file_exists($parent)) { return "409 Conflict"; @@ -225,7 +231,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem } } else { if (is_dir($source) && $options['depth'] == 'infinity') { - $files = egw_vfs::find($source,array('depth' => true,'url' => true)); // depth=true: return dirs first, url=true: allow urls! + $files = Vfs::find($source,array('depth' => true,'url' => true)); // depth=true: return dirs first, url=true: allow urls! } else { $files = array($source); } @@ -289,41 +295,41 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem $info['props'] = array(); // no special beautified displayname here ... - $info['props'][] = HTTP_WebDAV_Server::mkprop ('displayname', egw_vfs::basename(self::_unslashify($info['path']))); + $info['props'][] = self::mkprop ('displayname', Vfs::basename(self::_unslashify($info['path']))); // creation and modification time - $info['props'][] = HTTP_WebDAV_Server::mkprop ('creationdate', filectime($fspath)); - $info['props'][] = HTTP_WebDAV_Server::mkprop ('getlastmodified', filemtime($fspath)); + $info['props'][] = self::mkprop ('creationdate', filectime($fspath)); + $info['props'][] = self::mkprop ('getlastmodified', filemtime($fspath)); // Microsoft extensions: last access time and 'hidden' status - $info["props"][] = HTTP_WebDAV_Server::mkprop("lastaccessed", fileatime($fspath)); - $info["props"][] = HTTP_WebDAV_Server::mkprop("ishidden", egw_vfs::is_hidden($fspath)); + $info["props"][] = self::mkprop("lastaccessed", fileatime($fspath)); + $info["props"][] = self::mkprop("ishidden", Vfs::is_hidden($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', array( - HTTP_WebDAV_Server::mkprop('collection', ''))); - $info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontenttype', 'httpd/unix-directory'); + $info['props'][] = self::mkprop ('resourcetype', array( + self::mkprop('collection', ''))); + $info['props'][] = self::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)); + $info['props'][] = self::mkprop ('resourcetype', ''); + if (Vfs::is_readable($path)) { + $info['props'][] = self::mkprop ('getcontenttype', Vfs::mime_content_type($path)); } else { error_log(__METHOD__."($path) $fspath is not readable!"); - $info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontenttype', 'application/x-non-readable'); + $info['props'][] = self::mkprop ('getcontenttype', 'application/x-non-readable'); } - $info['props'][] = HTTP_WebDAV_Server::mkprop ('getcontentlength', filesize($fspath)); + $info['props'][] = self::mkprop ('getcontentlength', filesize($fspath)); } // generate etag from inode (sqlfs: fs_id), modification time and size $stat = stat($fspath); - $info['props'][] = HTTP_WebDAV_Server::mkprop('getetag', '"'.$stat['ino'].':'.$stat['mtime'].':'.$stat['size'].'"'); + $info['props'][] = self::mkprop('getetag', '"'.$stat['ino'].':'.$stat['mtime'].':'.$stat['size'].'"'); /* returning the supportedlock property causes Windows DAV provider and Konqueror to not longer work ToDo: return it only if explicitly requested ($options['props']) // supportedlock property - $info['props'][] = HTTP_WebDAV_Server::mkprop('supportedlock',' + $info['props'][] = self::mkprop('supportedlock',' @@ -384,13 +390,13 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem $_path = $info['path']; if (!$n && $info['path'] != '/' && substr($info['path'],-1) == '/') $_path = substr($info['path'],0,-1); - // need to encode path again, as $info['path'] is NOT encoded, but egw_vfs::(stat|propfind) require it + // need to encode path again, as $info['path'] is NOT encoded, but Vfs::(stat|propfind) require it // otherwise pathes containing url special chars like ? or # will not stat - $path = egw_vfs::encodePath($_path); + $path = Vfs::encodePath($_path); $path2n[$path] = $n; // adding some properties used instead of regular DAV times - if (($stat = egw_vfs::stat($path))) + if (($stat = Vfs::stat($path))) { $fileprops =& $files['files'][$path2n[$path]]['props']; foreach(self::$auto_props as $attr => $props) @@ -414,14 +420,14 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem } } } - if ($path2n && ($path2props = egw_vfs::propfind(array_keys($path2n),null))) + if ($path2n && ($path2props = Vfs::propfind(array_keys($path2n),null))) { foreach($path2props as $path => $props) { $fileprops =& $files['files'][$path2n[$path]]['props']; foreach($props as $prop) { - if ($prop['ns'] == egw_vfs::DEFAULT_PROP_NAMESPACE && $prop['name'][0] == '#') // eGW's customfields + if ($prop['ns'] == Vfs::DEFAULT_PROP_NAMESPACE && $prop['name'][0] == '#') // eGW's customfields { $prop['ns'] .= 'customfields/'; $prop['name'] = substr($prop['name'],1); @@ -437,13 +443,13 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem /** * Used eg. by get * - * @todo replace all calls to _mimetype with egw_vfs::mime_content_type() + * @todo replace all calls to _mimetype with Vfs::mime_content_type() * @param string $path * @return string */ function _mimetype($path) { - return egw_vfs::mime_content_type($path); + return Vfs::mime_content_type($path); } /** @@ -454,7 +460,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem */ function _is_readable($fspath) { - return egw_vfs::is_readable($fspath); + return Vfs::is_readable($fspath); } /** @@ -465,7 +471,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem */ function _is_writable($fspath) { - return egw_vfs::is_writable($fspath); + return Vfs::is_writable($fspath); } /** @@ -480,7 +486,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem */ function PROPPATCH(&$options) { - $path = translation::convert($options['path'],'utf-8'); + $path = Api\Translation::convert($options['path'],'utf-8'); foreach ($options['props'] as $key => $prop) { $attributes = array(); @@ -492,14 +498,14 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem { case 'srt_modifiedtime': case 'getlastmodified': - egw_vfs::touch($path,strtotime($prop['val'])); + Vfs::touch($path,strtotime($prop['val'])); break; //case 'srt_creationtime': // no streamwrapper interface / php function to set the ctime currently //$attributes['created'] = strtotime($prop['val']); //break; default: - if (!egw_vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; + if (!Vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; break; } break; @@ -509,7 +515,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem { // allow netdrive to change the modification time case 'getlastmodified': - egw_vfs::touch($path,strtotime($prop['val'])); + Vfs::touch($path,strtotime($prop['val'])); break; // not sure why, the filesystem example of the WebDAV class does it ... default: @@ -522,23 +528,23 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem switch($prop['name']) { case 'Win32LastModifiedTime': - egw_vfs::touch($path,strtotime($prop['val'])); + Vfs::touch($path,strtotime($prop['val'])); break; case 'Win32CreationTime': // eg. "Wed, 14 Sep 2011 15:48:26 GMT" case 'Win32LastAccessTime': case 'Win32FileAttributes': // not sure what that is, it was always "00000000" default: - if (!egw_vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; + if (!Vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; break; } break; - case egw_vfs::DEFAULT_PROP_NAMESPACE.'customfields/': // eGW's customfields - $prop['ns'] = egw_vfs::DEFAULT_PROP_NAMESPACE; + case Vfs::DEFAULT_PROP_NAMESPACE.'customfields/': // eGW's customfields + $prop['ns'] = Vfs::DEFAULT_PROP_NAMESPACE; $prop['name'] = '#'.$prop['name']; // fall through default: - if (!egw_vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; + if (!Vfs::proppatch($path,array($prop))) $options['props'][$key]['status'] = '403 Forbidden'; break; } if ($this->debug) $props[] = '('.$prop['ns'].')'.$prop['name'].'='.$prop['val']; @@ -570,7 +576,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem // 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']), + if (($ret = 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'; @@ -587,7 +593,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem function UNLOCK(&$options) { if ($this->debug) error_log(__METHOD__.'('.str_replace(array("\n",' '),'',print_r($options,true)).')'); - return egw_vfs::unlock($options['path'],$options['token']) ? '204 No Content' : '409 Conflict'; + return Vfs::unlock($options['path'],$options['token']) ? '204 No Content' : '409 Conflict'; } /** @@ -598,7 +604,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem */ function checkLock($path) { - return egw_vfs::checkLock($path); + return Vfs::checkLock($path); } /** @@ -612,7 +618,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem function GetDir($fspath, &$options) { // add a content-type header to overwrite an existing default charset in apache (AddDefaultCharset directiv) - header('Content-type: text/html; charset='.translation::charset()); + header('Content-type: text/html; charset='.Api\Translation::charset()); parent::GetDir($fspath, $options); } @@ -650,7 +656,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem if (($ok = parent::GET($options))) { // mitigate risks of serving javascript or css from our domain - html::safe_content_header($options['stream'], $options['path'], $options['mimetype'], $options['size'], false, + Api\Header\Content::safe($options['stream'], $options['path'], $options['mimetype'], $options['size'], false, $this->force_download, true); // true = do not send content-type and content-length header, but modify values if (!is_resource($options['stream'])) @@ -678,7 +684,7 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem { return $ret; // no collection } - header('Content-type: text/html; charset='.translation::charset()); + header('Content-type: text/html; charset='.Api\Translation::charset()); echo "\n\n\t".'EGroupware WebDAV server '.htmlspecialchars($options['path'])."\n"; echo "\t\n"; echo "\t