From d6c6dc2de064e2d50ce178a56fc716d2e847b54e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 3 Mar 2011 15:49:28 +0000 Subject: [PATCH] * fixed handling of + char in VFS filenames (using egw_vfs::decodePath() instead of urldecode()) --- egw-pear/HTTP/WebDAV/Server/Filesystem.php | 804 ++++++++++++ etemplate/inc/class.vfs_widget.inc.php | 538 ++++++++ filemanager/inc/class.filemanager_ui.inc.php | 26 +- phpgwapi/inc/class.egw_link.inc.php | 1149 +++++++++++++++++ phpgwapi/inc/class.egw_vfs.inc.php | 17 +- .../class.filesystem_stream_wrapper.inc.php | 24 +- 6 files changed, 2531 insertions(+), 27 deletions(-) create mode 100644 egw-pear/HTTP/WebDAV/Server/Filesystem.php create mode 100644 etemplate/inc/class.vfs_widget.inc.php create mode 100644 phpgwapi/inc/class.egw_link.inc.php diff --git a/egw-pear/HTTP/WebDAV/Server/Filesystem.php b/egw-pear/HTTP/WebDAV/Server/Filesystem.php new file mode 100644 index 0000000000..68a4174337 --- /dev/null +++ b/egw-pear/HTTP/WebDAV/Server/Filesystem.php @@ -0,0 +1,804 @@ + + * @version @package-version@ + */ +class HTTP_WebDAV_Server_Filesystem extends HTTP_WebDAV_Server +{ + /** + * Root directory for WebDAV access + * + * Defaults to webserver document root (set by ServeRequest) + * + * @access private + * @var string + */ + var $base = ""; + + /** + * MySQL Host where property and locking information is stored + * + * @access private + * @var string + */ + var $db_host = "localhost"; + + /** + * MySQL database for property/locking information storage + * + * @access private + * @var string + */ + var $db_name = "webdav"; + + /** + * MySQL table name prefix + * + * @access private + * @var string + */ + var $db_prefix = ""; + + /** + * MySQL user for property/locking db access + * + * @access private + * @var string + */ + var $db_user = "root"; + + /** + * MySQL password for property/locking db access + * + * @access private + * @var string + */ + var $db_passwd = ""; + + /** + * Serve a webdav request + * + * @access public + * @param string + */ + function ServeRequest($base = false) + { + // special treatment for litmus compliance test + // reply on its identifier header + // not needed for the test itself but eases debugging + foreach (apache_request_headers() as $key => $value) { + if (stristr($key, "litmus")) { + error_log("Litmus test $value"); + header("X-Litmus-reply: ".$value); + } + } + + // set root directory, defaults to webserver document root if not set + if ($base) { + $this->base = realpath($base); // TODO throw if not a directory + } else if (!$this->base) { + $this->base = $this->_SERVER['DOCUMENT_ROOT']; + } + + // establish connection to property/locking db + mysql_connect($this->db_host, $this->db_user, $this->db_passwd) or die(mysql_error()); + mysql_select_db($this->db_name) or die(mysql_error()); + // TODO throw on connection problems + + // let the base class do all the work + parent::ServeRequest(); + } + + /** + * No authentication is needed here + * + * @access private + * @param string HTTP Authentication type (Basic, Digest, ...) + * @param string Username + * @param string Password + * @return bool true on successful authentication + */ + function check_auth($type, $user, $pass) + { + return true; + } + + + /** + * PROPFIND method handler + * + * @param array general parameter passing array + * @param array return array for file properties + * @return bool true on success + */ + function PROPFIND(&$options, &$files) + { + // get absolute fs path to requested resource + $fspath = $this->base . $options["path"]; + + // sanity check + if (!file_exists($fspath)) { + return false; + } + + // prepare property array + $files["files"] = array(); + + // store information for the requested path itself + $files["files"][] = $this->fileinfo($options["path"]); + + // information for contained resources requested? + if (!empty($options["depth"])) { // TODO check for is_dir() first? + + // make sure path ends with '/' + $options["path"] = $this->_slashify($options["path"]); + + // try to open directory + $handle = @opendir($fspath); + + if ($handle) { + // ok, now get all its contents + while ($filename = readdir($handle)) { + if ($filename != "." && $filename != "..") { + $files["files"][] = $this->fileinfo($options["path"].$filename); + } + } + // TODO recursion needed if "Depth: infinite" + closedir($handle); + } + } + + // ok, all done + return true; + } + + /** + * Get properties for a single file/resource + * + * @param string resource path + * @return array resource properties + */ + function fileinfo($path) + { + // map URI path to filesystem path + $fspath = $this->base . $path; + + // create result array + $info = array(); + // TODO remove slash append code when base clase is able to do it itself + $info["path"] = is_dir($fspath) ? $this->_slashify($path) : $path; + $info["props"] = array(); + + // no special beautified displayname here ... + $info["props"][] = $this->mkprop("displayname", strtoupper($path)); + + // creation and modification time + $info["props"][] = $this->mkprop("creationdate", filectime($fspath)); + $info["props"][] = $this->mkprop("getlastmodified", filemtime($fspath)); + + // type and size (caller already made sure that path exists) + if (is_dir($fspath)) { + // directory (WebDAV collection) + $info["props"][] = $this->mkprop("resourcetype", array($this->mkprop('collection', ''))); + $info["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory"); + } else { + // plain file (WebDAV resource) + $info["props"][] = $this->mkprop("resourcetype", ""); + if (is_readable($fspath)) { + $info["props"][] = $this->mkprop("getcontenttype", $this->_mimetype($fspath)); + } else { + $info["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable"); + } + $info["props"][] = $this->mkprop("getcontentlength", filesize($fspath)); + } + + // get additional properties from database + $query = "SELECT ns, name, value + FROM {$this->db_prefix}properties + WHERE path = '$path'"; + $res = mysql_query($query); + while ($row = mysql_fetch_assoc($res)) { + $info["props"][] = $this->mkprop($row["ns"], $row["name"], $row["value"]); + } + mysql_free_result($res); + + return $info; + } + + /** + * detect if a given program is found in the search PATH + * + * helper function used by _mimetype() to detect if the + * external 'file' utility is available + * + * @param string program name + * @param string optional search path, defaults to $PATH + * @return bool true if executable program found in path + */ + function _can_execute($name, $path = false) + { + // path defaults to PATH from environment if not set + if ($path === false) { + $path = getenv("PATH"); + } + + // check method depends on operating system + if (!strncmp(PHP_OS, "WIN", 3)) { + // on Windows an appropriate COM or EXE file needs to exist + $exts = array(".exe", ".com"); + $check_fn = "file_exists"; + } else { + // anywhere else we look for an executable file of that name + $exts = array(""); + $check_fn = "is_executable"; + } + + // now check the directories in the path for the program + foreach (explode(PATH_SEPARATOR, $path) as $dir) { + // skip invalid path entries + if (!file_exists($dir)) continue; + if (!is_dir($dir)) continue; + + // and now look for the file + foreach ($exts as $ext) { + if ($check_fn("$dir/$name".$ext)) return true; + } + } + + return false; + } + + + /** + * try to detect the mime type of a file + * + * @param string file path + * @return string guessed mime type + */ + function _mimetype($fspath) + { + if (@is_dir($fspath)) { + // directories are easy + return "httpd/unix-directory"; + } else if (function_exists("mime_content_type")) { + // use mime magic extension if available + $mime_type = mime_content_type($fspath); + } else if ($this->_can_execute("file")) { + // it looks like we have a 'file' command, + // lets see it it does have mime support + $fp = popen("file -i '$fspath' 2>/dev/null", "r"); + $reply = fgets($fp); + pclose($fp); + + // popen will not return an error if the binary was not found + // and find may not have mime support using "-i" + // so we test the format of the returned string + + // the reply begins with the requested filename + if (!strncmp($reply, "$fspath: ", strlen($fspath)+2)) { + $reply = substr($reply, strlen($fspath)+2); + // followed by the mime type (maybe including options) + if (preg_match('|^[[:alnum:]_-]+/[[:alnum:]_-]+;?.*|', $reply, $matches)) { + $mime_type = $matches[0]; + } + } + } + + if (empty($mime_type)) { + // Fallback solution: try to guess the type by the file extension + // TODO: add more ... + // TODO: it has been suggested to delegate mimetype detection + // to apache but this has at least three issues: + // - works only with apache + // - needs file to be within the document tree + // - requires apache mod_magic + // TODO: can we use the registry for this on Windows? + // OTOH if the server is Windos the clients are likely to + // be Windows, too, and tend do ignore the Content-Type + // anyway (overriding it with information taken from + // the registry) + // TODO: have a seperate PEAR class for mimetype detection? + switch (strtolower(strrchr(basename($fspath), "."))) { + case ".html": + $mime_type = "text/html"; + break; + case ".gif": + $mime_type = "image/gif"; + break; + case ".jpg": + $mime_type = "image/jpeg"; + break; + default: + $mime_type = "application/octet-stream"; + break; + } + } + + return $mime_type; + } + + /** + * GET method handler + * + * @param array parameter passing array + * @return bool true on success + */ + function GET(&$options) + { + // get absolute fs path to requested resource + $fspath = $this->base . $options["path"]; + + // sanity check + if (!file_exists($fspath)) return false; + + // is this a collection? + if (is_dir($fspath)) { + return $this->GetDir($fspath, $options); + } + + // detect resource type + $options['mimetype'] = $this->_mimetype($fspath); + + // detect modification time + // see rfc2518, section 13.7 + // some clients seem to treat this as a reverse rule + // requiering a Last-Modified header if the getlastmodified header was set + $options['mtime'] = filemtime($fspath); + + // detect resource size + $options['size'] = filesize($fspath); + + // no need to check result here, it is handled by the base class + if (!($options['stream'] = fopen($fspath, "r"))) + { + return '403 Forbidden'; + } + return true; + } + + /** + * GET method handler for directories + * + * This is a very simple mod_index lookalike. + * See RFC 2518, Section 8.4 on GET/HEAD for collections + * + * @param string directory path + * @return void function has to handle HTTP response itself + */ + function GetDir($fspath, &$options) + { + $path = $this->_slashify($options["path"]); + if ($path != $options["path"]) { + header("Location: ".$this->base_uri.$path); + exit; + } + + // fixed width directory column format + $format = "%15s %-19s %-s\n"; + + $handle = @opendir($fspath); + if (!$handle) { + return false; + } + + echo "Index of ".htmlspecialchars($options['path'])."\n"; + + echo "

Index of ".htmlspecialchars($options['path'])."

\n"; + + echo "
";
+        printf($format, "Size", "Last modified", "Filename");
+        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 "
"; + + 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 "

".__METHOD__."($form_name,$value,".array2string($cell).",...)

\n"; + $type = $cell['type']; + if (!in_array($type,array('vfs-name','vfs-upload'))) $cell['readonly'] = true; // to not call post-process + + // check if we have a path and not the raw value, in that case we have to do a stat first + if (in_array($type,array('vfs-size','vfs-mode','vfs-uid','vfs-gid')) && !is_numeric($value) || $type == 'vfs' && !$value) + { + if (!$value || !($stat = egw_vfs::stat($value))) + { + if ($value) $value = lang("File '%1' not found!",egw_vfs::decodePath($value)); + $cell = etemplate::empty_cell(); + return true; // allow extra value; + } + } + $cell['type'] = 'label'; + + switch($type) + { + case 'vfs-upload': // option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files + if (empty($value) && preg_match('/^exec.*\[([^]]+)\]$/',$form_name,$matches)) // if no value via content array, use widget name + { + $value = $matches[1]; + } + $extension_data = array('value' => $value, 'mimetype' => $cell['size'], 'type' => $type); + if ($value[0] != '/') + { + list($app,$id,$relpath) = explode(':',$value,3); + if (empty($id)) + { + static $tmppath = array(); // static var, so all vfs-uploads get created in the same temporary dir + if (!isset($tmppath[$app])) $tmppath[$app] = '/home/'.$GLOBALS['egw_info']['user']['account_lid'].'/.'.$app.'_'.md5(time().session_id()); + $value = $tmppath[$app]; + unset($cell['onchange']); // no onchange, if we have to use a temporary dir + } + else + { + $value = egw_link::vfs_path($app,$id,'',true); + } + if (!empty($relpath)) $value .= '/'.$relpath; + } + $path = $extension_data['path'] = $value; + if (substr($path,-1) != '/' && self::file_exists($path) && !egw_vfs::is_dir($path)) // display download link and delete icon + { + $extension_data['path'] = $path; + $cell = $this->file_widget($value,$path,$cell['name'],$cell['label']); + } + else // file does NOT exists --> display file upload + { + $cell['type'] = 'file'; + // if no explicit help message set and we only allow certain file types --> show them + if (empty($cell['help']) && $cell['size']) + { + if (($type = mime_magic::mime2ext($cell['size']))) + { + $type = '*.'.strtoupper($type); + } + else + { + $type = $cell['size']; + } + $cell['help'] = lang('Allowed file type: %1',$type); + } + } + // check if directory (trailing slash) is given --> upload of multiple files + if (substr($path,-1) == '/' && egw_vfs::file_exists($path) && ($files = egw_vfs::scandir($path))) + { + //echo $path; _debug_array($files); + $upload = $cell; + $cell = etemplate::empty_cell('vbox','',array('size' => ',,0,0')); + $extension_data['files'] = $files; + $value = array(); + foreach($files as $file) + { + $file = $path.$file; + $basename = basename($file); + unset($widget); + $widget = $this->file_widget($value[$basename],$file,$upload['name']."[$basename]"); + etemplate::add_child($cell,$widget); + } + etemplate::add_child($cell,$upload); + } + break; + + case 'vfs-size': // option: add size in bytes in brackets + $value = egw_vfs::hsize($size = is_numeric($value) ? $value : $stat['size']); + if ($cell['size']) $value .= ' ('.$size.')'; + $cell['type'] = 'label'; + break; + + case 'vfs-mode': + $value = egw_vfs::int2mode(is_numeric($value) ? $value : $stat['mode']); + list($span,$class) = explode(',',$cell['span'],2); + $class .= ($class ? ' ' : '') . 'vfsMode'; + $cell['span'] = $span.','.$class; + $cell['no_lang'] = true; + break; + + case 'vfs-uid': + case 'vfs-gid': + $uid = !is_numeric($value) ? $stat[$type=='vfs-uid'?'uid':'gid'] : $value; + $value = !$uid ? 'root' : $GLOBALS['egw']->accounts->id2name($type=='vfs-uid'?$uid:-$uid); // our internal gid's are negative! + break; + + case 'vfs': + if (is_array($value)) + { + $name = $value['name']; + $path = substr($value['path'],0,-strlen($name)-1); + $mime = $value['mime']; + } + else + { + $name = $value; + $path = ''; + $mime = egw_vfs::mime_content_type($value); + $value = array(); + } + if (($cell_name = $cell['name']) == '$row') + { + $cell_name = array_pop($arr=explode('][',substr($form_name,0,-1))); + } + $cell['name'] = ''; + $cell['type'] = 'hbox'; + $cell['size'] = '0,,0,0'; + foreach($name != '/' ? explode('/',$name) : array('') as $n => $component) + { + if ($n > (int)($path === '/')) + { + $sep = soetemplate::empty_cell('label','',array('label' => '/')); + soetemplate::add_child($cell,$sep); + unset($sep); + } + $value['c'.$n] = $component !== '' ? egw_vfs::decodePath($component) : '/'; + $path .= ($path != '/' ? '/' : '').$component; + // replace id's in /apps again with human readable titles + $path_parts = explode('/',$path); + if ($path_parts[1] == 'apps') + { + switch(count($path_parts)) + { + case 2: + $value['c'.$n] = lang('Applications'); + break; + case 3: + $value['c'.$n] = lang($path_parts[2]); + break; + case 4: + if (is_numeric($value['c'.$n])) $value['c'.$n] .= egw_link::title($path_parts[2],$path_parts[3]); + break; + } + } + if (egw_vfs::is_readable($path)) // show link only if we have access to the file or dir + { + if ($n < count($comps)-1 || $mime == egw_vfs::DIR_MIME_TYPE || egw_vfs::is_dir($path)) + { + $value['l'.$n] = '/index.php?menuaction=filemanager.filemanager_ui.index&path='.urlencode($path); + $target = ''; + } + else + { + $value['l'.$n] = egw_vfs::download_url($path); + $target = ',,,_blank'; + } + } + + if ($cell['onclick']) + { + $comp = etemplate::empty_cell('button',$cell_name.'[c'.$n.']',array( + 'size' => '1', + 'no_lang' => true, + 'span' => ',vfsFilename', + 'label' => $value['c'.$n], + 'onclick' => str_replace('$path',"'".addslashes($path)."'",$cell['onclick']), + )); + } + else + { + $comp = etemplate::empty_cell('label',$cell_name.'[c'.$n.']',array( + 'size' => ',@'.$cell_name.'[l'.$n.']'.$target, + 'no_lang' => true, + 'span' => ',vfsFilename', + )); + } + etemplate::add_child($cell,$comp); + unset($comp); + } + unset($cell['onclick']); // otherwise it's handled by the grid too + //_debug_array($comps); _debug_array($cell); _debug_array($value); + break; + + case 'vfs-name': // size: [length][,maxLength[,allowPath]] + $cell['type'] = 'text'; + list($length,$maxLength,$allowPath) = $options = explode(',',$cell['size']); + $preg = $allowPath ? '' : '/[^\\/]/'; // no slash '/' allowed, if not allowPath set + $cell['size'] = "$length,$maxLength,$preg"; + $value = egw_vfs::decodePath($value); + $extension_data = array('type' => $type,'allowPath' => $allowPath); + break; + + case 'vfs-mime': + if (!$value) + { + $cell = etemplate::empty_cell(); + return true; + } + if (!is_array($value)) + { + if ($value[0] == '/' || count(explode('/',$value)) != 2) + { + $mime = egw_vfs::mime_content_type($path=$value); + } + else + { + $mime = $value; + } + } + else + { + $path = $value['path']; + $mime = $value['mime']; + } + //error_log(__METHOD__."() type=vfs-mime: value=".array2string($value).": mime=$mime, path=$path"); + $cell['type'] = 'image'; + $cell['label'] = mime_magic::mime2label($mime); + + list($mime_main,$mime_sub) = explode('/',$mime); + if ($mime_main == 'egw') + { + $value = $mime_sub.'/navbar'; // egw-applications for link-widget + $cell['label'] = lang($mime_sub); + list($span,$class) = explode(',',$cell['span'],2); + $class .= ($class ? ' ' : '') . 'vfsMimeIcon'; + $cell['span'] = $span.','.$class; + } + elseif($path && $mime_main == 'image' && in_array($mime_sub,array('png','jpeg','jpg','gif','bmp')) && + (string)$GLOBALS['egw_info']['server']['link_list_thumbnail'] != '0' && + (string)$GLOBALS['egw_info']['user']['preferences']['common']['link_list_thumbnail'] != '0' && + // check the size of the image, as too big images get no icon, but a PHP Fatal error: Allowed memory size exhausted + (!is_array($value) && ($stat = egw_vfs::stat($path)) ? $stat['size'] : $value['size']) < 600000) + { + if (substr($path,0,6) == '/apps/') + { + $path = parse_url(egw_vfs::resolve_url_symlinks($path),PHP_URL_PATH); + } + $value = $GLOBALS['egw']->link('/etemplate/thumbnail.php',array('path' => $path)); + } + else + { + $value = egw_vfs::mime_icon($mime); + } + // mark symlinks (check if method exists, to allow etemplate to run on 1.6 API!) + if (method_exists('egw_vfs','is_link') && egw_vfs::is_link($path)) + { + $broken = !egw_vfs::stat($path); + list($span,$class) = explode(',',$cell['span'],2); + $class .= ($class ? ' ' : '') . ($broken ? 'vfsIsBrokenLink' : 'vfsIsLink'); + $cell['span'] = $span.','.$class; + $cell['label'] = ($broken ? lang('Broken link') : lang('Link')).': '.egw_vfs::decodePath(egw_vfs::readlink($path)). + (!$broken ? ' ('.$cell['label'].')' : ''); + } + break; + + default: + $value = 'Not yet implemented'; + } + return true; + } + + /** + * Create widget with download and delete (only if dir is writable) link + * + * @param mixed &$value + * @param string $path vfs path of download + * @param string $name name of widget + * @param string $label=null label, if not set basename($path) is used + * @return array + */ + static function file_widget(&$value,$path,$name,$label=null) + { + $value = empty($label) ? egw_vfs::decodePath(egw_vfs::basename($path)) : lang($label); // display (translated) Label or filename (if label empty) + + $vfs_link = etemplate::empty_cell('label',$name,array( + 'size' => ','.egw_vfs::download_url($path).',,,_blank,,'.$path, + )); + // if dir is writable, add delete link + if (egw_vfs::is_writable(egw_vfs::dirname($path))) + { + $cell = etemplate::empty_cell('hbox','',array('size' => ',,0,0')); + etemplate::add_child($cell,$vfs_link); + $delete_icon = etemplate::empty_cell('button',$path,array( + 'label' => 'delete', + 'size' => 'delete', // icon + 'onclick' => "return confirm('Delete this file');", + 'span' => ',leftPad5', + )); + etemplate::add_child($cell,$delete_icon); + } + else + { + $cell = $vfs_link; + } + return $cell; + } + + /** + * Check if vfs file exists *without* using the extension + * + * If you rename a file, you have to clear the cache ($clear_after=true)! + * + * @param string &$path on call path without extension, if existing on return full path incl. extension + * @param boolean $clear_after=null clear file-cache after (true) or before (false), default dont clear + * @return + */ + static function file_exists(&$path,$clear_after=null) + { + static $files = array(); // static var, to scan each directory only once + $dir = egw_vfs::dirname($path); + if ($clear_after === false) unset($files[$dir]); + if (!isset($files[$dir])) $files[$dir] = egw_vfs::file_exists($dir) ? egw_vfs::scandir($dir) : array(); + + $basename = egw_vfs::basename($path); + $basename_len = strlen($basename); + $found = false; + foreach($files[$dir] as $file) + { + if (substr($file,0,$basename_len) == $basename) + { + $path = $dir.'/'.$file; + $found = true; + } + } + if ($clear_after === true) unset($files[$dir]); + //echo "

".__METHOD__."($path) returning ".array2string($found)."

\n"; + return $found; + } + + /** + * postprocessing method, called after the submission of the form + * + * It has to copy the allowed/valid data from $value_in to $value, otherwise the widget + * will return no data (if it has a preprocessing method). The framework insures that + * the post-processing of all contained widget has been done before. + * + * Only used by vfs-upload so far + * + * @param string $name form-name of the widget + * @param mixed &$value the extension returns here it's input, if there's any + * @param mixed &$extension_data persistent storage between calls or pre- and post-process + * @param boolean &$loop can be set to true to request a re-submision of the form/dialog + * @param object &$tmpl the eTemplate the widget belongs too + * @param mixed &value_in the posted values (already striped of magic-quotes) + * @return boolean true if $value has valid content, on false no content will be returned! + */ + function post_process($name,&$value,&$extension_data,&$loop,&$tmpl,$value_in) + { + //error_log(__METHOD__."('$name',".array2string($value).','.array2string($extension_data).",$loop,,".array2string($value_in).')'); + //echo '

'.__METHOD__."('$name',".array2string($value).','.array2string($extension_data).",$loop,,".array2string($value_in).")

\n"; + + if (!$extension_data) return false; + + switch($extension_data['type']) + { + case 'vfs-name': + $value = $extension_data['allowPath'] ? egw_vfs::encodePath($value_in) : egw_vfs::encodePathComponent($value_in); + return true; + + case 'vfs-upload': + break; // handeled below + + default: + return false; + } + // from here on vfs-upload only! + + // check if delete icon clicked + if ($_POST['submit_button'] == ($fname = str_replace($extension_data['value'],$extension_data['path'],$name)) || + substr($extension_data['path'],-1) == '/' && substr($_POST['submit_button'],0,strlen($fname)-1) == substr($fname,0,-1)) + { + if (substr($extension_data['path'],-1) == '/') // multiple files? + { + foreach($extension_data['files'] as $file) // check of each single file, to not allow deleting of arbitrary files + { + if ($_POST['submit_button'] == substr($fname,0,-1).$file.']') + { + if (!egw_vfs::unlink($extension_data['path'].$file)) + { + etemplate::set_validation_error($name,lang('Error deleting %1!',egw_vfs::decodePath($extension_data['path'].$file))); + } + break; + } + } + } + elseif (!egw_vfs::unlink($extension_data['path'])) + { + etemplate::set_validation_error($name,lang('Error deleting %1!',egw_vfs::decodePath($extension_data['path']))); + } + $loop = true; + return false; + } + + // handle file upload + $name = preg_replace('/^exec\[([^]]+)\](.*)$/','\\1\\2',$name); // remove exec prefix + + if (!is_array($_FILES['exec']) || !($filename = etemplate::get_array($_FILES['exec']['name'],$name))) + { + return false; // no file attached + } + $tmp_name = etemplate::get_array($_FILES['exec']['tmp_name'],$name); + $error = etemplate::get_array($_FILES['exec']['error'],$name); + if ($error) + { + etemplate::set_validation_error($name,lang('Error uploading file!')."\n". + etemplate::max_upload_size_message()); + $loop = true; + return false; + } + if (empty($tmp_name) || function_exists('is_uploaded_file') && !is_uploaded_file($tmp_name) || !file_exists($tmp_name)) + { + return false; + } + // check if type matches required mime-type, if specified + if (!empty($extension_data['mimetype'])) + { + $type = etemplate::get_array($_FILES['exec']['type'],$name); + $is_preg = $extension_data['mimetype'][0] == '/'; + if (!$is_preg && strcasecmp($extension_data['mimetype'],$type) || $is_preg && !preg_match($extension_data['mimetype'],$type)) + { + etemplate::set_validation_error($name,lang('File is of wrong type (%1 != %2)!',$type,$extension_data['mimetype'])); + return false; + } + } + $path = $extension_data['path']; + if (substr($path,-1) != '/') + { + // add extension to path + $parts = explode('.',$filename); + if (($extension = array_pop($parts)) && mime_magic::ext2mime($extension)) // really an extension --> add it to path + { + $path .= '.'.$extension; + } + } + else // multiple upload with dir given (trailing slash) + { + $path .= egw_vfs::encodePathComponent($filename); + } + if (!egw_vfs::file_exists($dir = egw_vfs::dirname($path)) && !egw_vfs::mkdir($dir,null,STREAM_MKDIR_RECURSIVE)) + { + etemplate::set_validation_error($name,lang('Error create parent directory %1!',egw_vfs::decodePath($dir))); + return false; + } + if (!copy($tmp_name,egw_vfs::PREFIX.$path)) + { + etemplate::set_validation_error($name,lang('Error copying uploaded file to vfs!')); + return false; + } + $value = $path; // return path of file, important if only a temporary location is used + + return true; + } +} diff --git a/filemanager/inc/class.filemanager_ui.inc.php b/filemanager/inc/class.filemanager_ui.inc.php index 81545b4e64..2857360b06 100644 --- a/filemanager/inc/class.filemanager_ui.inc.php +++ b/filemanager/inc/class.filemanager_ui.inc.php @@ -127,7 +127,7 @@ class filemanager_ui } else { - $msg .= lang('The requested path %1 is not available.',urldecode($path)); + $msg .= lang('The requested path %1 is not available.',egw_vfs::decodePath($path)); } // reset lettersearch as it confuses users (they think the dir is empty) $content['nm']['searchletter'] = false; @@ -205,11 +205,11 @@ class filemanager_ui $abs_target = $target[0] == '/' ? $target : egw_vfs::concat($content['nm']['path'],$target); if (!egw_vfs::stat($abs_target)) { - $content['nm']['msg'] = lang('Link target %1 not found!',urldecode($abs_target)); + $content['nm']['msg'] = lang('Link target %1 not found!',egw_vfs::decodePath($abs_target)); break; } $content['nm']['msg'] = egw_vfs::symlink($target,$link) ? - lang('Symlink to %1 created.',$target) : lang('Error creating symlink to target %1!',urldecode($target)); + lang('Symlink to %1 created.',$target) : lang('Error creating symlink to target %1!',egw_vfs::decodePath($target)); break; case 'paste': $content['nm']['msg'] = self::action($clipboard_type.'_paste',$clipboard_files,$content['nm']['path']); @@ -244,7 +244,7 @@ class filemanager_ui if ($upload_success) { $content['nm']['msg'] = count($upload_success) == 1 && !$upload_failure ? lang('File successful uploaded.') : - lang('%1 successful uploaded.',urldecode(implode(', ',$upload_success))); + lang('%1 successful uploaded.',implode(', ',$upload_success)); } if ($upload_failure) { @@ -262,9 +262,9 @@ class filemanager_ui $dir_is_writable = egw_vfs::is_writable($content['nm']['path']); } $content['paste_tooltip'] = $clipboard_files ? '

'.lang('%1 the following files into current directory', - $clipboard_type=='copy'?lang('Copy'):lang('Move')).':
'.urldecode(implode('
',$clipboard_files)).'

' : ''; + $clipboard_type=='copy'?lang('Copy'):lang('Move')).':
'.egw_vfs::decodePath(implode('
',$clipboard_files)).'

' : ''; $content['linkpaste_tooltip'] = $clipboard_files ? '

'.lang('%1 the following files into current directory', - lang('link')).':
'.urldecode(implode('
',$clipboard_files)).'

' : ''; + lang('link')).':
'.egw_vfs::decodePath(implode('
',$clipboard_files)).'

' : ''; $content['upload_size'] = etemplate::max_upload_size_message(); //_debug_array($content); @@ -320,7 +320,7 @@ class filemanager_ui } else { - $response->addScript("if (!confirm('".addslashes(lang('Do you want to overwrite the existing file %1?',urldecode($path)))."')) document.getElementById('$id').value='';"); + $response->addScript("if (!confirm('".addslashes(lang('Do you want to overwrite the existing file %1?',egw_vfs::decodePath($path)))."')) document.getElementById('$id').value='';"); } } else @@ -498,7 +498,7 @@ class filemanager_ui { if (preg_match('/^\/?(home|apps|)\/*$/',$path)) { - return lang("Cautiously rejecting to remove folder '%1'!",urldecode($path)); + return lang("Cautiously rejecting to remove folder '%1'!",egw_vfs::decodePath($path)); } } // now we use find to loop through all files and dirs: (selected only contains dirs now) @@ -558,7 +558,7 @@ class filemanager_ui // an appropriate message egw::redirect_link('/index.php',array('menuaction'=>'filemanager.filemanager_ui.index', 'path' => self::get_home_dir(), - 'msg' => lang('The requested path %1 is not available.',urldecode($query['path'])), + 'msg' => lang('The requested path %1 is not available.',egw_vfs::decodePath($query['path'])), )); } $rows = $dir_is_writable = array(); @@ -639,7 +639,7 @@ class filemanager_ui } else { - $GLOBALS['egw_info']['flags']['app_header'] = lang('Filemanager').': '.urldecode($query['path']); + $GLOBALS['egw_info']['flags']['app_header'] = lang('Filemanager').': '.egw_vfs::decodePath($query['path']); } return egw_vfs::$find_total; } @@ -737,14 +737,14 @@ class filemanager_ui } if (egw_vfs::rename($path,$to)) { - $msg .= lang('Renamed %1 to %2.',urldecode(basename($path)),urldecode(basename($to))).' '; + $msg .= lang('Renamed %1 to %2.',egw_vfs::decodePath(basename($path)),egw_vfs::decodePath(basename($to))).' '; $content['old']['name'] = $content[$name]; $path = $to; $content['mime'] = mime_magic::filename2mime($path); // recheck mime type } else { - $msg .= lang('Rename of %1 to %2 failed!',urldecode(basename($path)),urldecode(basename($to))).' '; + $msg .= lang('Rename of %1 to %2 failed!',egw_vfs::decodePath(basename($path)),egw_vfs::decodePath(basename($to))).' '; if (egw_vfs::deny_script($to)) { $msg .= lang('You are NOT allowed to upload a script!').' '; @@ -942,7 +942,7 @@ class filemanager_ui )); } $GLOBALS['egw_info']['flags']['java_script'] = "\n"; - $GLOBALS['egw_info']['flags']['app_header'] = lang('Preferences').' '.urldecode($path); + $GLOBALS['egw_info']['flags']['app_header'] = lang('Preferences').' '.egw_vfs::decodePath($path); $tpl->exec('filemanager.filemanager_ui.file',$content,$sel_options,$readonlys,$preserve,2); } diff --git a/phpgwapi/inc/class.egw_link.inc.php b/phpgwapi/inc/class.egw_link.inc.php new file mode 100644 index 0000000000..1d8ed40203 --- /dev/null +++ b/phpgwapi/inc/class.egw_link.inc.php @@ -0,0 +1,1149 @@ + + * @copyright 2001-2008 by RalfBecker@outdoor-training.de + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage link + * @version $Id$ + */ + +/** + * Generalized linking between entries of eGroupware apps + * + * Please note: this class can NOT and does not need to be initialised, all methods are static + * + * To participate in the linking an applications has to implement the following hooks: + * + * /** + * * Hook called by link-class to include app in the appregistry of the linkage + * * + * * @param array/string $location location and other parameters (not used) + * * @return array with method-names + * *% + * function search_link($location) + * { + * return array( + * 'query' => 'app.class.link_query', // method to search app for a pattern: array link_query(string $pattern, array $options) + * 'title' => 'app.class.link_title', // method to return title of an entry of app: string/false/null link_title(int/string $id) + * 'titles' => 'app.class.link_titles', // method to return multiple titles: array link_title(array $ids) + * 'view' => array( // get parameters to view an entry of app + * 'menuaction' => 'app.class.method', + * ), + * 'types' => array( // Optional list of sub-types to filter (eg organisations), app to handle different queries + * 'type_key' => array( + * 'name' => 'Human Reference', + * 'icon' => 'app/icon' // Optional icon to use for that sub-type + * ) + * ), + * 'view_id' => 'app_id', // name of get parameter of the id + * 'view_popup' => '400x300', // size of popup (XxY), if view is in popup + * 'add' => array( // get parameter to add an empty entry to app + * 'menuaction' => 'app.class.method', + * ), + * 'add_app' => 'link_app', // name of get parameter to add links to other app + * 'add_id' => 'link_id', // --------------------- " ------------------- id + * 'add_popup' => '400x300', // size of popup (XxY), if add is in popup + * 'notify' => 'app.class.method', // method to be called if an other applications liks or unlinks with app: notify(array $data) + * 'file_access' => 'app.class.method', // method to be called to check file access rights, see links_stream_wrapper class + * // boolean file_access(string $id,int $check,string $rel_path) + * 'find_extra' => array('name_preg' => '/^(?!.picture.jpg)$/') // extra options to egw_vfs::find, to eg. remove some files from the list of attachments + * 'edit' => array( + * 'menuaction' => 'app.class.method', + * ), + * 'edit_id' => 'app_id', + * 'edit_popup' => '400x300', + * } + * All entries are optional, thought you only get conected functionality, if you implement them ... + * + * The BO-layer implementes some extra features on top of the so-layer: + * 1) It handles links to not already existing entries. This is used by the eTemplate link-widget, which allows to + * setup links even for new / not already existing entries, before they get saved. + * In that case you have to set the first id to 0 for the link-static function and pass the array returned in that id + * (not the return-value) after saveing your new entry again to the link static function. + * 2) Attaching files: they are saved in the vfs and not the link-table (!). + * Attached files are stored under $vfs_basedir='/infolog' in the vfs! + * 3) It manages the link-registry, in which apps can register themselfs by implementing some hooks + * 4) It notifies apps, who registered for that service, about changes in the links their entries + */ +class egw_link extends solink +{ + /** + * appname used for returned attached files (!= 'filemanager'!) + */ + const VFS_APPNAME = 'file'; // pseudo-appname for own file-attachments in vfs, this is NOT the vfs-app + /** + * Baseurl for the attachments in the vfs + */ + const VFS_BASEURL = 'vfs://default/apps'; + /** + * Turns on debug-messages + */ + const DEBUG = false; + /** + * other apps can participate in the linking by implementing a 'search_link' hook, which + * has to return an array in the format of an app_register entry below + * + * @var array + */ + static $app_register = array( + 'felamimail' => array( + 'add' => array( + 'menuaction' => 'felamimail.uicompose.compose', + ), + 'add_popup' => '700x750', + ), + ); + /** + * Caches link titles for a better performance + * + * @var array + */ + private static $title_cache = array(); + + /** + * Cache file access permissions + * + * @var array + */ + private static $file_access_cache = array(); + + /** + * Private constructor to forbid instanciated use + * + */ + private function __construct() + { + + } + + /** + * initialize our static vars + */ + static function init_static( ) + { + // other apps can participate in the linking by implementing a search_link hook, which + // has to return an array in the format of an app_register entry + // for performance reasons, we do it only once / cache it in the session + if (!($search_link_hooks = $GLOBALS['egw']->session->appsession('search_link_hooks','phpgwapi'))) + { + $search_link_hooks = $GLOBALS['egw']->hooks->process('search_link'); + $GLOBALS['egw']->session->appsession('search_link_hooks','phpgwapi',$search_link_hooks); + } + if (is_array($search_link_hooks)) + { + foreach($search_link_hooks as $app => $data) + { + if (is_array($data)) + { + self::$app_register[$app] = $data; + } + } + } + if (!(self::$title_cache = $GLOBALS['egw']->session->appsession('link_title_cache','phpgwapi'))) + { + self::$title_cache = array(); + } + if (!(self::$file_access_cache = $GLOBALS['egw']->session->appsession('link_file_access_cache','phpgwapi'))) + { + self::$file_access_cache = array(); + } + //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); + } + + /** + * Called by egw::egw_final to store the title-cache in the session + * + */ + static function save_session_cache() + { + //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); + $GLOBALS['egw']->session->appsession('link_title_cache','phpgwapi',self::$title_cache); + $GLOBALS['egw']->session->appsession('link_file_access_cache','phpgwapi',self::$file_access_cache); + } + + /** + * creats a link between $app1,$id1 and $app2,$id2 - $id1 does NOT need to exist yet + * + * Does NOT check if link already exists. + * File-attachments return a negative link-id !!! + * + * @param string $app1 app of $id1 + * @param string/array &$id1 id of item to linkto or 0 if item not yet created or array with links + * of not created item or $file-array if $app1 == self::VFS_APPNAME (see below). + * If $id==0 it will be set on return to an array with the links for the new item. + * @param string/array $app2 app of 2.linkend or array with links ($id2 not used) + * @param string $id2='' id of 2. item of $file-array if $app2 == self::VFS_APPNAME (see below)
+ * $file array with 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 in $file are only needed if u want a symlink (if possible)) + * @param string $remark='' Remark to be saved with the link (defaults to '') + * @param int $owner=0 Owner of the link (defaults to user) + * @param int $lastmod=0 timestamp of last modification (defaults to now=time()) + * @param int $no_notify=0 &1 dont notify $app1, &2 dont notify $app2 + * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) + */ + static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0,$no_notify=0 ) + { + if (self::DEBUG) + { + echo "

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 array
\n"; + continue; + } + if ($link['app'] == self::VFS_APPNAME) + { + $link_id = self::attach_file($app1,$id1,$link['id'],$link['remark']); + } + else + { + $link_id = solink::link($app1,$id1,$link['app'],$link['id'], + $link['remark'],$link['owner'],$link['lastmod']); + + // notify both sides + if (!($no_notify&2)) self::notify('link',$link['app'],$link['id'],$app1,$id1,$link_id); + if (!($no_notify&1)) self::notify('link',$app1,$id1,$link['app'],$link['id'],$link_id); + } + } + return $link_id; + } + if ($app1 == self::VFS_APPNAME) + { + return self::attach_file($app2,$id2,$id1,$remark); + } + elseif ($app2 == self::VFS_APPNAME) + { + return self::attach_file($app1,$id1,$id2,$remark); + } + $link_id = solink::link($app1,$id1,$app2,$id2,$remark,$owner); + + if (!($no_notify&2)) self::notify('link',$app2,$id2,$app1,$id1,$link_id); + if (!($no_notify&1)) self::notify('link',$app1,$id1,$app2,$id2,$link_id); + + return $link_id; + } + + /** + * generate temporary link_id used as array-key + * + * @param string $app app-name + * @param mixed $id + * @return string + */ + static function temp_link_id($app,$id) + { + return $app.':'.($app != self::VFS_APPNAME ? $id : $id['name']); + } + + /** + * returns array of links to $app,$id (reimplemented to deal with not yet created items) + * + * @param string $app appname + * @param string/array $id id of entry in $app or array of links if entry not yet created + * @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 + * @param boolean $cache_titles=false should all titles be queryed and cached (allows to query each link app only once!) + * This option also removes links not viewable by current user from the result! + * @return array of links or empty array if no matching links found + */ + static function get_links( $app,$id,$only_app='',$order='link_lastmod DESC',$cache_titles=false ) + { + if (self::DEBUG) echo "

egw_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; }