From d6c6dc2de064e2d50ce178a56fc716d2e847b54e Mon Sep 17 00:00:00 2001
From: Ralf Becker ".__METHOD__."($form_name,$value,".array2string($cell).",...) ".__METHOD__."($path) returning ".array2string($found)." '.__METHOD__."('$name',".array2string($value).','.array2string($extension_data).",$loop,,".array2string($value_in).") '.lang('%1 the following files into current directory',
- $clipboard_type=='copy'?lang('Copy'):lang('Move')).':Index of ".htmlspecialchars($options['path'])."
\n";
+
+ echo "";
+ printf($format, "Size", "Last modified", "Filename");
+ echo "
";
+
+ closedir($handle);
+
+ echo "\n";
+
+ exit;
+ }
+
+ /**
+ * PUT method handler
+ *
+ * @param array parameter passing array
+ * @return bool true on success
+ */
+ function PUT(&$options)
+ {
+ $fspath = $this->base . $options["path"];
+
+ if (!@is_dir(dirname($fspath))) {
+ return "409 Conflict";
+ }
+
+ $options["new"] = ! file_exists($fspath);
+
+ $fp = fopen($fspath, "w");
+
+ return $fp;
+ }
+
+
+ /**
+ * MKCOL method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function MKCOL($options)
+ {
+ $path = $this->base .$options["path"];
+ $parent = dirname($path);
+ $name = basename($path);
+
+ if (!file_exists($parent)) {
+ return "409 Conflict";
+ }
+
+ if (!is_dir($parent)) {
+ return "403 Forbidden";
+ }
+
+ if ( file_exists($parent."/".$name) ) {
+ return "405 Method not allowed";
+ }
+
+ if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
+ return "415 Unsupported media type";
+ }
+
+ $stat = mkdir($parent."/".$name, 0777);
+ if (!$stat) {
+ return "403 Forbidden";
+ }
+
+ return ("201 Created");
+ }
+
+
+ /**
+ * DELETE method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function DELETE($options)
+ {
+ $path = $this->base . "/" .$options["path"];
+
+ if (!file_exists($path)) {
+ return "404 Not found";
+ }
+
+ if (is_dir($path)) {
+ $query = "DELETE FROM {$this->db_prefix}properties
+ WHERE path LIKE '".$this->_slashify($options["path"])."%'";
+ mysql_query($query);
+ System::rm("-rf $path");
+ } else {
+ unlink($path);
+ }
+ $query = "DELETE FROM {$this->db_prefix}properties
+ WHERE path = '$options[path]'";
+ mysql_query($query);
+
+ return "204 No Content";
+ }
+
+
+ /**
+ * MOVE method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function MOVE($options)
+ {
+ return $this->COPY($options, true);
+ }
+
+ /**
+ * COPY method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function COPY($options, $del=false)
+ {
+ // TODO Property updates still broken (Litmus should detect this?)
+
+ if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
+ return "415 Unsupported media type";
+ }
+
+ // no copying to different WebDAV Servers yet
+ if (isset($options["dest_url"])) {
+ return "502 bad gateway";
+ }
+
+ $source = $this->base .$options["path"];
+ if (!file_exists($source)) return "404 Not found";
+
+ $dest = $this->base . $options["dest"];
+ $new = !file_exists($dest);
+ $existing_col = false;
+
+ if (!$new) {
+ if ($del && is_dir($dest)) {
+ if (!$options["overwrite"]) {
+ return "412 precondition failed";
+ }
+ $dest .= basename($source);
+ if (file_exists($dest)) {
+ $options["dest"] .= basename($source);
+ } else {
+ $new = true;
+ $existing_col = true;
+ }
+ }
+ }
+
+ if (!$new) {
+ if ($options["overwrite"]) {
+ $stat = $this->DELETE(array("path" => $options["dest"]));
+ if (($stat{0} != "2") && (substr($stat, 0, 3) != "404")) {
+ return $stat;
+ }
+ } else {
+ return "412 precondition failed";
+ }
+ }
+
+ if (is_dir($source) && ($options["depth"] != "infinity")) {
+ // RFC 2518 Section 9.2, last paragraph
+ return "400 Bad request";
+ }
+
+ if ($del) {
+ if (!rename($source, $dest)) {
+ return "500 Internal server error";
+ }
+ $destpath = $this->_unslashify($options["dest"]);
+ if (is_dir($source)) {
+ $query = "UPDATE {$this->db_prefix}properties
+ SET path = REPLACE(path, '".$options["path"]."', '".$destpath."')
+ WHERE path LIKE '".$this->_slashify($options["path"])."%'";
+ mysql_query($query);
+ }
+
+ $query = "UPDATE {$this->db_prefix}properties
+ SET path = '".$destpath."'
+ WHERE path = '".$options["path"]."'";
+ mysql_query($query);
+ } else {
+ if (is_dir($source)) {
+ $files = System::find($source);
+ $files = array_reverse($files);
+ } else {
+ $files = array($source);
+ }
+
+ if (!is_array($files) || empty($files)) {
+ return "500 Internal server error";
+ }
+
+
+ foreach ($files as $file) {
+ if (is_dir($file)) {
+ $file = $this->_slashify($file);
+ }
+
+ $destfile = str_replace($source, $dest, $file);
+
+ if (is_dir($file)) {
+ if (!is_dir($destfile)) {
+ // TODO "mkdir -p" here? (only natively supported by PHP 5)
+ if (!@mkdir($destfile)) {
+ return "409 Conflict";
+ }
+ }
+ } else {
+ if (!@copy($file, $destfile)) {
+ return "409 Conflict";
+ }
+ }
+ }
+
+ $query = "INSERT INTO {$this->db_prefix}properties
+ SELECT *
+ FROM {$this->db_prefix}properties
+ WHERE path = '".$options['path']."'";
+ }
+
+ return ($new && !$existing_col) ? "201 Created" : "204 No Content";
+ }
+
+ /**
+ * PROPPATCH method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function PROPPATCH(&$options)
+ {
+ global $prefs, $tab;
+
+ $msg = "";
+ $path = $options["path"];
+ $dir = dirname($path)."/";
+ $base = basename($path);
+
+ foreach ($options["props"] as $key => $prop) {
+ if ($prop["ns"] == "DAV:") {
+ $options["props"][$key]['status'] = "403 Forbidden";
+ } else {
+ if (isset($prop["val"])) {
+ $query = "REPLACE INTO {$this->db_prefix}properties
+ SET path = '$options[path]'
+ , name = '$prop[name]'
+ , ns= '$prop[ns]'
+ , value = '$prop[val]'";
+ } else {
+ $query = "DELETE FROM {$this->db_prefix}properties
+ WHERE path = '$options[path]'
+ AND name = '$prop[name]'
+ AND ns = '$prop[ns]'";
+ }
+ mysql_query($query);
+ }
+ }
+
+ return "";
+ }
+
+
+ /**
+ * LOCK method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function LOCK(&$options)
+ {
+ // get absolute fs path to requested resource
+ $fspath = $this->base . $options["path"];
+
+ // TODO recursive locks on directories not supported yet
+ if (is_dir($fspath) && !empty($options["depth"])) {
+ return "409 Conflict";
+ }
+
+ $options["timeout"] = time()+300; // 5min. hardcoded
+
+ if (isset($options["update"])) { // Lock Update
+ $where = "WHERE path = '$options[path]' AND token = '$options[update]'";
+
+ $query = "SELECT owner, exclusivelock FROM {$this->db_prefix}locks $where";
+ $res = mysql_query($query);
+ $row = mysql_fetch_assoc($res);
+ mysql_free_result($res);
+
+ if (is_array($row)) {
+ $query = "UPDATE {$this->db_prefix}locks
+ SET expires = '$options[timeout]'
+ , modified = ".time()."
+ $where";
+ mysql_query($query);
+
+ $options['owner'] = $row['owner'];
+ $options['scope'] = $row["exclusivelock"] ? "exclusive" : "shared";
+ $options['type'] = $row["exclusivelock"] ? "write" : "read";
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ $query = "INSERT INTO {$this->db_prefix}locks
+ SET token = '$options[locktoken]'
+ , path = '$options[path]'
+ , created = ".time()."
+ , modified = ".time()."
+ , owner = '$options[owner]'
+ , expires = '$options[timeout]'
+ , exclusivelock = " .($options['scope'] === "exclusive" ? "1" : "0")
+ ;
+ mysql_query($query);
+
+ return mysql_affected_rows() ? "200 OK" : "409 Conflict";
+ }
+
+ /**
+ * UNLOCK method handler
+ *
+ * @param array general parameter passing array
+ * @return bool true on success
+ */
+ function UNLOCK(&$options)
+ {
+ $query = "DELETE FROM {$this->db_prefix}locks
+ WHERE path = '$options[path]'
+ AND token = '$options[token]'";
+ mysql_query($query);
+
+ return mysql_affected_rows() ? "204 No Content" : "409 Conflict";
+ }
+
+ /**
+ * checkLock() helper
+ *
+ * @param string resource path to check for locks
+ * @return bool true on success
+ */
+ function checkLock($path)
+ {
+ $result = false;
+
+ $query = "SELECT owner, token, created, modified, expires, exclusivelock
+ FROM {$this->db_prefix}locks
+ WHERE path = '$path'
+ ";
+ $res = mysql_query($query);
+
+ if ($res) {
+ $row = mysql_fetch_array($res);
+ mysql_free_result($res);
+
+ if ($row) {
+ $result = array( "type" => "write",
+ "scope" => $row["exclusivelock"] ? "exclusive" : "shared",
+ "depth" => 0,
+ "owner" => $row['owner'],
+ "token" => $row['token'],
+ "created" => $row['created'],
+ "modified" => $row['modified'],
+ "expires" => $row['expires']
+ );
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * create database tables for property and lock storage
+ *
+ * @param void
+ * @return bool true on success
+ */
+ function create_database()
+ {
+ // TODO
+ return false;
+ }
+}
+
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * indent-tabs-mode:nil
+ * End:
+ */
diff --git a/etemplate/inc/class.vfs_widget.inc.php b/etemplate/inc/class.vfs_widget.inc.php
new file mode 100644
index 0000000000..9d4c294f88
--- /dev/null
+++ b/etemplate/inc/class.vfs_widget.inc.php
@@ -0,0 +1,538 @@
+
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @copyright 2008-10 by RalfBecker@outdoor-training.de
+ * @package etemplate
+ * @subpackage extensions
+ * @version $Id$
+ */
+
+/**
+ * eTemplate extension to display stuff from the VFS system
+ *
+ * Contains the following widgets:
+ * - vfs aka File name+link: clickable filename, with evtl. clickable path-components
+ * - vfs-name aka Filename: filename automatically urlencoded on return (urldecoded on display to user)
+ * - vfs-size aka File size: human readable filesize, eg. 1.4k
+ * - vfs-mode aka File mode: posix mode as string eg. drwxr-x---
+ * - vfs-mime aka File icon: mime type icon or thumbnail (if configured AND enabled in the user-prefs)
+ * - vfs-uid aka File owner: Owner of file, or 'root' if none
+ * - vfs-gid aka File group: Group of file, or 'root' if none
+ * - vfs-upload aka VFS file: displays either download and delete (x) links or a file upload
+ * + value is either a vfs path or colon separated $app:$id:$relative_path, eg: infolog:123:special/offer
+ * + if empty($id) / new entry, file is created in a hidden temporary directory in users home directory
+ * and calling app is responsible to move content of that dir to entry directory, after entry is saved
+ * + option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files
+ * + if path ends in a slash, multiple files can be uploaded, their original filename is kept then
+ *
+ * All widgets accept as value a full path.
+ * vfs-mime and vfs itself also allow an array with values like stat (incl. 'path'!) as value.
+ * vfs-mime also allows just the mime type as value.
+ * All other widgets allow additionally the nummeric value from the stat call (to not call it again).
+ */
+class vfs_widget
+{
+ /**
+ * exported methods of this class
+ *
+ * @var array
+ */
+ var $public_functions = array(
+ 'pre_process' => True,
+ 'post_process' => true, // post_process is only used for vfs-upload (all other widgets set $cell['readlonly']!)
+ );
+ /**
+ * availible extensions and there names for the editor
+ *
+ * @var array
+ */
+ var $human_name = array(
+ 'vfs' => 'File name+link', // clickable filename, with evtl. clickable path-components
+ 'vfs-name' => 'File name', // filename automatically urlencoded
+ 'vfs-size' => 'File size', // human readable filesize
+ 'vfs-mode' => 'File mode', // posix mode as string eg. drwxr-x---
+ 'vfs-mime' => 'File icon', // mime type icon or thumbnail
+ 'vfs-uid' => 'File owner', // Owner of file, or 'root' if none
+ 'vfs-gid' => 'File group', // Group of file, or 'root' if none
+ 'vfs-upload' => 'VFS file', // displays either download and delete (x) links or a file upload
+ );
+
+ /**
+ * pre-processing of the extension
+ *
+ * This function is called before the extension gets rendered
+ *
+ * @param string $form_name form-name of the control
+ * @param mixed &$value value / existing content, can be modified
+ * @param array &$cell array with the widget, can be modified for ui-independent widgets
+ * @param array &$readonlys names of widgets as key, to be made readonly
+ * @param mixed &$extension_data data the extension can store persisten between pre- and post-process
+ * @param object &$tmpl reference to the template we belong too
+ * @return boolean true if extra label is allowed, false otherwise
+ */
+ function pre_process($form_name,&$value,&$cell,&$readonlys,&$extension_data,&$tmpl)
+ {
+ //echo "
";
+
+ while ($filename = readdir($handle)) {
+ if ($filename != "." && $filename != "..") {
+ $fullpath = $fspath.$filename;
+ $name = htmlspecialchars($filename);
+ printf($format,
+ number_format(filesize($fullpath)),
+ strftime("%Y-%m-%d %H:%M:%S", filemtime($fullpath)),
+ ''.$name.'');
+ }
+ }
+
+ echo "
'.urldecode(implode('
',$clipboard_files)).'
'.egw_vfs::decodePath(implode('
',$clipboard_files)).'
'.lang('%1 the following files into current directory',
- lang('link')).':
'.urldecode(implode('
',$clipboard_files)).'
egw_link::link('$app1',$id1,'".print_r($app2,true)."',".print_r($id2,true).",'$remark',$owner,$lastmod)
\n"; + } + if (!$app1 || !$app2 || $app1 == $app2 && $id1 == $id2) + { + return False; + } + if (is_array($id1) || !$id1) // create link only in $id1 array + { + if (!is_array($id1)) + { + $id1 = array( ); + } + $link_id = self::temp_link_id($app2,$id2); + + $id1[$link_id] = array( + 'app' => $app2, + 'id' => $id2, + 'remark' => $remark, + 'owner' => $owner, + 'link_id' => $link_id, + 'lastmod' => time() + ); + if (self::DEBUG) + { + _debug_array($id1); + } + return $link_id; + } + if (is_array($app2) && !$id2) + { + reset($app2); + $link_id = True; + while ($link_id && list(,$link) = each($app2)) + { + if (!is_array($link)) // check for unlink-marker + { + //echo "link='$link' is no arrayegw_link::get_links(app='$app',id='$id',only_app='$only_app',order='$order')
\n"; + + if (is_array($id) || !$id) + { + $ids = array(); + if (is_array($id)) + { + if (($not_only = $only_app[0] == '!')) + { + $only_app = substr(1,$only_app); + } + foreach (array_reverse($id) as $link) + { + if (is_array($link) // check for unlink-marker + && !($only_app && $not_only == ($link['app'] == $only_app))) + { + $ids[$link['link_id']] = $only_app ? $link['id'] : $link; + } + } + } + return $ids; + } + $ids = solink::get_links($app,$id,$only_app,$order); + if (empty($only_app) || $only_app == self::VFS_APPNAME || + ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) + { + if ($vfs_ids = self::list_attached($app,$id)) + { + $ids += $vfs_ids; + } + } + //echo "ids="; print_r($ids); echo "\n"; + if ($cache_titles) + { + // agregate links by app + $app_ids = array(); + foreach($ids as $link) + { + $app_ids[$link['app']][] = $link['id']; + } + foreach($app_ids as $appname => $a_ids) + { + self::titles($appname,array_unique($a_ids)); + } + // remove links, current user has no access, from result + foreach($ids as $key => $link) + { + if (!self::title($link['app'],$link['id'])) + { + unset($ids[$key]); + } + } + reset($ids); + } + return $ids; + } + + /** + * Query the links of multiple entries of one application + * + * @ToDo also query the attachments in a single query, eg. via a directory listing of /apps/$app + * @param string $app + * @param array $ids + * @param boolean $cache_titles=true should all titles be queryed and cached (allows to query each link app only once!) + * @param string $only_app if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' + * @param string $order='link_lastmod DESC' defaults to newest links first + * @return array of $id => array($links) pairs + */ + static function get_links_multiple($app,array $ids,$cache_titles=true,$only_app='',$order='link_lastmod DESC' ) + { + if (self::DEBUG) echo "
".__METHOD__."('$app',".print_r($ids,true).",$cache_titles,'$only_app','$order')
\n"; + + if (!$ids) + { + return array(); // no ids are linked to nothing + } + $links = solink::get_links($app,$ids,$only_app,$order); + + if (empty($only_app) || $only_app == self::VFS_APPNAME || + ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) + { + // todo do that in a single query, eg. directory listing, too + foreach($ids as $id) + { + if (!isset($links[$id])) + { + $links[$id] = array(); + } + if ($vfs_ids = self::list_attached($app,$id)) + { + $links[$id] += $vfs_ids; + } + } + } + if ($cache_titles) + { + // agregate links by app + $app_ids = array(); + foreach($links as $src_id => &$targets) + { + foreach($targets as $link) + { + $app_ids[$link['app']][] = $link['id']; + } + } + foreach($app_ids as $app => $a_ids) + { + self::titles($app,array_unique($a_ids)); + } + } + return $links; + } + + /** + * Read one link specified by it's link_id or by the two end-points + * + * If $id is an array (links not yet created) only link_ids are allowed. + * + * @param int/string $app_link_id > 0 link_id of link or app-name of link + * @param string/array $id='' id if $app_link_id is an appname or array with links, if 1. entry not yet created + * @param string $app2='' second app + * @param string $id2='' id in $app2 + * @return array with link-data or False + */ + static function get_link($app_link_id,$id='',$app2='',$id2='') + { + if (self::DEBUG) + { + echo ''.__METHOD__."($app_link_id,$id,$app2,$id2)
\n"; echo function_backtrace(); + } + if (is_array($id)) + { + if (strpos($app_link_id,':') === false) $app_link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given + + if (isset($id[$app_link_id]) && is_array($id[$app_link_id])) // check for unlinked-marker + { + return $id[$app_link_id]; + } + return False; + } + if ((int)$app_link_id < 0 || $app_link_id == self::VFS_APPNAME || $app2 == self::VFS_APPNAME) + { + if ((int)$app_link_id < 0) // vfs link_id ? + { + return self::fileinfo2link(-$app_link_id); + } + if ($app_link_id == self::VFS_APPNAME) + { + return self::info_attached($app2,$id2,$id); + } + return self::info_attached($app_link_id,$id,$id2); + } + return solink::get_link($app_link_id,$id,$app2,$id2); + } + + /** + * Remove link with $link_id or all links matching given $app,$id + * + * Note: if $link_id != '' and $id is an array: unlink removes links from that array only + * unlink has to be called with &$id to see the result (depricated) or unlink2 has to be used !!! + * + * @param $link_id link-id to remove if > 0 + * @param string $app='' appname of first endpoint + * @param string/array $id='' id in $app or array with links, if 1. entry not yet created + * @param string $app2='' app of second endpoint + * @param string $id2='' id in $app2 + * @return the number of links deleted + */ + static function unlink($link_id,$app='',$id='',$owner='',$app2='',$id2='') + { + return self::unlink2($link_id,$app,$id,$owner,$app2,$id2); + } + + /** + * Remove link with $link_id or all links matching given $app,$id + * + * @param $link_id link-id to remove if > 0 + * @param string $app='' appname of first endpoint + * @param string/array &$id='' id in $app or array with links, if 1. entry not yet created + * @param string $app2='' app of second endpoint, or !file (other !app are not yet supported!) + * @param string $id2='' id in $app2 + * @return the number of links deleted + */ + static function unlink2($link_id,$app,&$id,$owner='',$app2='',$id2='') + { + if (self::DEBUG) + { + echo "egw_link::unlink('$link_id','$app','$id','$owner','$app2','$id2')
\n"; + } + if ($link_id < 0) // vfs-link? + { + return self::delete_attached(-$link_id); + } + elseif ($app == self::VFS_APPNAME) + { + return self::delete_attached($app2,$id2,$id); + } + elseif ($app2 == self::VFS_APPNAME) + { + return self::delete_attached($app,$id,$id2); + } + if (!is_array($id)) + { + if (!$link_id && !$app2 && !$id2 && $app2 != '!'.self::VFS_APPNAME) + { + self::delete_attached($app,$id); // deleting all attachments + self::delete_cache($app,$id); + } + $deleted =& solink::unlink($link_id,$app,$id,$owner,$app2 != '!'.self::VFS_APPNAME ? $app2 : '',$id2); + + // only notify on real links, not the one cached for writing or fileattachments + self::notify_unlink($deleted); + + return count($deleted); + } + if (!$link_id) $link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given + + if (isset($id[$link_id])) + { + $id[$link_id] = False; // set the unlink marker + + if (self::DEBUG) + { + _debug_array($id); + } + return True; + } + return False; + } + + /** + * get list/array of link-aware apps the user has rights to use + * + * @param string $must_support capability the apps need to support, eg. 'add', default ''=list all apps + * @return array with app => title pairs + */ + static function app_list($must_support='') + { + $apps = array(); + foreach(self::$app_register as $app => $reg) + { + if ($must_support && !isset($reg[$must_support])) continue; + + if ($GLOBALS['egw_info']['user']['apps'][$app]) + { + $apps[$app] = $GLOBALS['egw_info']['apps'][$app]['title']; + } + } + return $apps; + } + + /** + * Searches for a $pattern in the entries of $app + * + * @param string $app app to search + * @param string $pattern pattern to search + * @param string $type Search only a certain sub-type of records (optional) + * @return array with $id => $title pairs of matching entries of app + */ + static function query($app,$pattern, &$options = array()) + { + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['query'])) + { + return array(); + } + $method = $reg['query']; + + if (self::DEBUG) + { + echo "egw_link::query('$app','$pattern') => '$method'
\n"; + echo "Options: "; _debug_array($options); + } + $result = ExecMethod2($method,$pattern,$options); + + if (!isset($options['total'])) + { + $options['total'] = count($result); + } + if (is_array($result) && (isset($options['start']) || count($result) > $options['num_rows'])) + { + $result = array_slice($result, $options['start'], $options['num_rows'], true); + } + + return $result; + } + + /** + * returns the title (short description) of entry $id and $app + * + * @param string $app appname + * @param string $id id in $app + * @param array $link=null link-data for file-attachments + * @return string/boolean string with title, null if $id does not exist in $app or false if no perms to view it + */ + static function title($app,$id,$link=null) + { + if (!$id) return ''; + + $title =& self::get_cache($app,$id); + if (isset($title) && !empty($title) && !is_array($id)) + { + if (self::DEBUG) echo ''.__METHOD__."('$app','$id')='$title' (from cache)
\n"; + return $title; + } + if ($app == self::VFS_APPNAME) + { + if (is_array($id) && $link) + { + $link = $id; + $title = egw_vfs::decodePath($link['name']); + } + else + { + $title = $id; + } + /* disabling mime-type and size in link-title of attachments, as it clutters the UI + and users dont need it most of the time. These details can allways be views in filemanager. + if (is_array($link)) + { + $title .= ': '.$link['type'] . ' '.egw_vfs::hsize($link['size']); + }*/ + if (self::DEBUG) echo ''.__METHOD__."('$app','$id')='$title' (file)
\n"; + return $title; + } + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['title'])) + { + if (self::DEBUG) echo "".__METHOD__."('$app','$id') something is wrong!!!
\n"; + return array(); + } + $method = $reg['title']; + + $title = ExecMethod($method,$id); + + if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it + { + self::unlink(0,$app,$id); + if (self::DEBUG) echo ''.__METHOD__."('$app','$id') unlinked, as $method returned null
\n"; + return False; + } + if (self::DEBUG) echo ''.__METHOD__."('$app','$id')='$title' (from $method)
\n"; + + return $title; + } + + /** + * Maximum number of titles to query from an application at once (to NOT trash mysql) + */ + const MAX_TITLES_QUERY = 100; + + /** + * Query the titles off multiple id's of one app + * + * Apps can implement that hook, if they have a quicker (eg. less DB queries) method to query the title of multiple entries. + * If it's not implemented, we call the regular title method multiple times. + * + * @param string $app + * @param array $ids + */ + static function titles($app,array $ids) + { + if (self::DEBUG) + { + echo "".__METHOD__."($app,".implode(',',$ids).")
\n"; + } + $titles = $ids_to_query = array(); + foreach($ids as $id) + { + $title =& self::get_cache($app,$id); + if (!isset($title)) + { + if (isset(self::$app_register[$app]['titles'])) + { + $ids_to_query[] = $id; // titles method --> collect links to query at once + } + else + { + $title = self::title($app,$id); // no titles method --> fallback to query each link separate + } + } + $titles[$id] = $title; + } + if ($ids_to_query) + { + for ($n = 0; $ids = array_slice($ids_to_query,$n*self::MAX_TITLES_QUERY,self::MAX_TITLES_QUERY); ++$n) + { + foreach(ExecMethod(self::$app_register[$app]['titles'],$ids) as $id => $t) + { + $title =& self::get_cache($app,$id); + $titles[$id] = $title = $t; + } + } + } + return $titles; + } + + /** + * Add new entry to $app, evtl. already linked to $to_app, $to_id + * + * @param string $app appname of entry to create + * @param string $to_app appname to link the new entry to + * @param string $to_id id in $to_app + * @return array/boolean with name-value pairs for link to add-methode of $app or false if add not supported + */ + static function add($app,$to_app='',$to_id='') + { + //echo "egw_link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['add'])) + { + return false; + } + $params = $reg['add']; + + if ($reg['add_app'] && $to_app && $reg['add_id'] && $to_id) + { + $params[$reg['add_app']] = $to_app; + $params[$reg['add_id']] = $to_id; + } + return $params; + } + + /** + * view entry $id of $app + * + * @param string $app appname + * @param string $id id in $app + * @param array $link=null link-data for file-attachments + * @return array with name-value pairs for link to view-methode of $app to view $id + */ + static function view($app,$id,$link=null) + { + if ($app == self::VFS_APPNAME && !empty($id) && is_array($link)) + { + return egw_vfs::download_url(self::vfs_path($link['app2'],$link['id2'],$link['id'],true)); + } + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['view']) || !isset($reg['view_id'])) + { + return array(); + } + $view = $reg['view']; + + $names = explode(':',$reg['view_id']); + if (count($names) > 1) + { + $id = explode(':',$id); + while (list($n,$name) = each($names)) + { + $view[$name] = $id[$n]; + } + } + else + { + $view[$reg['view_id']] = $id; + } + return $view; + } + + /** + * Check if $app uses a popup for $action + * + * @param string $app app-name + * @param string $action='view' name of the action, atm. 'view' or 'add' + * @return boolean|string false if no popup is used or $app is not registered, otherwise string with the prefered popup size (eg. '640x400) + */ + static function is_popup($app,$action='view') + { + return self::get_registry($app,$action.'_popup'); + } + + /** + * Check if $app is in the registry and has an entry for $name + * + * @param string $app app-name + * @param string $name name / key in the registry, eg. 'view' + * @return boolean|string false if $app is not registered, otherwise string with the value for $name + */ + static function get_registry($app,$name) + { + $reg = self::$app_register[$app]; + + return isset($reg) ? $reg[$name] : false; + } + + /** + * path to the attached files of $app/$ip or the directory for $app if no $id,$file given + * + * All link-files are based in the vfs-subdir '/apps/'.$app + * + * @param string $app appname + * @param string $id='' id in $app + * @param string $file='' filename + * @param boolean $just_the_path=false return url or just the vfs path + * @return string/array path or array with path and relatives, depending on $relatives + */ + static function vfs_path($app,$id='',$file='',$just_the_path=false) + { + $path = self::VFS_BASEURL; + + if ($app) + { + $path .= '/'.$app; + + if ($id) + { + $path .= '/'.$id; + + if ($file) + { + $path .= '/'.$file; + } + } + } + if ($just_the_path) + { + $path = parse_url($path,PHP_URL_PATH); + } + else + { + $path = egw_vfs::resolve_url($path); + } + //error_log(__METHOD__."($app,$id,$file)=$path"); + return $path; + } + + /** + * Put a file to the corrosponding place in the VFS and set the attributes + * + * @param string $app appname to linke the file to + * @param string $id id in $app + * @param array $file informations about the file in format of the etemplate file-type + * $file['name'] name of the file (no directory) + * $file['type'] mine-type of the file + * $file['tmp_name'] name of the uploaded file (incl. directory) + * $file['path'] path of the file on the client computer + * $file['ip'] of the client (path and ip are only needed if u want a symlink (if possible)) + * @param string $comment='' comment to add to the link + * @return int negative id of egw_sqlfs table as negative link-id's are for vfs attachments + */ + static function attach_file($app,$id,$file,$comment='') + { + $entry_dir = self::vfs_path($app,$id); + if (self::DEBUG) + { + echo "
attach_file: app='$app', id='$id', tmp_name='$file[tmp_name]', name='$file[name]', size='$file[size]', type='$file[type]', path='$file[path]', ip='$file[ip]', comment='$comment', entry_dir='$entry_dir'
\n"; + } + if (file_exists($entry_dir) || ($Ok = mkdir($entry_dir,0,true))) + { + if (($Ok = copy($file['tmp_name'],$fname = egw_vfs::concat($entry_dir,egw_vfs::encodePathComponent($file['name']))) && + ($stat = egw_vfs::url_stat($fname,0))) && $comment) + { + egw_vfs::proppatch(parse_url($fname,PHP_URL_PATH),array(array('name'=>'comment','val'=>$comment))); // set comment + } + } + else + { + error_log(__METHOD__."($app,$id,$file,$comment) Can't mkdir $entry_dir!"); + } + return $Ok ? -$stat['ino'] : false; + } + + /** + * deletes a single or all attached files of an entry (for all there's no acl check, as the entry probably not exists any more!) + * + * @param int/string $app > 0: file_id of an attchemnt or $app/$id entry which linked to + * @param string $id='' id in app + * @param string $fname='' filename + * @return boolean|array false on error ($app or $id not found), array with path as key and boolean result of delete + */ + static function delete_attached($app,$id='',$fname='') + { + if ((int)$app > 0) // is file_id + { + $url = links_stream_wrapper::PREFIX.links_stream_wrapper::id2path($app); + } + else + { + if (empty($app) || empty($id)) + { + return False; // dont delete more than all attachments of an entry + } + $url = self::vfs_path($app,$id,$fname); + + if (!$fname || !$id) // we delete the whole entry (or all entries), which probably not exist anymore + { + $current_is_root = egw_vfs::$is_root; + egw_vfs::$is_root = true; + } + } + if (self::DEBUG) + { + echo ''.__METHOD__."('$app','$id','$fname') url=$url
\n"; + } + if (($Ok = !file_exists($url) || egw_vfs::remove($url,true)) && ((int)$app > 0 || $fname)) + { + // try removing the dir, in case it's empty + @egw_vfs::rmdir(egw_vfs::dirname($url)); + } + if (!is_null($current_is_root)) + { + egw_vfs::$is_root = $current_is_root; + } + return $Ok; + } + + /** + * converts the infos vfs has about a file into a link + * + * @param string $app appname + * @param string $id id in app + * @param string $filename filename + * @return array 'kind' of link-array + */ + static function info_attached($app,$id,$filename) + { + $path = self::vfs_path($app,$id,$filename,true); + if (!($stat = egw_vfs::stat($path,STREAM_URL_STAT_QUIET))) + { + return false; + } + return self::fileinfo2link($stat,$path); + } + + /** + * converts a fileinfo (row in the vfs-db-table) in a link + * + * @param array/int $fileinfo a row from the vfs-db-table (eg. returned by the vfs ls static function) or a file_id of that table + * @return array a 'kind' of link-array + */ + static function fileinfo2link($fileinfo,$url=null) + { + if (!is_array($fileinfo)) + { + $url = links_stream_wrapper::id2path($fileinfo); + if (!($fileinfo = links_stream_wrapper::url_stat($url,STREAM_URL_STAT_QUIET))) + { + return false; + } + } + list(,,$app,$id) = explode('/',$url[0] == '/' ? $url : parse_url($url,PHP_URL_PATH)); // /apps/$app/$id + + return array( + 'app' => self::VFS_APPNAME, + 'id' => $fileinfo['name'], + 'app2' => $app, + 'id2' => $id, + 'remark' => '', // only list_attached currently sets the remark + 'owner' => $fileinfo['uid'], + 'link_id' => -$fileinfo['ino'], + 'lastmod' => $fileinfo['mtime'], + 'size' => $fileinfo['size'], + 'type' => $fileinfo['mime'], + ); + } + + /** + * lists all attachments to $app/$id + * + * @param string $app appname + * @param string $id id in app + * @return array with link_id => 'kind' of link-array pairs + */ + static function list_attached($app,$id) + { + $path = self::vfs_path($app,$id,'',true); + //error_log(__METHOD__."($app,$id) url=$url"); + + if (!($extra = self::get_registry($app,'find_extra'))) $extra = array(); + + $attached = array(); + if (($url2stats = egw_vfs::find($path,array('need_mime'=>true,'type'=>'F')+$extra,true))) + { + $props = egw_vfs::propfind(array_keys($url2stats)); // get the comments + foreach($url2stats as $url => &$fileinfo) + { + $link = self::fileinfo2link($fileinfo,$url); + if (isset($props[$path = parse_url($url,PHP_URL_PATH)])) + { + foreach($props[$path] as $prop) + { + if ($prop['ns'] == egw_vfs::DEFAULT_PROP_NAMESPACE && $prop['name'] == 'comment') + { + $link['remark'] = $prop['val']; + break; + } + } + } + $attached[$link['link_id']] = $link; + $urls[] = $url; + } + } + return $attached; + } + + /** + * reverse static function of htmlspecialchars() + * + * @param string $str string to decode + * @return string decoded string + */ + static private function decode_htmlspecialchars($str) + { + return str_replace(array('&','"','<','>'),array('&','"','<','>'),$str); + } + + /** + * notify other apps about changed content in $app,$id + * + * @param string $app name of app in which the updated happend + * @param string $id id in $app of the updated entry + * @param array $data=null updated data of changed entry, as the read-method of the BO-layer would supply it + */ + static function notify_update($app,$id,$data=null) + { + foreach(self::get_links($app,$id,'!'.self::VFS_APPNAME) as $link_id => $link) + { + self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data); + } + self::delete_cache($app,$id); + } + + /** + * notify an application about a new or deleted links to own entries or updates in the content of the linked entry + * + * Please note: not all apps supply update notifications + * + * @internal + * @param string $type 'link' for new links, 'unlink' for unlinked entries, 'update' of content in linked entries + * @param string $notify_app app to notify + * @param string $notify_id id in $notify_app + * @param string $target_app name of app whos entry changed, linked or deleted + * @param string $target_id id in $target_app + * @param array $data=null data of entry in app2 (optional) + */ + static private function notify($type,$notify_app,$notify_id,$target_app,$target_id,$link_id,$data=null) + { + if ($link_id && isset(self::$app_register[$notify_app]) && isset(self::$app_register[$notify_app]['notify'])) + { + ExecMethod(self::$app_register[$notify_app]['notify'],array( + 'type' => $type, + 'id' => $notify_id, + 'target_app' => $target_app, + 'target_id' => $target_id, + 'link_id' => $link_id, + 'data' => $data, + )); + } + } + + /** + * notifies about unlinked links + * + * @internal + * @param array &$links unlinked links from the database + */ + static private function notify_unlink(&$links) + { + foreach($links as $link) + { + // we notify both sides of the link, as the unlink command NOT clearly knows which side initiated the unlink + self::notify('unlink',$link['link_app1'],$link['link_id1'],$link['link_app2'],$link['link_id2'],$link['link_id']); + self::notify('unlink',$link['link_app2'],$link['link_id2'],$link['link_app1'],$link['link_id1'],$link['link_id']); + } + } + + /** + * Get a reference to the cached value for $app/$id for $type + * + * @param string $app + * @param string|int $id + * @param string $type='title' 'title' or 'file_access' + * @return int|string can be null, if cache not yet set + */ + private static function &get_cache($app,$id,$type = 'title') + { + switch($type) + { + case 'title': + return self::$title_cache[$app.':'.$id]; + case 'file_access': + return self::$file_access_cache[$app.':'.$id]; + default: + throw new egw_exception_wrong_parameter("Unknown type '$type'!"); + } + } + + /** + * Set title and optional file_access cache for $app,$id + * + * Allows applications to set values for title and file access, eg. in their search method, + * to not be called again. This offloads the need to cache from the app to the link class. + * If there's no caching, items get read multiple times from the database! + * + * @param string $app + * @param int|string $id + * @param string $title title string or null + * @param int $file_access=null EGW_ACL_READ, EGW_ACL_EDIT or both or'ed together + */ + public static function set_cache($app,$id,$title,$file_access=null) + { + //error_log(__METHOD__."($app,$id,$title,$file_access)"); + if (!is_null($title)) + { + $cache =& self::get_cache($app,$id); + $cache = $title; + } + if (!is_null($file_access)) + { + $cache =& self::get_cache($app,$id,'file_access'); + $cache = $file_access; + } + } + + /** + * Delete the diverse caches for $app/$id + * + * @param string $app + * @param int|string $id + */ + private static function delete_cache($app,$id) + { + unset(self::$title_cache[$app.':'.$id]); + unset(self::$file_access_cache[$app.':'.$id]); + } + + /** + * Check the file access perms for $app/id + * + * @ToDo $rel_path is not yet implemented, as no app use it currently + * @param string $app + * @param string|int $id id of entry + * @param int $required=EGW_ACL_READ EGW_ACL_{READ|EDIT} + * @param string $rel_path + * @return boolean + */ + static function file_access($app,$id,$required=EGW_ACL_READ,$rel_path=null) + { + $cache =& self::get_cache($app,$id,'file_access'); + + if (!isset($cache) || $required == EGW_ACL_EDIT && !($cache & $required)) + { + if(($method = self::get_registry($app,'file_access'))) + { + $cache |= ExecMethod2($method,$id,$required,$rel_path) ? $required : 0; + } + else + { + $cache |= self::title($app,$id) ? EGW_ACL_READ|EGW_ACL_EDIT : 0; + } + //error_log(__METHOD__."($app,$id,$required,$rel_path) got $cache --> ".($cache & $required ? 'true' : 'false')); + } + //else error_log(__METHOD__."($app,$id,$required,$rel_path) using cached value $cache --> ".($cache & $required ? 'true' : 'false')); + return !!($cache & $required); + } +} +egw_link::init_static(); diff --git a/phpgwapi/inc/class.egw_vfs.inc.php b/phpgwapi/inc/class.egw_vfs.inc.php index 825b8c6be9..833c6ee091 100644 --- a/phpgwapi/inc/class.egw_vfs.inc.php +++ b/phpgwapi/inc/class.egw_vfs.inc.php @@ -1365,7 +1365,7 @@ class egw_vfs extends vfs_stream_wrapper * * Not all chars get encoded, slashes '/' are silently removed! * - * To reverse the encoding, eg. to display a filename to the user, you can use urldecode() + * To reverse the encoding, eg. to display a filename to the user, you have to use egw_vfs::decodePath() * * @param string|array $component * @return string|array @@ -1378,7 +1378,7 @@ class egw_vfs extends vfs_stream_wrapper /** * Encode a path: replacing certain chars with their urlencoded counterparts * - * To reverse the encoding, eg. to display a filename to the user, you can use urldecode() + * To reverse the encoding, eg. to display a filename to the user, you have to use egw_vfs::decodePath() * * @param string $path * @return string @@ -1388,6 +1388,19 @@ class egw_vfs extends vfs_stream_wrapper return implode('/',self::encodePathComponent(explode('/',$path))); } + /** + * Decode a path: rawurldecode(): mostly urldecode(), but do NOT decode '+', as we're NOT encoding it! + * + * Used eg. to translate a path for displaying to the User. + * + * @param string $path + * @return string + */ + static public function decodePath($path) + { + return rawurldecode($path); + } + /** * Initialise our static vars */ diff --git a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php b/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php index c01706fa25..b85f6b8e10 100644 --- a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php @@ -153,7 +153,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper } // open the "real" file - if (!($this->opened_stream = fopen($path=urldecode(parse_url($url,PHP_URL_PATH)),$mode,$options))) + if (!($this->opened_stream = fopen($path=egw_vfs::decodePath(parse_url($url,PHP_URL_PATH)),$mode,$options))) { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) fopen('$path','$mode',$options) returned false!"); return false; @@ -295,7 +295,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function unlink ( $url ) { - $path = urldecode(parse_url($url,PHP_URL_PATH)); + $path = egw_vfs::decodePath(parse_url($url,PHP_URL_PATH)); // check access rights (file need to exist and directory need to be writable if (!file_exists($path) || is_dir($path) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) @@ -355,7 +355,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); return false; } - return rename(urldecode($from['path']),urldecode($to['path'])); + return rename(egw_vfs::decodePath($from['path']),egw_vfs::decodePath($to['path'])); } /** @@ -371,7 +371,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function mkdir ( $url, $mode, $options ) { - $path = urldecode(parse_url($url,PHP_URL_PATH)); + $path = egw_vfs::decodePath(parse_url($url,PHP_URL_PATH)); $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE); // find the real parent (might be more then one level if $recursive!) @@ -403,7 +403,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function rmdir ( $url, $options ) { - $path = urldecode(parse_url($url,PHP_URL_PATH)); + $path = egw_vfs::decodePath(parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) @@ -425,7 +425,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function touch($url,$time=null,$atime=null) { - $path = urldecode(parse_url($url,PHP_URL_PATH)); + $path = egw_vfs::decodePath(parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) @@ -492,7 +492,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $this->opened_dir = null; - $path = urldecode(parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); + $path = egw_vfs::decodePath(parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); // ToDo: check access rights @@ -533,7 +533,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper static function url_stat ( $url, $flags ) { $parts = parse_url($url); - $path = urldecode($parts['path']); + $path = egw_vfs::decodePath($parts['path']); $stat = @stat($path); // suppressed the stat failed warnings @@ -733,20 +733,20 @@ class filesystem_stream_wrapper implements iface_stream_wrapper list(,$query) = explode('?',$url,2); parse_str($query,$get); if (empty($get['url'])) return false; // no download url given for this mount-point - + if (!($mount_url = egw_vfs::mount_url($url))) return false; // no mount url found, should not happen list($mount_url) = explode('?',$mount_url); - + list($url,$query) = explode('?',$url,2); $relpath = substr($url,strlen($mount_url)); - + $download_url = egw_vfs::concat($get['url'],$relpath); if ($download_url[0] == '/') { $download_url = ($_SERVER['HTTPS'] ? 'https://' : 'http://'). $_SERVER['HTTP_HOST'].$download_url; } - + //die(__METHOD__."('$url') --> relpath = $relpath --> $download_url"); return $download_url; }