diff --git a/filemanager/tests/benchmark_dav.php b/filemanager/tests/benchmark_dav.php new file mode 100644 index 0000000000..a76caf910f --- /dev/null +++ b/filemanager/tests/benchmark_dav.php @@ -0,0 +1,61 @@ + "phpwebhosting", + "noheader" => False, + "noappheader" => False, + "enable_vfs_class" => True); + +include("../../header.inc.php"); + + function getmicrotime() + { + list($usec, $sec) = explode(" ",microtime()); + return ((float)$usec + (float)$sec); + } + + function stats($array) + { + $mean = array_sum($array)/count($array); + $a = 0; + foreach ($array as $value) + { + $a += ($value - $mean)*($value - $mean); + } + $std = sqrt($a/count($array)); + $error = $std/sqrt(count($array)); + echo "mean time: $mean error: +-$error"; + } + echo 'Benchmarking vfs::ls
'; + $times = array(); + $phpgw->vfs->cd(); + for ($i=0;$i<20; $i++) + { + $phpgw->vfs->dav_client->cached_props = array(); + $time1 = getmicrotime(); + $result = $phpgw->vfs->ls (array ('string' => '')); + $time = getmicrotime() - $time1; + $times[] = $time; + echo "run $i: $time
"; + //sleep(1); + flush(); + } + stats($times); + + echo '
Benchmarking dav_client::get_properties
'; + $times = array(); + $phpgw->vfs->cd(); + for ($i=0;$i<20; $i++) + { + $phpgw->vfs->dav_client->cached_props = array(); + $time1 = getmicrotime(); + $result = $phpgw->vfs->dav_client->get_properties('/home/sim'); + $time = getmicrotime() - $time1; + $times[] = $time; + echo "run $i: $time
"; + flush(); + } + stats($times); + + +?> diff --git a/filemanager/tests/test_dav.php b/filemanager/tests/test_dav.php new file mode 100644 index 0000000000..f3f17e49fc --- /dev/null +++ b/filemanager/tests/test_dav.php @@ -0,0 +1,531 @@ +$string "; +/* html_table_col_end(); + html_table_col_begin();*/ + echo " OK"; +/* html_table_col_end(); + html_table_row_end();*/ +} +function fail($string){ +/* html_table_row_begin(); + html_table_col_begin();*/ + echo "

$string "; +/* html_table_col_end(); + html_table_col_begin();*/ + echo " FAILED!

"; +/* html_table_col_end(); + html_table_row_end(); */ +} + +$phpgw_info["flags"] = array("currentapp" => "filemanager", + "noheader" => False, + "noappheader" => False, + "enable_vfs_class" => True); + +include("../../header.inc.php"); + +html_text('VFS_DAV tests:'); +html_break (1); +html_text_italic (PHP_OS . " - " . $phpgw_info["server"]["db_type"] . " - " . PHP_VERSION . " - " . $phpgw->vfs->basedir); +html_break (1); +//html_table_begin(); + + +$sep = SEP; +$user = $phpgw->vfs->working_lid; +$homedir = $phpgw->vfs->fakebase . "/" . $user; +$realhomedir = preg_replace ("|/|", $sep, $homedir); +$filesdir = $phpgw->vfs->basedir; +$currentapp = $phpgw_info["flags"]["currentapp"]; +$time1 = time(); + +echo ' override locks : ';print_r($phpgw->vfs->override_locks); + ### + # write test + + $phpgw->vfs->cd (); + $testfile = 'sdhdsjjkldggfsbhgbnirooaqojsdkljajklvagbytoi-test'; + $teststring = 'delete me' ; + if (!$result = $phpgw->vfs->write (array ('string' => $testfile, + 'content' => $teststring + ))) + { + fail( __LINE__." failed writing file!"); + } + else + { + ok("write"); + } + +#read + $phpgw->vfs->cd (); + $result = $phpgw->vfs->read (array ('string' => $testfile, 'noview' => true )); + if (!$result==$teststring) + { + fail( __LINE__." failed reading file!"); + } + else + { + ok(" read"); + } + + ### + # ls test + + $result1 = $phpgw->vfs->ls (array ('string' => $testfile )); + + if (!count($result1)) + { + fail(__LINE__." failed listing file!"); + } + else + { + ok(" ls : known file"); + } +//list the parent dir + + $result = $phpgw->vfs->ls (array ('string' => '')); + foreach ($result as $file) + { + if ($testfile == $file['name']) + { + $found = true; + break; + } + } + if (!$found) + { + fail(__LINE__." failed listing file!"); + } + else + { + ok(" ls : parent"); + } + $found = false; + foreach ($result as $file) + { + if ($result1[0]['directory'] == $file['full_name']) + { + $found = true; + break; + } + } + if ($found) + { + fail(__LINE__." parent is present in its own listing!"); + } + else + { + ok(" ls : parent self reference"); + } +# getsize + $phpgw->vfs->cd (); + $result = $phpgw->vfs->get_size (array ('string' => $testfile )); + $len = strlen($teststring); + if (!($result== $len)) + { + fail(__LINE__." failed getting size of file result $result strlen $len"); + } + else + { + ok("get_size"); + } + +#filetype + $phpgw->vfs->cd (); + $result = $phpgw->vfs->file_type(array ('string' => $testfile )); + $len = strlen($teststring); + if (!($result== 'application/octet-stream')) + { + fail(__LINE__." failed getting file type $result"); + } + else + { + ok("file_type"); + } + + +#file_exists + $phpgw->vfs->cd (); + $result = $phpgw->vfs->file_exists(array ('string' => $testfile )); + if (!$result) + { + fail(__LINE__." file_exist failed: $result"); + } + else + { + ok("file_exists"); + } + +#lock + $phpgw->vfs->cd (); + $result = $phpgw->vfs->lock (array ('string' => $testfile )); + if (!$result) + { + fail(__LINE__."failed locking file!"); + } + else + { + ok(" lock"); + } + + + $ls_array = $GLOBALS['phpgw']->vfs->ls (array ( + 'string' => $testfile, + 'relatives' => array (RELATIVE_ALL), + 'checksubdirs' => False + ) + ); + if (!count($ls_array[0]['locks'])) + { + fail(__LINE__."after locking file no locks exist!"); + } + else + { + ok(" lock: after locking lock exists."); + } + $lock = end($ls_array[0]['locks']); + $tokens = end($lock['lock_tokens']); + $token = $tokens['name']; + //write should now fail + $result = $phpgw->vfs->write (array ('string' => $testfile, + 'content' => 'delete me' + )); + if ($result) + { + fail(__LINE__."I can write a supposidly locked file!"); + } + else + { + ok("lock: after locking write fails"); + } + + $phpgw->vfs->add_lock_override(array ('string' => $testfile)); + $result = $phpgw->vfs->write (array ('string' => $testfile, + 'content' => 'delete me' + )); + if (!$result) + { + fail(__LINE__."I cant write a locked file after overriding the lock!"); + } + else + { + ok("lock: after lock override write succeeds"); + } +### +# unlock test + + $phpgw->vfs->cd (); + $result = $phpgw->vfs->unlock (array ('string' => $testfile ), $token); + if (!$result) + { + fail( __LINE__."failed unlocking file!"); + } + else + { + OK("unlock"); + } + +#server side copy + + $phpgw->vfs->cd (); + $result = $phpgw->vfs->cp(array ('from' => $testfile, + 'to' => $testfile.'2', + 'relatives' => array (RELATIVE_ALL, RELATIVE_ALL) + )); + if (!$result) + { + fail(__LINE__." failed copying! returned: $result"); + } + else + { + ok("server-side copy"); + } + $result = $phpgw->vfs->file_exists(array ('string' => $testfile.'2' + )); + if (!$result) + { + fail(__LINE__." after copy, target doesnt exist!"); + } + else + { + ok("server-side copy : test for target"); + } + $result = $phpgw->vfs->read (array ('string' => "$testfile".'2', + 'noview' => true, + 'relatives' => array (RELATIVE_ALL) + )); + if (!$result==$teststring) + { + fail( __LINE__."after copy, read returned '$result' not '$teststring' "); + } + else + { + ok(" server-side copy: read target"); + } + $result = $phpgw->vfs->delete(array ('string' => $testfile.'2' + )); + + if (!$result) + { + fail(__LINE__." failed copying! delete copied file returned: $result"); + } + else + { + ok("server-side copy : delete target"); + } + +#remote -> local copy + $phpgw->vfs->cd (); + echo "
";
+	$result = $phpgw->vfs->cp(array ('from' => $testfile,
+				'to' => "/tmp/$testfile".'2',
+				'relatives'	=> array (RELATIVE_ALL, RELATIVE_NONE | VFS_REAL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed remote->local copying! returned: $result"); + } + else + { + ok("remote->local copy"); + } + echo "
";
+	$result = $phpgw->vfs->file_exists(array ('string' => "/tmp/$testfile".'2',
+	'relatives'	=> array (RELATIVE_NONE | VFS_REAL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." after remote->local copy, target doesnt exist!"); + } + else + { + ok("remote->local copy : test for target"); + } + $phpgw->vfs->cd (); + echo "
";
+	$result = $phpgw->vfs->read (array ('string' => "/tmp/$testfile".'2', 
+		'noview' => true,
+		'relatives'	=> array (RELATIVE_NONE | VFS_REAL)
+			));
+	echo "
"; + if (!$result==$teststring) + { + fail( __LINE__."after remote->local copy, returned $result"); + } + else + { + ok(" remote->local copy: read target"); + } + echo "
";	
+	$result = $phpgw->vfs->delete(array ('string' => "/tmp/$testfile".'2',
+	'relatives'	=> array (RELATIVE_NONE | VFS_REAL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed copying! delete copied file returned: $result"); + } + else + { + ok("remote->local copy : delete target"); + } + +#move + $phpgw->vfs->cd (); + echo "
";
+	$result = $phpgw->vfs->mv(array ('from' => $testfile,
+				'to' => $testfile.'2',
+				'relatives'	=> array (RELATIVE_ALL, RELATIVE_ALL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("server-side move"); + } + $result = $phpgw->vfs->file_exists(array ('string' => $testfile, + )); + if ($result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("server-side move : test for source"); + } + $result = $phpgw->vfs->file_exists(array ('string' => $testfile.'2', + )); + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("server-side move : test for target"); + } + echo "
";
+	$result = $phpgw->vfs->read (array ('string' => $testfile.'2', 
+		'noview' => true,
+		'relatives'	=> array (RELATIVE_ALL)
+			));
+	echo "
"; + if (!$result==$teststring) + { + fail( __LINE__."after move, read returned '$result' not '$teststring' "); + } + else + { + ok(" server-side move: read target"); + } + + echo "
";	
+	$result = $phpgw->vfs->mv(array ('from' => $testfile.'2',
+				'to' => $testfile,
+				'relatives'	=> array (RELATIVE_ALL, RELATIVE_ALL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("server-side move : move back"); + } + +#remote->local move + echo "
";
+	$phpgw->vfs->cd ();
+	$result = $phpgw->vfs->mv(array ('from' => $testfile,
+				'to' => '/tmp/'.$testfile.'2',
+				'relatives'	=> array (RELATIVE_ALL, RELATIVE_NONE | VFS_REAL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("remote->local move"); + } + $result = $phpgw->vfs->file_exists(array ('string' => $testfile, + )); + if ($result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("remote->local move : test for source"); + } + $result = $phpgw->vfs->file_exists(array ('string' => '/tmp/'.$testfile.'2', + 'relatives' => array(RELATIVE_NONE | VFS_REAL) + )); + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("remote->local move : test for target"); + } + + $result = $phpgw->vfs->read (array ('string' => '/tmp/'.$testfile.'2', + 'noview' => true, + 'relatives' => array(RELATIVE_NONE | VFS_REAL) + )); + if (!$result==$teststring) + { + fail( __LINE__."after move, read returned '$result' not '$teststring' "); + } + else + { + ok("remote->local move: read target"); + } + + echo "
";
+	$result = $phpgw->vfs->mv(array ('from' => '/tmp/'.$testfile.'2',
+				'to' => $testfile,
+				'relatives'	=> array (RELATIVE_NONE | VFS_REAL, RELATIVE_ALL)
+				));
+	echo "
"; + if (!$result) + { + fail(__LINE__." failed moving! returned: $result"); + } + else + { + ok("server-side move : move back"); + } + + +### +# delete test + + $phpgw->vfs->cd (); + $result = $phpgw->vfs->delete(array ('string' => $testfile )); + if (!$result) + { + fail(__LINE__."failed deleting file! returned: $result"); + } + else + { + ok("delete"); + } + +### +# mkdir test + + $phpgw->vfs->cd (); + $result = $phpgw->vfs->mkdir(array ('string' => $testfile )); + if (!$result) + { + fail(__LINE__."failed creating collection! returned: $result"); + } + else + { + ok("mkdir"); + } + + $result = $phpgw->vfs->write (array ('string' => $testfile.'/'.$testfile.'2', + 'content' => $teststring + )); + + if (!$result) + { + fail( __LINE__." failed writing file into new dir!"); + } + else + { + ok("mkdir : write into dir"); + } +### +# rm dir test + + $phpgw->vfs->cd (); + $result = $phpgw->vfs->rm(array ('string' => $testfile )); + if (!$result) + { + fail(__LINE__." failed deleting collection! returned: $result"); + } + else + { + ok("delete dir"); + } + + //html_table_end(); + $time = time() - $time1; + html_text("Done in $time s"); + html_page_close (); + +?> diff --git a/phpgwapi/doc/vfs/vfs_dav/README b/phpgwapi/doc/vfs/vfs_dav/README new file mode 100644 index 0000000000..5b3de61473 --- /dev/null +++ b/phpgwapi/doc/vfs/vfs_dav/README @@ -0,0 +1,122 @@ +vfs_dav: WebDAV support for phpGroupWare +---------------------------------------- + +WARNING::This is still alpha-quality code, and may delete all your files, +corrupt all your data, and run away with your daughter. Dont use it unless +you know what your doing. + + +This package contains classes and patches implementing a virtual filesystem +for phpGroupWare (0.9.14) that uses a WebDAV file repository as a +storage area. This allows you to store your files online in phpgroupware, +in a way that cooperates well with other web applications (for instance, +in Windows you can then access your files as a "web folder", and similarly +KDE, Gnome, MacOSX, and a multitude of applications all include some way of +browsing files on a WebDAV share) + +Features +-------- +- (should be) fully compatible with the WebDAV standard, as specified in + RFC2518 http://www.ietf.org/rfc/rfc2518.txt (NB : this doesn't imply that it + is :) only that I consider it a bug if it isn't ) +- Maps vfs properties (eg creator_id etc) to the Dublin Core metadata + specification (http://dublincore.org/) (eg "author") where possible, and + stores them as DAV properties +- Limited Access control support - when you create a dir in groupware, it + puts a .htaccess file in it with "limit user " in it. Combined + with an Apache module authenticating against your phpgroupware account + database, this provides reasonable user-security (proper ACL support is + trickier to implement though :( + +TODO: +----- +- DELTA-V versioning extensions, using the subversion DELTA-V server +- Journalling -> Versioning. Journalling is currently unimplemented +- Locking (phpGroupWare doesn't really have any locking mechanism yet) +- ACL support for users accessing through an external webdav client. + +Installation +------------ +To install: + +1/ Setup a WebDAV server - currently this code has only been well tested using + Apache's mod_dav (http://www.webdav.org/mod_dav/). To setup mod_dav ensure + that you have the module installed correctly ( RTFM :) and create a virtual + host (eg files.yourdomain.com) something like this: + + + AccessFileName .htaccess + ServerAdmin webmaster@yourdomain.com + DocumentRoot /var/files + + AllowOverride All + Options +Indexes + DAV on + DirectoryIndex / + RemoveHandler .php .php4 .php3 .phtml + + + #This ensures phpgroupware can modify .htaccess files + order deny,allow + deny from all + #make sure your phpgroupware server is included here. + allow from localhost .localdomain 192.168. + + ServerName files.yourdomain.com + ErrorLog logs/dav_err + CustomLog logs/dav_acc combined + + +2/ On the setup page (phpgroupware/setup/config.php) specify + the WebDAV server URL (eg http://files.yourdomain.com ) in the: "Full path + for users and groups files" text area, and select DAV in: + "Select where you want to store/retrieve filesystem information" + combo. If your file repository supports SSL you might want to enter + 'https://files.yourdomain.com' instead - note that phpGroupWare itself wont + use SSL to access the repository, but when it redirects the users browser to + the repository it will use the secure https url. + +3/ Make sure your WebDAV repository contains a "home" directory (important!) + So if your WebDAV directory is /var/files, you would need: + /var/files/ + /var/files/home/ + +4/ To enable authentication you must use a third-party Apache authentication + module. Which you use depends on how you have setup authentication in + phpGroupWare - for instance if you use an SQL DB (the default) then set up + mod_auth_pgsql (http://www.giuseppetanzilli.it/mod_auth_pgsql/) or + mod_auth_mysql (http://modauthmysql.sourceforge.net/) + An example .htaccess file is included for postgresql - mysql would be + similar. Your file repository also needs to be configured to allow + phpGroupWare to write .htaccess files (the setup in (3) will allow this) + + Note that using an Apache module for authentication is not strictly + required in order to use WebDAV within phpGroupWare. + +Authors +------- +Jonathon Sim for Zeald Ltd (http://zeald.com), Paolo Andreetto +. Also contains code from the Net_HTTP_Client PHP class +(http://lwest.free.fr/doc/php/lib/net_http_client-en.html) by Leo West +. + +License +------- +By using this software you agree to the terms of the GNU LGPL: this is the same +license that the phpGroupWare API is distributed under: + +Copyright (C) 2002 Zeald Ltd. + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, +or any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this library; if not, write to the Free Software Foundation, +Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA diff --git a/phpgwapi/doc/vfs/vfs_dav/htaccess.pgsql_example b/phpgwapi/doc/vfs/vfs_dav/htaccess.pgsql_example new file mode 100644 index 0000000000..15e3b016e2 --- /dev/null +++ b/phpgwapi/doc/vfs/vfs_dav/htaccess.pgsql_example @@ -0,0 +1,21 @@ + php_flag engine off + RemoveHandler cgi-script .cgi .pl + RemoveType application/x-httpd-php .php .php3 + RemoveType application/x-httpd-php-source .phps + +AuthName "Groupware File Repository" +AuthType basic +Auth_PG_cache_passwords on +Auth_PG_host orion.zeald.com +Auth_PG_database gw_zeald +Auth_PG_user zeald +Auth_PG_pwd password +Auth_PG_pwd_table phpgw_accounts +Auth_PG_grp_table phpgw_accounts +Auth_PG_uid_field account_lid +Auth_PG_pwd_field account_pwd +Auth_PG_gid_field account_groups +Auth_PG_hash_type MD5 +Auth_PG_pwd_whereclause " AND account_status='A' AND account_type='u' AND account_expires * + * Provides methods for manipulating an RFC 2518 DAV repository * + * Copyright (C) 2002 Zeald Ltd * + * -------------------------------------------------------------------------* + * This library is part of the phpGroupWare API * + * http://www.phpgroupware.org/api * + * ------------------------------------------------------------------------ * + * This library is free software; you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as published by * + * the Free Software Foundation; either version 2.1 of the License, * + * or any later version. * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU Lesser General Public License for more details. * + * You should have received a copy of the GNU Lesser General Public License * + * along with this library; if not, write to the Free Software Foundation, * + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * + \**************************************************************************/ + + /* $Id$ */ + + /*At the moment much of this is simply a wrapper around the NET_HTTP_Client class, + with some other methods for parsing the returned XML etc + Ideally this will eventually use groupware's inbuilt HTTP class + */ + + define ('DEBUG_DAV_CLIENT', 0); + define ('DEBUG_DAV_XML', 0); + define ('DEBUG_CACHE', 0); + + +############################################################## +# 'Private' classes - these are only used internally and should +# not be used by external code +############################################################## + + /* + PHP STILL doesnt have any sort of stable DOM parser. So lets make our + own XML parser, that parses XML into a tree of arrays (I know, it could do + something resembling DOM, but it doesnt!) + */ + class xml_tree_parser + { + var $namespaces; + var $current_element; + var $num = 1; + var $tree = NULL; + + /* + This is the only end-user function in the class. Call parse with an XML string, and + you will get back the tree of 'element' arrays + */ + function parse($xml_string) + { + $this->xml_parser = xml_parser_create(); + xml_set_element_handler($this->xml_parser,array(&$this,"start_element"),array(&$this,"end_element")); + xml_set_character_data_handler($this->xml_parser,array(&$this,"parse_data")); + + $this->parser_result=array(); + $this->xml = $xml_string; + xml_parse($this->xml_parser,$xml_string); + xml_parser_free($this->xml_parser); + if (DEBUG_DAV_XML) + { + echo '
'.htmlentities($xml_string).'
'; + echo 'parsed to:' ; $this->print_tree(); + } + return $this->tree; + } + + //a useful function for debug output - will print the tree after parsing + function print_tree($element=NULL, $prefix='|', $processed=array()) + { + + if ($processed[$element['id']]) + { + echo '***RECURSION!!***
'; + die(); + } + else + { + $processed[$element['id']] = true; + } + + if ($element == NULL) + { + $element = $this->tree; + } + echo $prefix.$element['namespace'].':'.$element['name'].' '.$element['start'].'->'.$element['end'].'
'; + $prefix .= '-->'; + if ($element['data']) + { + echo $prefix.$element['data'].'
'; + } + + foreach ($element['children'] as $id=>$child) + { + $this->print_tree($child, $prefix, &$processed); + } + + } + + //called by the xml parser at the start of an element, it creates that elements node in the tree + function start_element($parser,$name,$attr) + { + + if (preg_match('/(.*):(.*)/', $name, $matches)) + { + $ns = $this->namespaces[$matches[1]]; + $element = $matches[2]; + } + else + { + $element = $name; + } + + if ($this->tree == NULL) + { + $this->tree = array( + 'namespace'=>$ns, + 'name' => $element, + 'attributes' => $attr, + 'data' => '', + 'children' => array(), + 'parent' => NULL, + 'type' => 'xml_element', + 'id' => $this->num + ); + $this->current_element = &$this->tree; + } + else + { + $parent = &$this->current_element; + $parent['children'][$this->num]=array( + 'namespace'=>$ns, + 'name' => $element, + 'attributes' => $attr, + 'data' => '', + 'children' => array(), + 'parent' => &$parent, + 'type' => 'xml_element', + 'id' => $this->num + ); + $this->current_element = &$parent['children'][$this->num]; + } + $this->num++; + $this->current_element['start'] = xml_get_current_byte_index($parser); + foreach ($attr as $name => $value) + { + if (ereg('^XMLNS:(.*)', $name, $matches) ) + { + $this->namespaces[$matches[1]] = $value; + } + } + } + + //at the end of an element, stores the start and end positions in the xml stream, and moves up the tree + function end_element($parser,$name) + { + $curr = xml_get_current_byte_index($parser); + $this->current_element['end'] =strpos($this->xml, '>', $curr); + $this->current_element = &$this->current_element['parent']; + + } + + //if there is a CDATA element, puts it into the parent elements node + function parse_data($parser,$data) + { + $this->current_element['data']=$data; + } + + } + + /*This class uses a bunch of recursive functions to process the DAV XML tree + digging out the relevent information and putting it into an array + */ + class dav_processor + { + function dav_processor($xml_string) + { + $this->xml = $xml_string; + $this->dav_parser = new xml_tree_parser(); + $this->tree = $this->dav_parser->parse($xml_string); + + } + + function process_tree(&$element, &$result_array) + { + + //This lets us mark a node as 'done' and provides protection against infinite loops + if ($this->processed[$element['id']]) + { + return $result_array; + } + else + { + $this->processed[$element['id']] = true; + } + + if ( $element['namespace'] == 'DAV:') + { + if ($element['name'] == 'RESPONSE') + { + $result = array( + 'size' => 0, + 'getcontenttype' => 'application/octet-stream', + 'is_dir' => 0 + ); + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result); + } + $result_array[$result['full_name']] = $result; + + } + } + // ->recursion + foreach ($element['children'] as $id=>$child) + { + $this->process_tree($child, $result_array); + } + return $result_array; + } + + function process_properties($element, &$result_array) + { + if ($this->processed[$element['id']]) + { + return $result_array; + } + else + { + $this->processed[$element['id']] = true; + } + + if ( $element['namespace'] == 'DAV:') + { + switch ($element['name']) + { + case 'HREF': + $string = $element['data']; + $idx=strrpos($string,SEP); + if($idx && $idx==strlen($string)-1) + { + $this->current_ref=substr($string,0,$idx); + } + else + { + $this->current_ref=$string; + } + $result_array['name']=basename($string); + $result_array['directory']=dirname($string); + $result_array['full_name'] = $this->current_ref; + break; + case 'SUPPORTEDLOCK': + if (count($element['children'])) //There are active locks + { + $result_array['supported_locks'] = array(); + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result_array['supported_locks']); + } + } + break; + case 'LOCKDISCOVERY': + if (count($element['children'])) //There are active locks + { + $result_array['locks'] = array(); + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result_array['locks']); + } + } + break; + case 'LOCKENTRY': + if (count($element['children'])) + { + $result_array[$element['id']] = array(); + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result_array[$element['id']] ); + } + } + break; + case 'ACTIVELOCK': + if (count($element['children'])) + { + $result_array[$element['id']] = array(); + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result_array[$element['id']] ); + } + } + break; + case 'OWNER': + $result_array['owner'] = array(); + + foreach ($element['children'] as $child) + { + $this->process_verbatim($child, &$result_array['owner'][]); + } + + //print_r($element);die(); + //die(); + $result_array['owner_xml'] = substr($this->xml, $element['start'], $element['end']-$element['start']+1); + return $result_array; //No need to process this branch further + break; + case 'LOCKTOKEN': + if (count($element['children'])) + { + + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $tmp_result , $processed); + $result_array['lock_tokens'][$tmp_result['full_name']] = $tmp_result; + } + } + break; + case 'LOCKTYPE': + $child = end($element['children']); + if ($child) + { + $this->processed[$child['id']] = true; + $result_array['locktype'] = $child['name']; + } + break; + case 'LOCKSCOPE': + $child = end($element['children']); + if ($child) + { + $this->processed[$child['id']] = true; + $result_array['lockscope'] = $child['name']; + } + break; + default: + if (trim($element['data'])) + { + $result_array[strtolower($element['name'])] = $element['data']; + } + } + } + else + { + if (trim($element['data'])) + { + $result_array[strtolower($element['name'])] = $element['data']; + } + } + + foreach ($element['children'] as $id=>$child) + { + $this->process_properties($child, $result_array); + } + + return $result_array; + } + + function process_verbatim($element, &$result_array) + { + if ($this->processed[$element['id']]) + { + return $result_array; + } + else + { + $this->processed[$element['id']] = true; + } + + foreach ( $element as $key => $value) + { + //The parent link is death to naive programmers (eg me) :) + if (!( $key == 'children' || $key == 'parent') ) + { + $result_array[$key] = $value; + } + } + $result_array['children'] = array(); + foreach ($element['children'] as $id=>$child) + { + echo 'processing child:'; + $this->process_verbatim($child, $result_array['children']); + } + return $result_array; + } + } + +##################################################### +#This is the actual public interface of this class +##################################################### + class http_dav_client + { + var $attributes=array(); + var $vfs_property_map = array(); + var $cached_props = array(); + function http_dav_client() + { + $this->http_client = CreateObject('phpgwapi.net_http_client'); + $this->set_debug(0); + } + + //TODO: Get rid of this + //A quick, temporary debug output function + function debug($info) { + + if (DEBUG_DAV_CLIENT) + { + echo ' http_dav_client debug: '; + if (is_array($info)) + { + print_r($info); + } + else + { + echo $info; + } + echo '
'; + } + } + /*! + @function glue_url + @abstract glues a parsed url (ie parsed using PHP's parse_url) back + together + @param $url The parsed url (its an array) + */ + + function glue_url ($url){ + if (!is_array($url)) + { + return false; + } + // scheme + $uri = (!empty($url['scheme'])) ? $url['scheme'].'://' : ''; + // user & pass + if (!empty($url['user'])) + { + $uri .= $url['user']; + if (!empty($url['pass'])) + { + $uri .=':'.$url['pass']; + } + $uri .='@'; + } + // host + $uri .= $url['host']; + // port + $port = (!empty($url['port'])) ? ':'.$url['port'] : ''; + $uri .= $port; + // path + $uri .= $url['path']; + // fragment or query + if (isset($url['fragment'])) + { + $uri .= '#'.$url['fragment']; + } elseif (isset($url['query'])) + { + $uri .= '?'.$url['query']; + } + return $uri; + } + + /*! + @function encodeurl + @abstract encodes a url from its "display name" to something the dav server will accept + @param uri The unencoded uri + @discussion + Deals with "url"s which may contain spaces and other unsavoury characters, + by using appropriate %20s + */ + function encodeurl($uri) + { + $parsed_uri = parse_url($uri); + if (empty($parsed_uri['scheme'])) + { + $path = $uri; + } + else + { + $path = $parsed_uri['path']; + } + $fixed_array = array(); + foreach (explode('/', $path) as $name) + { + $fixed_array[] = rawurlencode($name); + } + $fixed_path = implode('/', $fixed_array); + if (empty($parsed_uri['scheme'])) + { + $parsed_uri['path'] = $fixed_path; + $newuri = $this->glue_url($parsed_uri); + } + else + { + $newuri = $fixed_path; + } + return $newuri; + + } + /*! + @function decodeurl + @abstract decodes a url to its "display name" + @param uri The encoded uri + @discussion + Deals with "url"s which may contain spaces and other unsavoury characters, + by using appropriate %20s + */ + function decodeurl($uri) + { + $parsed_uri = parse_url($uri); + if (empty($parsed_uri['scheme'])) + { + $path = $uri; + } + else + { + $path = $parsed_uri['path']; + } + $fixed_array = array(); + foreach (explode('/', $path) as $name) + { + $fixed_array[] = rawurldecode($name); + } + $fixed_path = implode('/', $fixed_array); + if (empty($parsed_uri['scheme'])) + { + $parsed_uri['path'] = $fixed_path; + $newuri = $this->glue_url($parsed_uri); + } + else + { + $newuri = $fixed_path; + } + return $newuri; + + } + /*! + @function set_attributes + @abstract Sets the "attribute map" + @param attributes Attributes to extract "as-is" from the DAV properties + @param dav_map A mapping of dav_property_name => attribute_name for attributes + with different names in DAV and the desired name space. + @discussion + This is mainly for use by VFS, where the VFS attributes (eg size) differ + from the corresponding DAV ones ("getcontentlength") + */ + function set_attributes($attributes, $dav_map) + { + $this->vfs_property_map = $dav_map; + $this->attributes = $attributes; + } + + /*! + @function set_credentials + @abstract Sets authentication credentials for HTTP AUTH + @param username The username to connect with + @param password The password to connect with + @discussion + The only supported authentication type is "basic" + */ + function set_credentials( $username, $password ) + { + $this->http_client->setCredentials($username, $password ); + } + + /*! + @function connect + @abstract connects to the server + @param dav_host The host to connect to + @param dav_port The port to connect to + @discussion + If the server requires authentication you will need to set credentials + with set_credentials first + */ + + function connect($dav_host,$dav_port) + { + $this->dav_host = $dav_host; + $this->dav_port = $dav_port; + $this->http_client->addHeader('Host',$this->dav_host); + $this->http_client->addHeader('Connection','close'); + //$this->http_client->addHeader('transfer-encoding','identity'); + // $this->http_client->addHeader('Connection','keep-alive'); + // $this->http_client->addHeader('Keep-Alive','timeout=20, state="Accept,Accept-Language"'); + $this->http_client->addHeader('Accept-Encoding','chunked'); + $this->http_client->setProtocolVersion( '1.1' ); + $this->http_client->addHeader( 'user-agent', 'Mozilla/5.0 (compatible; PHPGroupware dav_client/1; Linux)'); + return $this->http_client->Connect($dav_host,$dav_port); + } + function set_debug($debug) + { + $this->http_client->setDebug($debug); + } + + /*! + @function disconnect + @abstract disconnect from the server + @discussion + When doing HTTP 1.1 we frequently close/reopen the connection + anyway, so this function needs to be called after any other DAV calls + (since if they find the connection closed, they just reopen it) + */ + + function disconnect() + { + $this->http_client->Disconnect(); + } + + /*! + @function get_properties + @abstract a high-level method of getting DAV properties + @param url The URL to get properties for + @param scope the 'depth' to recuse subdirectories (default 1) + @param sorted whether we should sort the rsulting array (default True) + @result array of file->property arra + @discussion + This function performs all the necessary XML parsing etc to convert DAV properties (ie XML nodes) + into associative arrays of properties - including doing mappings + from DAV property names to any desired property name format (eg the VFS one) + This is controlled by the attribute arrays set in the set_attributes function. + */ + function get_properties($url,$scope=1){ + $request_id = $url.'//'.$scope.'//'.$sorted; //A unique id for this request (for caching) + if ($this->cached_props[$request_id]) + { +if (DEBUG_CACHE) echo'Cache hit : cache id:'.$request_id; + return $this->cached_props[$request_id]; + } + else if (! $sorted && $this->cached_props[$url.'//'.$scope.'//1']) + { +if (DEBUG_CACHE) echo ' Cache hit : cache id: '.$request_id; + return $this->cached_props[$url.'//'.$scope.'//1']; + } +if (DEBUG_CACHE) +{ + echo ' Cache miss : cache id: '.$request_id; +/* echo " cache:
";
+	print_r($this->cached_props);
+	echo '
';*/ +} + + + if($this->propfind($url,$scope) != 207) + { + if($this->propfind($url.'/',$scope) != 207) + { + return array(); + } + } + $xml_result=$this->http_client->getBody(); + $result_array = array(); + $dav_processor = new dav_processor($xml_result); + $tmp_list = $dav_processor->process_tree($dav_processor->tree, $result_array); + + foreach($tmp_list as $name=>$item) { + $fixed_name = $this->decodeurl($name); + $newitem = $item; + $newitem['is_dir']= ($item['getcontenttype'] =='httpd/unix-directory' ? 1 : 0); + $item['directory'] = $this->decodeurl($item['directory']); + //Since above we sawed off the protocol and host portions of the url, lets readd them. + if (strlen($item['directory'])) { + $path = $item['directory']; + $host = $this->dav_host; + $newitem['directory'] = $host.$path; + } + + //Get any extra properties that may share the vfs name + foreach ($this->attributes as $num=>$vfs_name) + { + if ($item[$vfs_name]) + { + $newitem[$vfs_name] = $item[$vfs_name]; + } + } + + //Map some DAV properties onto VFS ones. + foreach ($this->vfs_property_map as $dav_name=>$vfs_name) + { + if ($item[$dav_name]) + { + $newitem[$vfs_name] = $item[$dav_name]; + } + } + + if ($newitem['is_dir'] == 1) + { + $newitem['mime_type']='Directory'; + } + + $this->debug('

properties:
'); + $this->debug($newitem); + $newitem['name'] = $this->decodeurl($newitem['name']); + $result[$fixed_name]=$newitem; + $this->cached_props[$name.'//1//1'] = array($fixed_name=>$newitem); + } + if ($sorted) + { + ksort($result); + } + $this->cached_props[$request_id] = $result; + return $result; + } + + function get($uri) + { + $uri = $this->encodeurl($uri); + return $this->http_client->Get($uri); + } + + /*! + @function get_body + @abstract return the response body + @result string body content + @discussion + invoke it after a Get() call for instance, to retrieve the response + */ + function get_body() + { + return $this->http_client->getBody(); + } + + /*! + @function get_headers + @abstract return the response headers + @result array headers received from server in the form headername => value + @discussion + to be called after a Get() or Head() call + */ + function get_headers() + { + return $this->http_client->getHeaders(); + } + + /*! + @function copy + @abstract PUT is the method to sending a file on the server. + @param uri the location of the file on the server. dont forget the heading "/" + @param data the content of the file. binary content accepted + @result string response status code 201 (Created) if ok + */ + function put($uri, $data, $token='') + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + + $this->cached_props = array(); + $result = $this->http_client->Put($uri, $data); + $this->http_client->removeHeader('If'); + return $result; + } + + /*! + @function copy + @abstract Copy a file -allready on the server- into a new location + @param srcUri the current file location on the server. dont forget the heading "/" + @param destUri the destination location on the server. this is *not* a full URL + @param overwrite boolean - true to overwrite an existing destination - overwrite by default + @result Returns the HTTP status code + @discussion + returns response status code 204 (Unchanged) if ok + */ + function copy( $srcUri, $destUri, $overwrite=true, $scope=0, $token='') + { + $srcUri = $this->encodeurl($srcUri); + $destUri = $this->encodeurl($destUri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + $this->cached_props = array(); + $result = $this->http_client->Copy( $srcUri, $destUri, $overwrite, $scope); + $this->http_client->removeHeader('If'); + return $result; + } + + /*! + @function move + @abstract Moves a WEBDAV resource on the server + @param srcUri the current file location on the server. dont forget the heading "/" + @param destUri the destination location on the server. this is *not* a full URL + @param overwrite boolean - true to overwrite an existing destination (default is yes) + @result Returns the HTTP status code + @discussion + returns response status code 204 (Unchanged) if ok + */ + function move( $srcUri, $destUri, $overwrite=true, $scope=0, $token='' ) + { + $srcUri = $this->encodeurl($srcUri); + $destUri = $this->encodeurl($destUri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + $this->cached_props = array(); + $result = $this->http_client->Move( $srcUri, $destUri, $overwrite, $scope); + $this->http_client->removeHeader('If'); + return $result; + } + + /*! + @function delete + @abstract Deletes a WEBDAV resource + @param uri The URI we are deleting + @result Returns the HTTP status code + @discussion + returns response status code 204 (Unchanged) if ok + */ + function delete( $uri, $scope=0, $token='') + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + + $this->cached_props = array(); + $result = $this->http_client->Delete( $uri, $scope); + $this->http_client->removeHeader('If'); + return $result; + } + + /*! + @function mkcol + @abstract Creates a WEBDAV collection (AKA a directory) + @param uri The URI to create + @result Returns the HTTP status code + */ + function mkcol( $uri, $token='' ) + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + $this->cached_props = array(); + return $this->http_client->MkCol( $uri ); + $this->http_client->removeHeader('If'); + } + + /*! + @function propfind + @abstract Queries WEBDAV properties + @param uri uri of resource whose properties we are changing + @param scope Specifies how "deep" to search (0=just this file/dir 1=subfiles/dirs etc) + @result Returns the HTTP status code + @discussion + to get the result XML call get_body() + */ + function propfind( $uri, $scope=0 ) + { + $uri = $this->encodeurl($uri); + return $this->http_client->PropFind( $uri, $scope); + } + /*! + @function proppatch + @abstract Sets DAV properties + @param uri uri of resource whose properties we are changing + @param attributes An array of attributes and values. + @param namespaces Extra namespace definitions that apply to the properties + @result Returns the HTTP status code + @discussion + To make DAV properties useful it helps to use a well established XML dialect + such as the "Dublin Core" + + */ + function proppatch($uri, $attributes, $namespaces='', $token='') + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + if (strlen($token)) + { + $this->http_client->addHeader('If', '<'.$uri.'>'.' (<'.$token.'>)'); + } + $this->cached_props = array(); + //Begin evil nastiness + $davxml = ' + $value) + { + $davxml .= ' + + + <'.$name.'>'.utf8_encode(htmlspecialchars($value)).' + + +'; + } + $davxml .= ' +'; + + if (DEBUG_DAV_XML) { + echo 'send
'.htmlentities($davxml).'
'; + } + $this->http_client->requestBody = $davxml; + if( $this->http_client->sendCommand( 'PROPPATCH '.$uri.' HTTP/1.1' ) ) + { + $this->http_client->processReply(); + } + + if (DEBUG_DAV_XML) { + echo 'Recieve
'.htmlentities($this->http_client->getBody()).'
'; + } + $this->http_client->removeHeader('If'); + return $this->http_client->reply; + } + + /*! + @function unlock + @abstract unlocks a locked resource on the DAV server + @param uri uri of the resource we are unlocking + @param a 'token' for the lock (to get the token, do a propfind) + @result true if successfull + @discussion + Not all DAV servers support locking (its in the RFC, but many common + DAV servers only implement "DAV class 1" (no locking) + */ + + function unlock($uri, $token) + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + $this->cached_props = array(); + $this->http_client->addHeader('Lock-Token', '<'.$token.'>'); + $this->http_client->sendCommand( 'UNLOCK '.$uri.' HTTP/1.1'); + $this->http_client->removeHeader('Lock-Token'); + $this->http_client->processReply(); + if ( $this->http_client->reply == '204') + { + return true; + } + else + { + $headers = $this->http_client->getHeaders(); + echo $this->http_client->getBody(); + if ($headers['Content-Type'] == 'text/html') + { + echo $this->http_client->getBody(); + die(); + } + else + { + return false; + } + } + } + + /*! + @function lock + @abstract locks a resource on the DAV server + @param uri uri of the resource we are locking + @param owner the 'owner' information for the lock (purely informative) + @param depth the depth to which we lock collections + @result true if successfull + @discussion + Not all DAV servers support locking (its in the RFC, but many common + DAV servers only implement "DAV class 1" (no locking) + */ + function lock($uri, $owner, $depth=0, $timeout='infinity') + { + $uri = $this->encodeurl($uri); +if (DEBUG_CACHE) echo 'cache cleared'; + $this->cached_props = array(); + $body = " + +\n + $owner +\n"; + + $this->http_client->requestBody = utf8_encode( $body ); + $this->http_client->addHeader('Depth', $depth); + if (! (strtolower(trim($timeout)) == 'infinite')) + { + $timeout = 'Second-'.$timeout; + } + $this->http_client->addHeader('Timeout', $timeout); + + if( $this->http_client->sendCommand( "LOCK $uri HTTP/1.1" ) ) + $this->http_client->processReply(); + $this->http_client->removeHeader('timeout'); + if ( $this->http_client->reply == '200') + { + return true; + } + else + { + $headers = $this->http_client->getHeaders(); + echo $this->http_client->getBody(); + return false; + + } + + } + /*! + @function options + @abstract determines the optional HTTP features supported by a server + @param uri uri of the resource we are seeking options for (or * for the whole server) + @result Returns an array of option values + @discussion + Interesting options include "ACCESS" (whether you can read a file) and + DAV (DAV features) + + */ + function options($uri) + { + $uri = $this->encodeurl($uri); + if( $this->http_client->sendCommand( 'OPTIONS '.$uri.' HTTP/1.1' ) == '200' ) + { + $this->http_client->processReply(); + $headers = $this->http_client->getHeaders(); + return $headers; + } + else + { + return False; + } + } + /*! + @function dav_features + @abstract determines the features of a DAV server + @param uri uri of resource whose properties we are changing + @result Returns an array of option values + @discussion + Likely return codes include NULL (this isnt a dav server!), 1 + (This is a dav server, supporting all standard DAV features except locking) + 2, (additionally supports locking (should also return 1)) and + 'version-control' (this server supports versioning extensions for this resource) + */ + function dav_features($uri) + { + $uri = $this->encodeurl($uri); + $options = $this->options($uri); + $dav_options = $options['DAV']; + if ($dav_options) + { + $features=explode(',', $dav_options); + } + else + { + $features = NULL; + } + return $features; + } +/************************************************************** + RFC 3253 DeltaV versioning extensions + ************************************************************** + These are 100% untested, and almost certainly dont work yet... + eventually they will be made to work with subversion... + */ + + /*! + @function report + @abstract Report is a kind of extended PROPFIND - it queries properties accros versions etc + @param uri uri of resource whose properties we are changing + @param report the type of report desired eg DAV:version-tree, DAV:expand-property etc (see http://greenbytes.de/tech/webdav/rfc3253.html#METHOD_REPORT) + @param namespace any extra XML namespaces needed for the specified properties + @result Returns an array of option values + @discussion + From the relevent RFC: + "A REPORT request is an extensible mechanism for obtaining information about + a resource. Unlike a resource property, which has a single value, the value + of a report can depend on additional information specified in the REPORT + request body and in the REPORT request headers." + */ + function report($uri, $report, $properties, $namespaces='') + { + $uri = $this->encodeurl($uri); + $davxml = ' +\n'; + } + $davxml .= '\t\n'; + if (DEBUG_DAV_XML) { + echo 'send
'.htmlentities($davxml).'
'; + } + $this->http_client->requestBody = $davxml; + if( $this->http_client->sendCommand( 'REPORT '.$uri.' HTTP/1.1' ) ) + { + $this->http_client->processReply(); + } + + if (DEBUG_DAV_XML) { + echo 'Recieve
'.htmlentities($this->http_client->getBody()).'
'; + } + return $this->http_client->reply; + } + + } + diff --git a/phpgwapi/inc/class.vfs_dav.inc.php b/phpgwapi/inc/class.vfs_dav.inc.php new file mode 100644 index 0000000000..c0028ac508 --- /dev/null +++ b/phpgwapi/inc/class.vfs_dav.inc.php @@ -0,0 +1,2978 @@ + * + * This class handles file/dir access for phpGroupWare * + * Copyright (C) 2001-2003 Jason Wies, Jonathon Sim * + * -------------------------------------------------------------------------* + * This library is part of the phpGroupWare API * + * http://www.phpgroupware.org/api * + * ------------------------------------------------------------------------ * + * This library is free software; you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as published by * + * the Free Software Foundation; either version 2.1 of the License, * + * or any later version. * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU Lesser General Public License for more details. * + * You should have received a copy of the GNU Lesser General Public License * + * along with this library; if not, write to the Free Software Foundation, * + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * + \**************************************************************************/ + + + /* Relative defines. Used mainly by getabsolutepath () */ + define ('RELATIVE_ROOT', 1); + define ('RELATIVE_USER', 2); + define ('RELATIVE_CURR_USER', 4); + define ('RELATIVE_USER_APP', 8); + define ('RELATIVE_PATH', 16); + define ('RELATIVE_NONE', 32); + define ('RELATIVE_CURRENT', 64); + define ('VFS_REAL', 1024); + define ('RELATIVE_ALL', RELATIVE_PATH); + + /* These are used in calls to extra_sql () */ + define ('VFS_SQL_SELECT', 1); + define ('VFS_SQL_DELETE', 2); + define ('VFS_SQL_UPDATE', 4); + + /* These are used in calls to add_journal (), and allow journal messages to be more standard */ + define ('VFS_OPERATION_CREATED', 1); + define ('VFS_OPERATION_EDITED', 2); + define ('VFS_OPERATION_EDITED_COMMENT', 4); + define ('VFS_OPERATION_COPIED', 8); + define ('VFS_OPERATION_MOVED', 16); + define ('VFS_OPERATION_DELETED', 32); + + /*Different aspects of debugging. DEBUG enables debug output for this class + DEBUG_SQL enables some SQL debugging. DEBUG_DAV enables (LOTS) of debugging inside + the HTTP class + */ + define ('DEBUG', 0); + define ('TRACE', 0);//This generates a whole lotta output + define ('DEBUG_SQL', 0); + define ('DEBUG_DAV', 0); + + /*! + @class path_class + @abstract helper class for path_parts + */ + + class path_class + { + var $mask; + var $outside; + var $fake_full_path; + var $fake_leading_dirs; + var $fake_extra_path; + var $fake_name; + var $real_full_url; + var $real_full_auth_url; + var $real_full_secure_url; + var $real_full_path; + var $real_leading_dirs; + var $real_extra_path; + var $real_name; + var $fake_full_path_clean; + var $fake_leading_dirs_clean; + var $fake_extra_path_clean; + var $fake_name_clean; + var $real_full_url_clean; + var $real_full_auth_url_clean; + var $real_full_secure_url_clean; + var $real_full_path_clean; + var $real_leading_dirs_clean; + var $real_extra_path_clean; + var $real_name_clean; + } + + class vfs + { + var $basedir; + var $fakebase; + var $relative; + var $working_id; + var $working_lid; + var $attributes; + var $override_acl; + var $linked_dirs; + var $meta_types; + var $now; + var $override_locks; + //These are DAV-native properties that have different names in VFS + var $vfs_property_map = array( + 'creationdate' => 'created', + 'getlastmodified' => 'modified', + 'getcontentlength' => 'size', + 'getcontenttype' => 'mime_type', + 'description' => 'comment', + 'creator_id' => 'createdby_id', + 'contributor_id' => 'modifiedby_id', + 'publisher_id' => 'owner_id' + ); + + /*! + @function vfs + @abstract constructor, sets up variables + */ + + function vfs () + { + $this->basedir = $GLOBALS['phpgw_info']['server']['files_dir']; + $this->fakebase = '/home'; + $this->working_id = $GLOBALS['phpgw_info']['user']['account_id']; + $this->working_lid = $GLOBALS['phpgw']->accounts->id2name($this->working_id); + $this->now = date ('Y-m-d'); + $this->override_acl = 0; + /* + File/dir attributes, each corresponding to a database field. Useful for use in loops + If an attribute was added to the table, add it here and possibly add it to + set_attributes () + + set_attributes now uses this array(). 07-Dec-01 skeeter + */ + + $this->attributes = array( + 'file_id', + 'owner_id', + 'createdby_id', + 'modifiedby_id', + 'created', + 'modified', + 'size', + 'mime_type', + 'deleteable', + 'comment', + 'app', + 'directory', + 'name', + 'link_directory', + 'link_name', + 'version' + ); + + /* + These are stored in the MIME-type field and should normally be ignored. + Adding a type here will ensure it is normally ignored, but you will have to + explicitly add it to acl_check (), and to any other SELECT's in this file + */ + + $this->meta_types = array ('journal', 'journal-deleted'); + + /* We store the linked directories in an array now, so we don't have to make the SQL call again */ + if ($GLOBALS['phpgw_info']['server']['db_type']=='mssql' + || $GLOBALS['phpgw_info']['server']['db_type']=='sybase') + { + $query = $GLOBALS['phpgw']->db->query ("SELECT directory, name, link_directory, link_name FROM phpgw_vfs WHERE CONVERT(varchar,link_directory) != '' AND CONVERT(varchar,link_name) != ''" . $this->extra_sql (array ('query_type' => VFS_SQL_SELECT)), __LINE__,__FILE__); + } + else + { + $query = $GLOBALS['phpgw']->db->query ("SELECT directory, name, link_directory, link_name FROM phpgw_vfs WHERE (link_directory IS NOT NULL or link_directory != '') AND (link_name IS NOT NULL or link_name != '')" . $this->extra_sql (array ('query_type' => VFS_SQL_SELECT)), __LINE__,__FILE__); + } + + $this->linked_dirs = array (); + while ($GLOBALS['phpgw']->db->next_record ()) + { + $this->linked_dirs[] = $GLOBALS['phpgw']->db->Record; + } + + + $this->repository = $GLOBALS['phpgw_info']['server']['files_dir']; + $this->dav_user=$GLOBALS['phpgw_info']['user']['userid']; + $this->dav_pwd=$GLOBALS['phpgw_info']['user']['passwd']; + $parsed_url = parse_url($this->repository); + $this->dav_host=$parsed_url['host']; + $this->dav_port=@isset($parsed_url['port']) ? $parsed_url['port'] : 80; + + $this->dav_client = CreateObject('phpgwapi.http_dav_client'); + $this->dav_client->set_credentials($this->dav_user,$this->dav_pwd); + $this->dav_client->set_attributes($this->attributes,$this->vfs_property_map); + $result = $this->dav_client->connect($this->dav_host,$this->dav_port); + if (DEBUG_DAV) + { + echo 'DAV client debugging enabled!'; + $this->dav_client->set_debug(DBGTRACE|DBGINDATA|DBGOUTDATA|DBGSOCK|DBGLOW); + } + if (!$result) + { + echo '

Cannot connect to the file repository server!

'; + die($this->dav_client->get_body()); + } + //determine the supported DAV features +/* $features = $this->dav_client->dav_features('http://'.$this->dav_host); + if (!$features || ! in_array( '1', $features) ) + { + die("Error :: The specified file repository: $this->dav_host doesn't appear to support WebDAV! "); + + } +*/ + //Reload the overriden_locks + $app = $GLOBALS['phpgw_info']['flags']['currentapp']; + $session_data = base64_decode($GLOBALS['phpgw']->session->appsession ('vfs_dav',$app)); + $this->override_locks = array(); + if ($session_data) + { + $locks = explode('\n', $session_data); + foreach ($locks as $lock) + { + $lockdata = explode(';', $lock); + $name = $lockdata[0]; + $token = $lockdata[1]; + $this->override_locks[$name] = $token; + } + } + + register_shutdown_function(array(&$this, 'vfs_umount')); + $this->debug('Constructed with debug enabled'); + + } + + //TODO: Get rid of this + //A quick, temporary debug output function + function debug($info) { + if (DEBUG) + { + echo ' vfs_sql_dav debug: '; + if (is_array($info)) + { + print_r($info); + } + else + { + echo $info; + } + echo '
'; + } + } + + /*! + @function dav_path + @abstract Apaches mod_dav in particular requires that the path sent in a dav request NOT be a URI + */ + function dav_path($uri) { + //$this->debug('DAV path'); + $parsed = parse_url($uri); + return $parsed['path']; + } + + /*! + @function glue_url + @abstract glues a parsed url (ie parsed using PHP's parse_url) back + together + @param $url The parsed url (its an array) + */ + function glue_url ($url){ + if (!is_array($url)) + { + return false; + } + // scheme + $uri = (!empty($url['scheme'])) ? $url['scheme'].'://' : ''; + // user & pass + if (!empty($url['user'])) + { + $uri .= $url['user']; + if (!empty($url['pass'])) + { + $uri .=':'.$url['pass']; + } + $uri .='@'; + } + // host + $uri .= $url['host']; + // port + $port = (!empty($url['port'])) ? ':'.$url['port'] : ''; + $uri .= $port; + // path + $uri .= $url['path']; + // fragment or query + if (isset($url['fragment'])) + { + $uri .= '#'.$url['fragment']; + } elseif (isset($url['query'])) + { + $uri .= '?'.$url['query']; + } + return $uri; + } + + function dav_host($uri) { + //$this->debug('DAV path'); + $parsed = parse_url($uri); + $parsed['path'] = ''; + $host = $this->glue_url($parsed); + return $host; + } + + function vfs_umount() + { + $this->dav_client->disconnect(); + } + + + /*! + @function set_relative + @abstract Set path relativity + @param mask Relative bitmask (see RELATIVE_ defines) + */ + function set_relative ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + if (!$data['mask']) + { + unset ($this->relative); + } + else + { + $this->relative = $data['mask']; + } + } + + /*! + @function get_relative + @abstract Return relativity bitmask + @discussion Returns relativity bitmask, or the default of "completely relative" if unset + */ + function get_relative () + { + if (isset ($this->relative) && $this->relative) + { + return $this->relative; + } + else + { + return RELATIVE_ALL; + } + } + + /*! + @function sanitize + @abstract Removes leading .'s from 'string' + @discussion You should not pass all filenames through sanitize () unless you plan on rejecting + .files. Instead, pass the name through securitycheck () first, and if it fails, + pass it through sanitize + @param string string to sanitize + @result $string 'string' without it's leading .'s + */ + function sanitize ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + /* We use path_parts () just to parse the string, not translate paths */ + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array (RELATIVE_NONE) + ) + ); + + return (ereg_replace ('^\.+', '', $p->fake_name)); + } + + /*! + @function securitycheck + @abstract Security check function + @discussion Checks for basic violations such as .. + If securitycheck () fails, run your string through vfs->sanitize () + @param string string to check security of + @result Boolean True/False. True means secure, False means insecure + */ + function securitycheck ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + if (substr ($data['string'], 0, 1) == "\\" || strstr ($data['string'], "..") || strstr ($data['string'], "\\..") || strstr ($data['string'], ".\\.")) + { + return False; + } + else + { + return True; + } + } + + /*! + @function db_clean + @abstract Clean 'string' for use in database queries + @param string String to clean + @result Cleaned version of 'string' + */ + function db_clean ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + $string = ereg_replace ("'", "\'", $data['string']); + + return $string; + } + + /*! + @function extra_sql + @abstract Return extra SQL code that should be appended to certain queries + @param query_type The type of query to get extra SQL code for, in the form of a VFS_SQL define + @result Extra SQL code + */ + function extra_sql ($data) + { //This is purely for SQL + return ''; + } + + /*! + @function add_journal + @abstract Add a journal entry after (or before) completing an operation, + and increment the version number. This function should be used internally only + @discussion Note that state_one and state_two are ignored for some VFS_OPERATION's, for others + they are required. They are ignored for any "custom" operation + The two operations that require state_two: + operation state_two + VFS_OPERATION_COPIED fake_full_path of copied to + VFS_OPERATION_MOVED fake_full_path of moved to + + If deleting, you must call add_journal () before you delete the entry from the database + @param string File or directory to add entry for + @param relatives Relativity array + @param operation The operation that was performed. Either a VFS_OPERATION define or + a non-integer descriptive text string + @param state_one The first "state" of the file or directory. Can be a file name, size, + location, whatever is appropriate for the specific operation + @param state_two The second "state" of the file or directory + @param incversion Boolean True/False. Increment the version for the file? Note that this is + handled automatically for the VFS_OPERATION defines. + i.e. VFS_OPERATION_EDITED would increment the version, VFS_OPERATION_COPIED + would not + @result Boolean True/False + */ + function add_journal ($data) { + //The journalling dont work :( Ideally this will become "versioning" + return True; + } + + + /*! + @function flush_journal + @abstract Flush journal entries for $string. Used before adding $string + @discussion flush_journal () is an internal function and should be called from add_journal () only + @param string File/directory to flush journal entries of + @param relatives Realtivity array + @param deleteall Delete all types of journal entries, including the active Create entry. + Normally you only want to delete the Create entry when replacing the file + Note that this option does not effect $deleteonly + @param deletedonly Only flush 'journal-deleted' entries (created when $string was deleted) + @result Boolean True/False + */ + function flush_journal ($data) + { + return True; + } + + + /*! + @function get_journal + @abstract Retrieve journal entries for $string + @param string File/directory to retrieve journal entries of + @param relatives Relativity array + @param type 0/False = any, 1 = 'journal', 2 = 'journal-deleted' + @result Array of arrays of journal entries + */ + function get_journal ($data) + { + return array(); + } + + /*! + @function path_parts + @abstract take a real or fake pathname and return an array of its component parts + @param string full real or fake path + @param relatives Relativity array + @param object True returns an object instead of an array + @param nolinks Don't check for links (made with make_link ()). Used internally to prevent recursion + @result $rarray/$robject Array or object containing the fake and real component parts of the path + @discussion Returned values are: + mask + outside + fake_full_path + fake_leading_dirs + fake_extra_path BROKEN + fake_name + real_full_path + real_leading_dirs + real_extra_path BROKEN + real_name + fake_full_path_clean + fake_leading_dirs_clean + fake_extra_path_clean BROKEN + fake_name_clean + real_full_path_clean + real_leading_dirs_clean + real_extra_path_clean BROKEN + real_name_clean + real_uri + "clean" values are run through vfs->db_clean () and + are safe for use in SQL queries that use key='value' + They should be used ONLY for SQL queries, so are used + mostly internally + mask is either RELATIVE_NONE or RELATIVE_NONE|VFS_REAL, + and is used internally + outside is boolean, True if 'relatives' contains VFS_REAL + */ + function path_parts ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'object' => True, + 'nolinks' => False + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $sep = SEP; + + $rarray['mask'] = RELATIVE_NONE; + + if (!($data['relatives'][0] & VFS_REAL)) + { + $rarray['outside'] = False; + $fake = True; + } + else + { + $rarray['outside'] = True; + $rarray['mask'] |= VFS_REAL; + } + + $string = $this->getabsolutepath (array( + 'string' => $data['string'], + 'mask' => array ($data['relatives'][0]), + 'fake' => $fake + ) + ); + + if ($fake) + { + $base_sep = '/'; + $base = '/'; + + $opp_base = $this->basedir . $sep; + + $rarray['fake_full_path'] = $string; + } + else + { + $base_sep = $sep; + if (ereg ("^$this->basedir" . $sep, $string)) + { + $base = $this->basedir . $sep; + } + else + { + $base = $sep; + } + + $opp_base = '/'; + $rarray['real_full_url'] = $string; + $rarray['real_full_path'] = $this->dav_path($string); + } + + /* This is needed because of substr's handling of negative lengths */ + $baselen = strlen ($base); + $lastslashpos = strrpos ($string, $base_sep); + $lastslashpos < $baselen ? $length = 0 : $length = $lastslashpos - $baselen; + + $extra_path = $rarray['fake_extra_path'] = $rarray['real_extra_path'] = substr ($string, strlen ($base), $length); + $name = $rarray['fake_name'] = $rarray['real_name'] = substr ($string, strrpos ($string, $base_sep) + 1); + + if ($fake) + { + $rarray['real_extra_path'] ? $dispsep = $sep : $dispsep = ''; + $rarray['real_full_url'] = $opp_base . $rarray['real_extra_path'] . $dispsep . $rarray['real_name']; + $rarray['real_full_path'] = $this->dav_path($rarray['real_full_url']); + if ($extra_path) + { + $rarray['fake_leading_dirs'] = $base . $extra_path; + $rarray['real_leading_dirs'] = $this->dav_path($opp_base . $extra_path); + } + elseif (strrpos ($rarray['fake_full_path'], $sep) == 0) + { + /* If there is only one $sep in the path, we don't want to strip it off */ + $rarray['fake_leading_dirs'] = $sep; + $rarray['real_leading_dirs'] = $this->dav_path( substr ($opp_base, 0, strlen ($opp_base) - 1)); + } + else + { + /* These strip the ending / */ + $rarray['fake_leading_dirs'] = substr ($base, 0, strlen ($base) - 1); + $rarray['real_leading_dirs'] = $this->dav_path( substr ($opp_base, 0, strlen ($opp_base) - 1)); + } + } + else + { + $rarray['fake_full_path'] = $opp_base . $rarray['fake_extra_path'] . '/' . $rarray['fake_name']; + if ($extra_path) + { + $rarray['fake_leading_dirs'] = $opp_base . $extra_path; + $rarray['real_leading_dirs'] = $this->dav_path($base . $extra_path); + } + else + { + $rarray['fake_leading_dirs'] = substr ($opp_base, 0, strlen ($opp_base) - 1); + $rarray['real_leading_dirs'] = $this->dav_path(substr ($base, 0, strlen ($base) - 1)); + } + } + + /* We check for linked dirs made with make_link (). This could be better, but it works */ + if (!$data['nolinks']) + { + reset ($this->linked_dirs); + while (list ($num, $link_info) = each ($this->linked_dirs)) + { + if (ereg ("^$link_info[directory]/$link_info[name](/|$)", $rarray['fake_full_path'])) + { + $rarray['real_full_path'] = ereg_replace ("^$this->basedir", '', $rarray['real_full_path']); + $rarray['real_full_path'] = ereg_replace ("^$link_info[directory]" . SEP . "$link_info[name]", $link_info['link_directory'] . SEP . $link_info['link_name'], $rarray['real_full_path']); + + $p = $this->path_parts (array( + 'string' => $rarray['real_full_path'], + 'relatives' => array (RELATIVE_NONE|VFS_REAL), + 'nolinks' => True + ) + ); + + $rarray['real_leading_dirs'] = $this->dav_path($p->real_leading_dirs); + $rarray['real_extra_path'] = $p->real_extra_path; + $rarray['real_name'] = $p->real_name; + } + } + } + + /* + Create the 'real_auth_url', which includes the user and + password (for the view method to redirect you there) + */ + + $parsed_url = parse_url($rarray['real_full_url']); + $parsed_url['user'] = $this->dav_user; +// $parsed_url['pass'] = $this->dav_pwd; + $rarray['real_full_auth_url'] = $this->glue_url($parsed_url); + + $parsed_url = parse_url($rarray['real_full_url']); + $parsed_url['scheme'] = 'https'; + $parsed_url['user'] = $this->dav_user; + $rarray['real_full_secure_url'] = $this->glue_url($parsed_url); + + + /* + We have to count it before because new keys will be added, + which would create an endless loop + */ + $count = count ($rarray); + reset ($rarray); + for ($i = 0; (list ($key, $value) = each ($rarray)) && $i != $count; $i++) + { + $rarray[$key . '_clean'] = $this->db_clean (array ('string' => $value)); + } + + if ($data['object']) + { + $robject = new path_class; + + reset ($rarray); + while (list ($key, $value) = each ($rarray)) + { + $robject->$key = $value; + } + } + +/* + echo "
fake_full_path: $rarray[fake_full_path] +
fake_leading_dirs: $rarray[fake_leading_dirs] +
fake_extra_path: $rarray[fake_extra_path] +
fake_name: $rarray[fake_name] +
real_full_path: $rarray[real_full_path] +
real_full_url: $rarray[real_full_url] +
real_leading_dirs: $rarray[real_leading_dirs] +
real_extra_path: $rarray[real_extra_path] +
real_name: $rarray[real_name]"; +*/ + + if ($data['object']) + { + return ($robject); + } + else + { + return ($rarray); + } + } + + /*! + @function getabsolutepath + @abstract get the absolute path + @param string defaults to False, directory/file to get path of, relative to relatives[0] + @param mask Relativity bitmask (see RELATIVE_ defines). RELATIVE_CURRENT means use $this->relative + @param fake Returns the "fake" path, ie /home/user/dir/file (not always possible. use path_parts () instead) + @result $basedir Full fake or real path + */ + function getabsolutepath ($data) + { + $default_values = array + ( + 'string' => False, + 'mask' => array (RELATIVE_CURRENT), + 'fake' => True + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $currentdir = $this->pwd (False); + + /* If they supply just VFS_REAL, we assume they want current relativity */ + if ($data['mask'][0] == VFS_REAL) + { + $data['mask'][0] |= RELATIVE_CURRENT; + } + + if (!$this->securitycheck (array( + 'string' => $data['string'] + )) + ) + { + return False; + } + + if ($data['mask'][0] & RELATIVE_NONE) + { + return $data['string']; + } + + if ($data['fake']) + { + $sep = '/'; + } + else + { + $sep = SEP; + } + + /* if RELATIVE_CURRENT, retrieve the current mask */ + if ($data['mask'][0] & RELATIVE_CURRENT) + { + $mask = $data['mask'][0]; + /* Respect any additional masks by re-adding them after retrieving the current mask*/ + $data['mask'][0] = $this->get_relative () + ($mask - RELATIVE_CURRENT); + } + + if ($data['fake']) + { + $basedir = '/'; + } + else + { + $basedir = $this->basedir . $sep; + + /* This allows all requests to use /'s */ + $data['string'] = preg_replace ("|/|", $sep, $data['string']); + } + + if (($data['mask'][0] & RELATIVE_PATH) && $currentdir) + { + $basedir = $basedir . $currentdir . $sep; + } + elseif (($data['mask'][0] & RELATIVE_USER) || ($data['mask'][0] & RELATIVE_USER_APP)) + { + $basedir = $basedir . $this->fakebase . $sep; + } + + if ($data['mask'][0] & RELATIVE_CURR_USER) + { + $basedir = $basedir . $this->working_lid . $sep; + } + + if (($data['mask'][0] & RELATIVE_USER) || ($data['mask'][0] & RELATIVE_USER_APP)) + { + $basedir = $basedir . $GLOBALS['phpgw_info']['user']['account_lid'] . $sep; + } + + if ($data['mask'][0] & RELATIVE_USER_APP) + { + $basedir = $basedir . "." . $GLOBALS['phpgw_info']['flags']['currentapp'] . $sep; + } + + /* Don't add string if it's a /, just for aesthetics */ + if ($data['string'] && $data['string'] != $sep) + { + $basedir = $basedir . $data['string']; + } + + /* Let's not return // */ + while (ereg ($sep . $sep, $basedir)) + { + $basedir = ereg_replace ($sep . $sep, $sep, $basedir); + } + + $basedir = ereg_replace ($sep . '$', '', $basedir); + + return $basedir; + } + + /*! + @function acl_check + @abstract Check ACL access to $file for $GLOBALS['phpgw_info']["user"]["account_id"]; + @param string File to check access of + @discussion To check the access for a file or directory, pass 'string'/'relatives'/'must_exist'. + To check the access to another user or group, pass 'owner_id'. + If 'owner_id' is present, we bypass checks on 'string'/'relatives'/'must_exist' + @param relatives Standard relativity array + @param operation Operation to check access to. In the form of a PHPGW_ACL defines bitmask. Default is read + @param owner_id Owner id to check access of (see discussion above) + @param must_exist Boolean. Set to True if 'string' must exist. Otherwise, we check the parent directory as well + @result Boolean. True if access is ok, False otherwise + */ + function acl_check ($data) + { + return True; + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'operation' => PHPGW_ACL_READ, + 'must_exist' => False + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + /* Accommodate special situations */ + if ($this->override_acl || $data['relatives'][0] == RELATIVE_USER_APP) + { + return True; + } + + if (!$data['owner_id']) + { + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + /* Temporary, until we get symlink type files set up */ + if ($p->outside) + { + return True; + } + + /* Read access is always allowed here, but nothing else is */ + if ($data['string'] == '/' || $data['string'] == $this->fakebase) + { + if ($data['operation'] == PHPGW_ACL_READ) + { + return True; + } + else + { + return False; + } + } + + /* If the file doesn't exist, we get ownership from the parent directory */ + if (!$this->file_exists (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask) + )) + ) + { + if ($data['must_exist']) + { + return False; + } + + $data['string'] = $p->fake_leading_dirs; + $p2 = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($p->mask) + ) + ); + + if (!$this->file_exists (array( + 'string' => $data['string'], + 'relatives' => array ($p->mask) + )) + ) + { + return False; + } + } + else + { + $p2 = $p; + } + + $file_info = $this->ls($data); + $owner_id = $file_info['owner_id']; + } + else + { + $owner_id = $data['owner_id']; + } + + /* This is correct. The ACL currently doesn't handle undefined values correctly */ + if (!$owner_id) + { + $owner_id = 0; + } + + $user_id = $GLOBALS['phpgw_info']['user']['account_id']; + + /* They always have access to their own files */ + if ($owner_id == $user_id) + { + return True; + } + + /* Check if they're in the group */ + $memberships = $GLOBALS['phpgw']->accounts->membership ($user_id); + + if (is_array ($memberships)) + { + reset ($memberships); + while (list ($num, $group_array) = each ($memberships)) + { + if ($owner_id == $group_array['account_id']) + { + $group_ok = 1; + break; + } + } + } + + $acl = CreateObject ('phpgwapi.acl', $owner_id); + $acl->account_id = $owner_id; + $acl->read_repository (); + + $rights = $acl->get_rights ($user_id); + + /* Add privileges from the groups this user belongs to */ + if (is_array ($memberships)) + { + reset ($memberships); + while (list ($num, $group_array) = each ($memberships)) + { + $rights |= $acl->get_rights ($group_array['account_id']); + } + } + + if ($rights & $data['operation']) + { + return True; + } + elseif (!$rights && $group_ok) + { + $conf = CreateObject('phpgwapi.config', 'phpgwapi'); + $conf->read_repository(); + if ($conf->config_data['acl_default'] == 'grant') + { + return True; + } + else + { + return False; + } + } + else + { + return False; + } + } + + /*! + @function cd + @abstract Change directory + @discussion To cd to the files root '/', use cd ('/', False, array (RELATIVE_NONE)); + @param string default '/'. directory to cd into. if "/" and $relative is True, uses "/home/"; + @param relative default True/relative means add target to current path, else pass $relative as mask to getabsolutepath() + @param relatives Relativity array + */ + function cd ($data = '') + { + if (!is_array ($data)) + { + $noargs = 1; + $data = array (); + } + + $default_values = array + ( + 'string' => '/', + 'relative' => True, + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + if ($data['relatives'][0] & VFS_REAL) + { + $sep = SEP; + } + else + { + $sep = '/'; + } + + if ($data['relative'] == 'relative' || $data['relative'] == True) + { + /* if 'string' is "/" and 'relative' is set, we cd to the user/group home dir */ + if ($data['string'] == '/') + { + $data['relatives'][0] = RELATIVE_USER; + $basedir = $this->getabsolutepath (array( + 'string' => False, + 'mask' => array ($data['relatives'][0]), + 'fake' => True + ) + ); + } + else + { + $currentdir = $GLOBALS['phpgw']->session->appsession('vfs',''); + $basedir = $this->getabsolutepath (array( + 'string' => $currentdir . $sep . $data['string'], + 'mask' => array ($data['relatives'][0]), + 'fake' => True + ) + ); + } + } + else + { + $basedir = $this->getabsolutepath (array( + 'string' => $data['string'], + 'mask' => array ($data['relatives'][0]) + ) + ); + } + + $GLOBALS['phpgw']->session->appsession('vfs','',$basedir); + + return True; + } + + /*! + @function pwd + @abstract current working dir + @param full default True returns full fake path, else just the extra dirs (false strips the leading /) + @result $currentdir currentdir + */ + function pwd ($data = '') + { + $default_values = array + ( + 'full' => True + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $currentdir = $GLOBALS['phpgw']->session->appsession('vfs',''); + + if (!$data['full']) + { + $currentdir = ereg_replace ("^/", '', $currentdir); + } + + if ($currentdir == '' && $data['full']) + { + $currentdir = '/'; + } + + $currentdir = trim ($currentdir); + + return $currentdir; + } + + /*! + @function read + @abstract return file contents + @param string filename + @param relatives Relativity array + @result $contents Contents of $file, or False if file cannot be read + */ + function read ($data) + { + + /*If the user really wants to 'view' the file in the browser, it + is much smarter simply to redirect them to the files web-accessable + url */ +/* $app = $GLOBALS['phpgw_info']['flags']['currentapp']; + if ( ! $data['noview'] && ($app == 'phpwebhosting' || $app = 'filemanager' )) + { + $this->view($data); + } +*/ + if (!is_array ($data)) + { + $data = array (); + } + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_READ + )) + ) + { + return False; + } + if ($p->outside) + { + + if (! $fp = fopen ($p->real_full_path, 'r')) + { + return False; + } + $size=filesize($p->real_full_path); + $buffer=fread($fp, $size); + fclose ($fp); + return $buffer; + } + else + { + $status=$this->dav_client->get($p->real_full_path); + $this->debug($this->dav_client->get_headers()); + + if($status != 200) return False; + $contents=$this->dav_client->get_body(); + $this->debug('Read:returning contents. Status:'.$status); + return $contents; + } + } + + /* + @function view + @abstract Redirect the users browser to the file + @param string filename + @param relatives Relativity array + @result None (doesnt return) + @discussion In the case of WebDAV, the file is web-accessible. So instead + of reading it into memory and then dumping it back out again when someone + views a file, it makes much more sense to simply redirect, which is what + this method does (its only called when reading from the file in the file manager, + when the variable "noview" isnt set to "true" + */ + function view($data) + { + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + $data = array_merge ($this->default_values ($data, $default_values), $data); + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + //Determine whether the repository supports SSL + $parsed_url = parse_url($this->repository); + if ($parsed_url['scheme']=='https') + { + header( 'Location: '.$p->real_full_secure_url, true ); + } + else + { + header( 'Location: '.$p->real_full_auth_url, true ); + } + exit(); + + } + + /* + @function lock + @abstract DAV (class 2) locking - sets an exclusive write lock + @param string filename + @param relatives Relativity array + @result True if successfull + */ + function lock ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'timeout' => 'infinity', + + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + return $this->dav_client->lock($p->real_full_url, $this->dav_user, 0, $data['timeout']); + + } + function lock_token ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'token' => '' + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $ls_array = $GLOBALS['phpgw']->vfs->ls (array ( + 'string' => $data['string'], + 'relatives' => $data['relatives'] + ) + ); + $lock = @end($ls_array[0]['locks']); + $token = @end($lock['lock_tokens']); + return $token['full_name']; + } + + + /* + @function add_lock_override + @abstract override a lock + @param string filename + @param relatives Relativity array + @param token (optional) a token for the lock we want to override + @result None + @discussion locks are no good unless you can write to a file you yourself locked: + to do this call add_lock_override with the lock token (or without it - it will + find it itself, so long as there is only one). lock_override info is stored in + the groupware session, so it will persist between page loads, but will be lost + when the browser is closed + */ + function add_lock_override($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'token' => '' + + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + if (!strlen($data['token'])) + { + $ls_array = $GLOBALS['phpgw']->vfs->ls (array ( + 'string' => $data['string'], + 'relatives' => $data['relatives'] + ) + ); + $lock = @end($ls_array[0]['locks']); + $token_array = @end($lock['lock_tokens']); + $token = $token_array['full_name']; + } + else + { + $token = $data['token']; + } + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + $this->override_locks[$p->real_full_path] = $token; + $this->save_session(); + } + + /* + @function remove_lock_override + @abstract stops overriding a lock + @param string filename + @param relatives Relativity array + @result None + */ + function remove_lock_override($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + if (!strlen($data['token'])) + { + $ls_array = $GLOBALS['phpgw']->vfs->ls (array ( + 'string' => $data['string'], + 'relatives' => $data['relatives'] + ) + ); + $lock = @end($ls_array[0]['locks']); + $token_array = @end($lock['lock_tokens']); + $token = $token_array['full_name']; + } + else + { + $token = $data['token']; + } + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + unset($this->override_locks[$p->real_full_path]); + $this->save_session(); + } + + /* + @function unlock + @abstract DAV (class 2) unlocking - unsets the specified lock + @param string filename + @param relatives Relativity array + @param tocken The token for the lock we wish to remove. + @result True if successfull + */ + function unlock ($data, $token) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'content' => '' + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + $this->remove_lock_override (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + return $this->dav_client->unlock($p->real_full_url, $token); + + + } + + /* + @function options + @abstract Allows querying for optional features - esp optional DAV features + like locking + @param option The option you want to test for. Options include 'LOCKING' + 'VIEW', 'VERSION-CONTROL (eventually) etc + @result true if the specified option is supported + @discussion This should really check the server. Unfortunately the overhead of doing this + in every VFS instance is unacceptable (it essentially doubles the time for any request). Ideally + we would store these features in the session perhaps? + */ + function options($option) + { + switch ($option) + { + case 'LOCKING': + return true; + case 'VIEW': + return true; + default: + return false; + } + } + + /*! + @function write + @abstract write to a file + @param string file name + @param relatives Relativity array + @param content content + @result Boolean True/False + */ + function write ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'content' => '' + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if ($this->file_exists (array ( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + )) + ) + { + $acl_operation = PHPGW_ACL_EDIT; + $journal_operation = VFS_OPERATION_EDITED; + } + else + { + $acl_operation = PHPGW_ACL_ADD; + } + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => $acl_operation + )) + ) + { + return False; + } + + //umask(000); + + /* + If 'string' doesn't exist, touch () creates both the file and the database entry + If 'string' does exist, touch () sets the modification time and modified by + */ + /*$this->touch (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask) + ) + );*/ + + $size=strlen($data['content']); + if ($p->outside) + { + if (! $fp = fopen ($p->real_full_path, 'w')) + { + return False; + } + $result = fwrite($fp, $data['content']); + fclose ($fp); + return $result; + } + else + { + $token = $this->override_locks[$p->real_full_path]; + $status=$this->dav_client->put($p->real_full_path,$data['content'],$token); +$this->debug('Put complete, status: '.$status); + if($status!=201 && $status!=204) + { + return False; + } + else + { + return True; + } + } + } + + /*! + @function touch + @abstract Create blank file $file or set the modification time and modified by of $file to current time and user + @param string File to touch or set modifies + @param relatives Relativity array + @result Boolean True/False + */ + function touch ($data) + { + $default_values = array( + 'relatives' => array (RELATIVE_CURRENT) + ); + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $account_id = $GLOBALS['phpgw_info']['user']['account_id']; + $currentapp = $GLOBALS['phpgw_info']['flags']['currentapp']; + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + umask (000); + + /* + PHP's touch function will automatically decide whether to + create the file or set the modification time + */ + if($p->outside) + { + return @touch($p->real_full_path); + } + elseif ($this->file_exists (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask) + )) + ) + { + $result = $this->set_attributes (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'attributes' => array( + 'modifiedby_id' => $account_id, + 'modified' => $this->now + ))); + } + else + { + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_ADD + )) + ) return False; + $result = $this->write (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]), + 'content' => '' + )); + $this->set_attributes(array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'attributes' => array ( + 'createdby_id' => $account_id, + 'created' => $this->now, + 'app' => $currentapp + ))); + } + + return ($result); + } + + /*! + @function cp + @abstract copy file + @param from from file/directory + @param to to file/directory + @param relatives Relativity array + @result boolean True/False + */ + function cp ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT, RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $account_id = $GLOBALS['phpgw_info']['user']['account_id']; + + $f = $this->path_parts (array( + 'string' => $data['from'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + $t = $this->path_parts (array( + 'string' => $data['to'], + 'relatives' => array ($data['relatives'][1]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask), + 'operation' => PHPGW_ACL_READ + )) + ) + { + return False; + } + + if ($this->file_exists (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask) + )) + ) + { + $remote_operation=PHPGW_ACL_EDIT; + } + else + { + $remote_operation=PHPGW_ACL_ADD; + + } + if (!$this->acl_check (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'operation' => $remote_operation + )) + ) + { + return False; + } + + umask(000); + + if ($this->file_type (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask) + )) != 'Directory' + ) + { + + if ($f->outside && $t->outside) + { + return copy($f->real_full_path, $t->real_full_url); + } + elseif ($f->outside || $t->outside) + { + $content = $this->read(array( + 'string' => $f->fake_full_path, + 'noview' => true, + 'relatives' => array ($f->mask) + ) + ); + $result = $this->write(array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'content' => $content + ) + ); + } + else + { + $status=$this->dav_client->copy($f->real_full_path, $t->real_full_url,True, 'Infinity', $this->override_locks[$p->real_full_path]); + $result = $status == 204 || $status==201; + if (!$result) + { + return False; + } + } + + $this->set_attributes(array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'attributes' => array ( + 'owner_id' => $this->working_id, + 'createdby_id' => $account_id, + ) + ) + ); + return $result; + + } + else if (!($f->outside || $t->outside)) + { + //if the files are both on server, its just a depth=infinity copy + $status=$this->dav_client->copy($f->real_full_path, $t->real_full_url,True, 'infinity', $this->override_locks[$p->real_full_path]); + if($status != 204 && $status!=201) + { + return False; + } + else + { + return True; + } + } + else /* It's a directory, and one of the files is local */ + { + /* First, make the initial directory */ + $this->mkdir (array( + 'string' => $data['to'], + 'relatives' => array ($data['relatives'][1]) + ) + ); + + /* Next, we create all the directories below the initial directory */ + $ls = $this->ls (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask), + 'checksubdirs' => True, + 'mime_type' => 'Directory' + ) + ); + + while (list ($num, $entry) = each ($ls)) + { + $newdir = ereg_replace ("^$f->fake_full_path", "$t->fake_full_path", $entry['directory']); + $this->mkdir (array( + 'string' => $newdir.'/'.$entry['name'], + 'relatives' => array ($t->mask) + ) + ); + } + + /* Lastly, we copy the files over */ + $ls = $this->ls (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask) + ) + ); + + while (list ($num, $entry) = each ($ls)) + { + if ($entry['mime_type'] == 'Directory') + { + continue; + } + + $newdir = ereg_replace ("^$f->fake_full_path", "$t->fake_full_path", $entry['directory']); + $this->cp (array( + 'from' => "$entry[directory]/$entry[name]", + 'to' => "$newdir/$entry[name]", + 'relatives' => array ($f->mask, $t->mask) + ) + ); + } + } + + return True; + } + + function copy ($data) + { + return $this->cp ($data); + } + + /*! + @function mv + @abstract move file/directory + @param from from file/directory + @param to to file/directory + @param relatives Relativity array + @result boolean True/False + */ + function mv ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT, RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $account_id = $GLOBALS['phpgw_info']['user']['account_id']; + + $f = $this->path_parts (array( + 'string' => $data['from'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + $t = $this->path_parts (array( + 'string' => $data['to'], + 'relatives' => array ($data['relatives'][1]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask), + 'operation' => PHPGW_ACL_READ + )) + || !$this->acl_check (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask), + 'operation' => PHPGW_ACL_DELETE + )) + ) + { + return False; + } + + if (!$this->acl_check (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'operation' => PHPGW_ACL_ADD + )) + ) + { + return False; + } + + if ($this->file_exists (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask) + )) + ) + { + if (!$this->acl_check (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'operation' => PHPGW_ACL_EDIT + )) + ) + { + return False; + } + } + umask (000); + + /* We can't move directories into themselves */ + if (($this->file_type (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask) + ) == 'Directory')) + && ereg ("^$f->fake_full_path", $t->fake_full_path) + ) + { + if (($t->fake_full_path == $f->fake_full_path) || substr ($t->fake_full_path, strlen ($f->fake_full_path), 1) == '/') + { + return False; + } + } + + if ($this->file_exists (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask) + )) + ) + { + /* We get the listing now, because it will change after we update the database */ + $ls = $this->ls (array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask) + ) + ); + + if ($this->file_exists (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask) + )) + ) + { + $this->rm (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask) + ) + ); + } + + $this->correct_attributes (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask) + ) + ); + + if ($f->outside && $t->outside) + { + echo 'local'; + $result = rename ($f->real_full_path, $t->real_full_path); + } + else if ($f->outside || $t->outside) //if either file is local, read then write + { + $content = $this->read(array( + 'string' => $f->fake_full_path, + 'noview' => true, + 'relatives' => array ($f->mask) + ) + ); + $result = $this->write(array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'content' => $content + ) + ); + if ($result) + { + $result = $this->rm(array( + 'string' => $f->fake_full_path, + 'relatives' => array ($f->mask), + 'content' => $content + ) + ); + } + } + else { //we can do a server-side copy if both files are on the server + $status=$this->dav_client->move($f->real_full_path, $t->real_full_url,True, 'infinity', $this->override_locks[$p->real_full_path]); + $result = ($status==201 || $status==204); + } + + if ($result) $this->set_attributes(array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'attributes' => array ( + 'modifiedby_id' => $account_id, + 'modified' => $this->now + ))); + return $result; + } + else + { + return False; + } + + $this->add_journal (array( + 'string' => $t->fake_full_path, + 'relatives' => array ($t->mask), + 'operation' => VFS_OPERATION_MOVED, + 'state_one' => $f->fake_full_path, + 'state_two' => $t->fake_full_path + ) + ); + + return True; + } + + /*! + @function move + @abstract shortcut to mv + */ + function move ($data) + { + return $this->mv ($data); + } + + /*! + @function rm + @abstract delete file/directory + @param string file/directory to delete + @param relatives Relativity array + @result boolean True/False + */ + function rm ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + $this->debug("rm: $p->real_full_path"); + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_DELETE + )) + ) + { + return False; + } + +/*this would become apparent soon enough anyway? + if (!$this->file_exists (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + )) + ) return False; +*/ + if ($this->file_type (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + )) != 'Directory' + ) + { + if ($p->outside) + { + return unlink($p->real_full_path); + } + else + { + $rr=$this->dav_client->delete($p->real_full_path, 0, $this->override_locks[$p->real_full_path]); + return $rr == 204; + } + } + else + { + $ls = $this->ls (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask) + ) + ); + + while (list ($num, $entry) = each ($ls)) + { + $this->rm (array( + 'string' => "$entry[directory]/$entry[name]", + 'relatives' => array ($p->mask) + ) + ); + } + + /* If the directory is linked, we delete the placeholder directory */ + $ls_array = $this->ls (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'checksubdirs' => False, + 'mime_type' => False, + 'nofiles' => True + ) + ); + $link_info = $ls_array[0]; + + if ($link_info['link_directory'] && $link_info['link_name']) + { + $path = $this->path_parts (array( + 'string' => $link_info['directory'] . '/' . $link_info['name'], + 'relatives' => array ($p->mask), + 'nolinks' => True + ) + ); + $this->dav_client->delete($path->real_full_path,0, $this->override_locks[$p->real_full_path]); + } + + /* Last, we delete the directory itself */ + $this->add_journal (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operaton' => VFS_OPERATION_DELETED + ) + ); + + $query = $GLOBALS['phpgw']->db->query ("DELETE FROM phpgw_vfs WHERE directory='$p->fake_leading_dirs_clean' AND name='$p->fake_name_clean'" . $this->extra_sql (array ('query_type' => VFS_SQL_DELETE)), __LINE__, __FILE__); + + //rmdir ($p->real_full_path); + $this->dav_client->delete($p->real_full_path.'/','Infinity', $this->override_locks[$p->real_full_path]); + + return True; + } + } + + /*! + @function delete + @abstract shortcut to rm + */ + function delete ($data) + { + return $this->rm ($data); + } + + /*! + @function mkdir + @abstract make a new directory + @param string Directory name + @param relatives Relativity array + @result boolean True on success + */ + function mkdir ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $account_id = $GLOBALS['phpgw_info']['user']['account_id']; + $currentapp = $GLOBALS['phpgw_info']['flags']['currentapp']; + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_ADD) + ) + ) + { + return False; + } + + /* We don't allow /'s in dir names, of course */ + if (ereg ('/', $p->fake_name)) + { + return False; + } + if ($p->outside) + { + if (!mkdir($p->real_full_path, 0777)) + { + return False; + } + } + else if($this->dav_client->mkcol($p->real_full_path, $this->override_locks[$p->real_full_path]) != 201) + { + return False; + } + + + if (!$this->file_exists (array( + 'string' => $p->fake_full_path.'/' + )) + ) + { + /*Now we need to set access control for this dir. Simply create an .htaccess + file limiting access to this user, if we are creating this dir in the user's home dir*/ + $homedir = $this->fakebase.'/'.$this->dav_user; + if ( substr($p->fake_leading_dirs, 0, strlen($homedir)) == $homedir) + { + $conf = CreateObject('phpgwapi.config', 'phpgwapi'); + $conf->read_repository(); + if (!$conf->config_data['acl_default'] == 'grant') + { + $htaccess = 'require user '.$GLOBALS['phpgw_info']['user']['account_lid']; + if ( ! $this->write(array( + 'string' => $p->fake_full_path.'/.htaccess', + 'content' => $htaccess, + 'relatives' => array(RELATIVE_NONE) + ))) + { + echo '

Unable to write .htaccess file

'; + }; + } + } + return True; + } + else + { + return False; + } + } + + /*! + @function make_link + @abstract Make a link from virtual directory 'vdir' to real directory 'rdir' + @discussion Making a link from 'vdir' to 'rdir' will cause path_parts () to substitute 'rdir' for the real + path variables when presented with 'vdir' + @param vdir Virtual dir to make link from + @param rdir Real dir to make link to + @param relatives Relativity array + @result Boolean True/False + */ + function make_link ($data) + { + return False; //This code certainly wont work anymore. Does anything use it? + /* + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT, RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $account_id = $GLOBALS['phpgw_info']['user']['account_id']; + $currentapp = $GLOBALS['phpgw_info']['flags']['currentapp']; + + $vp = $this->path_parts (array( + 'string' => $data['vdir'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + $rp = $this->path_parts (array( + 'string' => $data['rdir'], + 'relatives' => array ($data['relatives'][1]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $vp->fake_full_path, + 'relatives' => array ($vp->mask), + 'operation' => PHPGW_ACL_ADD + )) + ) return False; + + if ((!$this->file_exists (array( + 'string' => $rp->real_full_path, + 'relatives' => array ($rp->mask) + ))) + && !mkdir ($rp->real_full_path, 0770)) return False; + + if (!$this->mkdir (array( + 'string' => $vp->fake_full_path, + 'relatives' => array ($vp->mask) + )) + )return False; + + $size = $this->get_size (array( + 'string' => $rp->real_full_path, + 'relatives' => array ($rp->mask) + ) + ); + + $this->set_attributes(array( + 'string' => $vp->fake_full_path, + 'relatives' => array ($vp->mask), + 'attributes' => array ( + 'link_directory' => $rp->real_leading_dirs, + 'link_name' => $rp->real_name, + 'size' => $size + ) + ) + ); + + $this->correct_attributes (array( + 'string' => $vp->fake_full_path, + 'relatives' => array ($vp->mask) + ) + ); + + return True; + */ + } + + /*! + @function set_attributes + @abstract Update database entry for 'string' with the attributes in 'attributes' + @param string file/directory to update + @param relatives Relativity array + @param attributes keyed array of attributes. key is attribute name, value is attribute value + @result Boolean True/False + @discussion Valid attributes are: + owner_id + createdby_id + modifiedby_id + created + modified + size + mime_type + deleteable + comment + app + link_directory + link_name + version + name + directory + */ + function set_attributes ($data,$operation=PHPGW_ACL_EDIT) + { + /*To get much benefit out of DAV properties we should use + some sensible XML namespace. We will use the Dublin Core + metadata specification (http://dublincore.org/) here where + we can*/ + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + )); + $dav_properties = array(); + $lid=''; $fname = ''; $lname=''; + if ($data['attributes']['comment']) + { + $dav_properties['dc:description'] = $data['attributes']['comment']; + } + if ($id=$data['attributes']['owner_id']) + { + $GLOBALS['phpgw']->accounts->get_account_name($id,&$lid,&$fname,&$lname); + $dav_properties['dc:publisher'] = $fname .' '. $lname; + $dav_properties['publisher_id'] = $id; + } + if ($id=$data['attributes']['createdby_id']) + { + $GLOBALS['phpgw']->accounts->get_account_name($id,&$lid,&$fname,&$lname); + $dav_properties['dc:creator'] = $fname .' '. $lname; + $dav_properties['creator_id'] = $id; + } + if ($id=$data['attributes']['modifiedby_id']) + { + $GLOBALS['phpgw']->accounts->get_account_name($id,&$lid,&$fname,&$lname); + $dav_properties['dc:contributor'] = $fname .' '. $lname; + $dav_properties['contributor_id'] = $id; + } + + $xmlns = 'xmlns:dc="http://purl.org/dc/elements/1.0/"'; + $this->dav_client->proppatch($p->real_full_path, $dav_properties, $xmlns, $this->override_locks[$p->real_full_path]); + return True; + } + + /*! + @function correct_attributes + @abstract Set the correct attributes for 'string' (e.g. owner) + @param string File/directory to correct attributes of + @param relatives Relativity array + @result Boolean True/False + */ + function correct_attributes ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); +$this->debug('correct_attributes: '.$data['string']); + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if ($p->fake_leading_dirs != $this->fakebase && $p->fake_leading_dirs != '/') + { + $ls_array = $this->ls (array( + 'string' => $p->fake_leading_dirs, + 'relatives' => array ($p->mask), + 'checksubdirs' => False, + 'nofiles' => True + ) + ); + $set_attributes_array = Array( + 'owner_id' => $ls_array[0]['owner_id'] + ); + } + elseif (preg_match ("+^$this->fakebase\/(.*)$+U", $p->fake_full_path, $matches)) + { + $set_attributes_array = Array( + 'owner_id' => $GLOBALS['phpgw']->accounts->name2id ($matches[1]) + ); + } + else + { + $set_attributes_array = Array( + 'owner_id' => 0 + ); + } + + $this->set_attributes (array( + 'string' => $p->fake_full_name, + 'relatives' => array ($p->mask), + 'attributes' => $set_attributes_array + ) + ); + + return True; + } + + /*! + @function file_type + @abstract return file/dir type (MIME or other) + @param string File or directory path (/home/user/dir/dir2/dir3, /home/user/dir/dir2/file) + @param relatives Relativity array + @result MIME type, "Directory", or nothing if MIME type is not known + */ + function file_type ($data) + { +$this->debug('file_type'); + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_READ, + 'must_exist' => True + )) + ) return False; + + if ($p->outside) + { + if(is_dir($p->real_full_path)) return ('Directory'); + else return $this->get_ext_mime_type(array('string' => $p->real_full_path)); + + } + $tmp_prop=$this->dav_client->get_properties($p->real_full_path); +$this->debug('tmpprop: '.$p->real_full_path); +$this->debug($tmp_prop); + $mime_type=$tmp_prop[$p->real_full_path]['mime_type']; + if ($mime_type == 'httpd/unix-directory' || $tmp_prop[$p->real_full_path]['is_dir']== '1') + { + $mime_type='Directory'; + } +$this->debug('file_type: Mime type : '.$mime_type); + return $mime_type; + } + + /*! + @function get_ext_mime_type + @abstract return MIME type based on file extension + @description Authors: skeeter + Internal use only. Applications should call vfs->file_type () + @param string File name, with or without leading paths + @result MIME type based on file extension + */ + function get_ext_mime_type ($data) + { + $file=basename($data['string']); + $mimefile=PHPGW_API_INC.'/phpgw_mime.types'; + $fp=fopen($mimefile,'r'); + $contents = explode("\n",fread($fp,filesize($mimefile))); + fclose($fp); + + $parts=explode('.',strtolower($file)); + $ext=$parts[(sizeof($parts)-1)]; + + for($i=0;$i= 2) + { + for($j=1;$j array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); +$this->debug('vfs->file_exists() data:'.$data['string']); +$this->debug('vfs->file_exists() full_path: '.$p->real_full_path); + if ($p->outside) + { + return file_exists($p->real_full_path); + } + + $path = $p->real_full_path; + + //Even though this does full XML parsing on the output, because + // it then caches the result this limits the amount of traffic to + //the dav server (which makes it faster even over a local connection) + $props = $this->dav_client->get_properties($path); + if ($props[$path]) + { + $this->debug('found'); + return True; + } + else + { + $this->debug('not found'); + return False; + } + } + + /*! + @function get_size + @abstract Return size of 'string' + @param string file/directory to get size of + @param relatives Relativity array + @param checksubdirs Boolean, recursively add the size of all sub directories as well? + @result Size of 'string' in bytes + */ + function get_size ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'checksubdirs' => True + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_READ, + 'must_exist' => True + )) + ) + { + return False; + } + + /* + WIP - this should run through all of the subfiles/directories in the directory and tally up + their sizes. Should modify ls () to be able to return a list for files outside the virtual root + */ + if ($p->outside){ + return filesize($p->real_full_path); + } + + $ls_array = $this->ls (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'checksubdirs' => $data['checksubdirs'], + 'nofiles' => !$data['checksubdirs'] + ) + ); + + while (list ($num, $file_array) = each ($ls_array)) + { + /* + Make sure the file is in the directory we want, and not + some deeper nested directory with a similar name + */ +/* + if (@!ereg ('^' . $file_array['directory'], $p->fake_full_path)) + { + continue; + } +*/ + + $size += $file_array['size']; +$this->debug('size:getting size from fs: '.$size); + } + + return $size; + } + + /*! + @function checkperms + @abstract Check if $this->working_id has write access to create files in $dir + @discussion Simple call to acl_check + @param string Directory to check access of + @param relatives Relativity array + @result Boolean True/False + */ + function checkperms ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (!$this->acl_check (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask), + 'operation' => PHPGW_ACL_ADD + )) + ) + { + return False; + } + else + { + return True; + } + } + + /*! + @function ls + @abstract get directory listing or info about a single file + @discussion Note: The entries are not guaranteed to be returned in any logical order + Note: The size for directories does not include subfiles/subdirectories. + If you need that, use $this->get_size () + @param string File or Directory + @param relatives Relativity array + @param checksubdirs Boolean, recursively list all sub directories as well? + @param mime_type Only return entries matching MIME-type 'mime_type'. Can be any MIME-type, "Directory" or "\ " for those without MIME types + @param nofiles Boolean. True means you want to return just the information about the directory $dir. If $dir is a file, $nofiles is implied. This is the equivalent of 'ls -ld $dir' + @param orderby How to order results. Note that this only works for directories inside the virtual root + @result array of arrays. Subarrays contain full info for each file/dir. + */ + function ls ($data) + { + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT), + 'checksubdirs' => True, + 'mime_type' => False, + 'nofiles' => False, + 'orderby' => 'directory' + ); + $data = array_merge ($this->default_values ($data, $default_values), $data); + //Stupid "nofiles" fix" + if ($data['nofiles']) + { + $data['relatives'] = array (RELATIVE_NONE); + } + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if ($data['checksubdirs']==False && ereg('.*/$', $data['string']) && $data['nofiles'] ) + { +$this->debug('Returning empty for'.$data['string']); + return array(); + } + $dir = $p->fake_full_path; +$this->debug("ls'ing dir: $dir path: ".$p->real_full_path); + /* If they pass us a file or 'nofiles' is set, return the info for $dir only */ + if (((($type = $this->file_type (array( + 'string' => $dir, + 'relatives' => array ($p->mask) + )) != 'Directory')) + || ($data['nofiles'])) && !$p->outside + ) + { +$this->debug('ls branch 1'); + $prop=$this->dav_client->get_properties($p->real_full_path, 1); + //make the key the 'orderby' attribute + if (! ($data['orderby'] == 'directory')) + { + $tmp_prop = array(); + $id=0; + foreach ( $prop as $key=>$value) + { + $id++; + $new_key = substr($value[$data['orderby']].' ',0, 8); + $tmp_prop[strtolower($new_key).'_'.$id] = $value; + } + } + else + { + $tmp_prop = $prop; + } + ksort($tmp_prop); + $rarray = array (); + foreach($tmp_prop as $idx => $value) + { + if($value['mime_type']==$data['mime_type'] or $data['mime_type']=='') + { + $directory = $this->path_parts($value['directory']); + $value['directory'] = $directory->fake_full_path; + if($value['is_dir']) $value['mime_type']='Directory'; + $rarray[] = $value; + } + } +$this->debug('ls returning 1:'); + return $rarray; + } + + //WIP - this should recurse using the same options the virtual part of ls () does + /* If $dir is outside the virutal root, we have to check the file system manually */ + if ($p->outside) + { +$this->debug('ls branch 2 (outside)'); + if ($this->file_type (array( + 'string' => $p->fake_full_path, + 'relatives' => array ($p->mask) + )) == 'Directory' + && !$data['nofiles'] + ) + { + $dir_handle = opendir ($p->real_full_path); + while ($filename = readdir ($dir_handle)) + { + if ($filename == '.' || $filename == '..') + { + continue; + } + + $rarray[] = $this->get_real_info (array( + 'string' => $p->real_full_path . SEP . $filename, + 'relatives' => array ($p->mask) + ) + ); + } + } + else + { + $rarray[] = $this->get_real_info (array( + 'string' => $p->real_full_path, + 'relatives' => array ($p->mask) + ) + ); + } +$this->debug('ls returning 2:'); + return $rarray; + } +$this->debug('ls branch 3'); + /* $dir's not a file, is inside the virtual root, and they want to check subdirs */ + $prop=$this->dav_client->get_properties($p->real_full_path,1); + unset($prop[$p->real_full_path]); + //make the key the 'orderby' attribute + + if (! ($data['orderby'] == 'directory')) + { + $tmp_prop = array(); + $id=0; + foreach ( $prop as $key=>$value) + { + $id++; + $new_key = substr($value[$data['orderby']].' ',0, 8); + $tmp_prop[strtolower($new_key).'_'.$id] = $value; + } + } + else + { + $tmp_prop = $prop; + } + + ksort($tmp_prop); + + unset($tmp_prop[ $p->real_full_path]); + $rarray = array (); + foreach($tmp_prop as $idx => $value) + { + if($data['mime_type']=='' || $value['mime_type']==$data['mime_type']) + { + //$directory = $this->path_parts($value['directory']); + $value['directory'] = $p->fake_full_path; + $rarray[] = $value; + } + } +$this->debug('ls:returning 3:'); + return $rarray; + } + + /*! + @function dir + @abstract shortcut to ls + */ + function dir ($data) + { + return $this->ls ($data); + } + + /*! + @function command_line + @abstract Process and run a Unix-sytle command line + @discussion EXPERIMENTAL. DANGEROUS. DO NOT USE THIS UNLESS YOU KNOW WHAT YOU'RE DOING! + This is mostly working, but the command parser needs to be improved to take + files with spaces into consideration (those should be in ""). + @param command_line Unix-style command line with one of the commands in the $args array + @result $result The return value of the actual VFS call + */ + function command_line ($data) + { + if (!is_array ($data)) + { + $data = array (); + } + + $args = array + ( + array ('name' => 'mv', 'params' => 2), + array ('name' => 'cp', 'params' => 2), + array ('name' => 'rm', 'params' => 1), + array ('name' => 'ls', 'params' => -1), + array ('name' => 'du', 'params' => 1, 'func' => get_size), + array ('name' => 'cd', 'params' => 1), + array ('name' => 'pwd', 'params' => 0), + array ('name' => 'cat', 'params' => 1, 'func' => read), + array ('name' => 'file', 'params' => 1, 'func' => file_type), + array ('name' => 'mkdir', 'params' => 1), + array ('name' => 'touch', 'params' => 1) + ); + + if (!$first_space = strpos ($data['command_line'], ' ')) + { + $first_space = strlen ($data['command_line']); + } + if ((!$last_space = strrpos ($data['command_line'], ' ')) || ($last_space == $first_space)) + { + $last_space = strlen ($data['command_line']) + 1; + } + $argv[0] = substr ($data['command_line'], 0, $first_space); + if (strlen ($argv[0]) != strlen ($data['command_line'])) + { + $argv[1] = substr ($data['command_line'], $first_space + 1, $last_space - ($first_space + 1)); + if ((strlen ($argv[0]) + 1 + strlen ($argv[1])) != strlen ($data['command_line'])) + { + $argv[2] = substr ($data['command_line'], $last_space + 1); + } + } + $argc = count ($argv); + + reset ($args); + while (list (,$arg_info) = each ($args)) + { + if ($arg_info['name'] == $argv[0]) + { + $command_ok = 1; + if (($argc == ($arg_info['params'] + 1)) || ($arg_info['params'] == -1)) + { + $param_count_ok = 1; + } + break; + } + } + + if (!$command_ok) + { +// return E_VFS_BAD_COMMAND; + return False; + } + if (!$param_count_ok) + { +// return E_VFS_BAD_PARAM_COUNT; + return False; + } + + for ($i = 1; $i != ($arg_info['params'] + 1); $i++) + { + if (substr ($argv[$i], 0, 1) == '/') + { + $relatives[] = RELATIVE_NONE; + } + else + { + $relatives[] = RELATIVE_ALL; + } + } + + $func = $arg_info['func'] ? $arg_info['func'] : $arg_info['name']; + + if (!$argv[2]) + { + $rv = $this->$func (array( + 'string' => $argv[1], + 'relatives' => $relatives + ) + ); + } + else + { + $rv = $this->$func (array( + 'from' => $argv[1], + 'to' => $argv[2], + 'relatives' => $relatives + ) + ); + } + + return ($rv); + } + + /* Helper functions */ + + function default_values ($data, $default_values) + { + if(!is_array($data)) $data=array(); + for ($i = 0; list ($key, $value) = each ($default_values); $i++) + { + if (!isset ($data[$key])) + { + $data[$key] = $value; + } + } + + return $data; + } + + /* Since we are always dealing with real info, this just calls ls */ + function get_real_info ($data){ + if (!is_array ($data)) + { + $data = array (); + } + + $default_values = array + ( + 'relatives' => array (RELATIVE_CURRENT) + ); + + $data = array_merge ($this->default_values ($data, $default_values), $data); + + $p = $this->path_parts (array( + 'string' => $data['string'], + 'relatives' => array ($data['relatives'][0]) + ) + ); + + if (is_dir ($p->real_full_path)) + { + $mime_type = 'Directory'; + } + else + { + $mime_type = $this->get_ext_mime_type (array( + 'string' => $p->fake_name + ) + ); + + if($mime_type) + { + $GLOBALS['phpgw']->db->query ("UPDATE phpgw_vfs SET mime_type='".$mime_type."' WHERE directory='".$p->fake_leading_dirs_clean."' AND name='".$p->fake_name_clean."'" . $this->extra_sql (array ('query_type' => VFS_SQL_SELECT)), __LINE__, __FILE__); + } + } + + $size = filesize ($p->real_full_path); + $rarray = array( + 'directory' => $p->fake_leading_dirs, + 'name' => $p->fake_name, + 'size' => $size, + 'mime_type' => $mime_type + ); + + return ($rarray); + } + + function update_real() + { //hmmm. things break without this, but it does nothing in this implementation + return True; + } + + function save_session() + { + //Save the overrided locks in the session + $app = $GLOBALS['phpgw_info']['flags']['currentapp']; + $a = array(); + foreach ($this->override_locks as $name => $token) + { + $a[] = $name.';'.$token; + } + $session_data = implode('\n', $a); + $this->session = $GLOBALS['phpgw']->session->appsession ('vfs_dav',$app, base64_encode($session_data)); + + } + } +?>