From e9d851b14356bfb64a47dfe22e697eb23dc7f92e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 24 Jan 2015 10:02:51 +0000 Subject: [PATCH 01/42] fixed a couple more broken placeholders in Brasilian translation --- calendar/lang/egw_pt-br.lang | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/calendar/lang/egw_pt-br.lang b/calendar/lang/egw_pt-br.lang index 4ced5a17d8..1cf9a9bfd0 100644 --- a/calendar/lang/egw_pt-br.lang +++ b/calendar/lang/egw_pt-br.lang @@ -1,13 +1,13 @@ %1 %2 in %3 calendar pt-br %1 %2 em %3 %1 days calendar pt-br %1 dias %1 event(s) %2 calendar pt-br %1 evento (s) 2% -%1 hours calendar pt-br % uma hora +%1 hours calendar pt-br %1 uma hora %1 minutes calendar pt-br %1 minuto %1 records imported calendar pt-br %1 registro(s) importado(s) %1 records read (not yet imported, you may go back and uncheck test import) calendar pt-br %1 registro(s) lido(s) (não importado(s) ainda. Você deve voltar e desmarcar "Testar Importação") %1 weeks calendar pt-br %1 semanas -%s the event calendar pt-br % Do evento -(%1 events in %2 seconds) calendar pt-br (%1% 2 eventos em segundos) +%s the event calendar pt-br %1 do evento +(%1 events in %2 seconds) calendar pt-br (%1 eventos em %2 segundos) (empty = use global limit, no = no export at all) admin pt-br (= Vazias usar limite global, não = nenhuma exportação em tudo) , exceptions preserved calendar pt-br , Exceções preservados , stati of participants reset calendar pt-br , Estado de participantes redefinir @@ -17,7 +17,7 @@ accept calendar pt-br aceitar accept or reject an invitation calendar pt-br Aceitar ou rejeitar um convite accepted calendar pt-br Aceito access denied to the calendar of %1 !!! calendar pt-br Acesso negado à agenda de %1 -access to calendar of %1 denied! calendar pt-br Acesso ao calendário de 1% negada! +access to calendar of %1 denied! calendar pt-br Acesso ao calendário de %1 negada! action that caused the notify: added, canceled, accepted, rejected, ... calendar pt-br Ação que provocou a notificação: (Adicionada, Cancelada, Aceita, Rejeitada, ...) actions calendar pt-br Ações actions... calendar pt-br Ações... @@ -274,7 +274,7 @@ location, start- and endtimes, ... calendar pt-br Local, Horários de início e mail all participants calendar pt-br Enviar e-mail para todos os participantes make freebusy information available to not loged in persons? calendar pt-br Mostrar informação de disponibilidade para pessoas não logadas? manage mapping calendar pt-br Gerenciar mapeamento -maximum available quantity of %1 exceeded! calendar pt-br A quantidade máxima disponível de 1% excedido! +maximum available quantity of %1 exceeded! calendar pt-br A quantidade máxima disponível de %1 excedido! meeting request calendar pt-br Pedido de reunião meetingrequest to all participants calendar pt-br Meetingrequest a todos os participantes merge document... calendar pt-br Mesclar documento ... @@ -301,7 +301,7 @@ no owner selected calendar pt-br Nenhum proprietário selecionado. no preview for ical calendar pt-br Pré-visualização para iCal no recurrence calendar pt-br Sem retorno no response calendar pt-br Sem resposta -no rights to export more than %1 entries! calendar pt-br Nenhum direito a exportar mais de 1% entradas! +no rights to export more than %1 entries! calendar pt-br Nenhum direito a exportar mais de %1 entradas! non blocking calendar pt-br Posse compartilhada not calendar pt-br não not rejected calendar pt-br Não rejeitado From 70b603ac7714854142c03cc5da110abea177c02f Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 26 Jan 2015 09:15:07 +0000 Subject: [PATCH 02/42] moving VFS API classes into a namespaced PSR4 autoloadable structure: - PSR4 autoloader exists beside our old autloader to support old as well as new structure until everything is ported over - moved ported API stuff from phpgwapi to new api directory (idea is phpgwapi become a compatibility layer for old code, while we only port selected stuff to new api directory) - namespaces use prefix "EGroupware", then (first letter capitalised) app-name or "Api", sub-system names like "Vfs" or for apps "Ui", "Bo, "So" and at least class name starting with a capital letter and without understores eg. "StreamWrapper" plus just ".php" - examples: + egw_vfs in phpgwapi/inc/class.egw_vfs.inc.php --> EGroupware\Api\Vfs in api/src/Vfs.php + sqlfs_stream_wrapper in phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php --> EGroupware\Api\Vfs\Sqlfs\StreamWrapper in api/src/Vfs/Sqlfs/StreamWrapper.php + sqlfs_utils in phpgwapi/inc/class.sqlfs_utils.inc.php --> EGroupware\Api\Vfs\Sqlfs\Utils in api/src/Vfs/Sqlfs/Utils.php - api directory is no a new svn module but exists (like home) as sub-directory under base egroupware module --- api/src/Vfs.php | 2116 +++++++++++++++++ .../src/Vfs/Filesystem/StreamWrapper.php | 122 +- .../src/Vfs/Links/StreamWrapper.php | 87 +- api/src/Vfs/Sqlfs/StreamWrapper.php | 1939 +++++++++++++++ api/src/Vfs/Sqlfs/Utils.php | 483 ++++ .../src/Vfs/StreamWrapper.php | 69 +- api/src/Vfs/StreamWrapperIface.php | 250 ++ etemplate/inc/class.etemplate_new.inc.php | 2 +- phpgwapi/inc/class.egw_vfs.inc.php | 2089 +--------------- .../inc/class.iface_stream_wrapper.inc.php | 241 +- .../inc/class.sqlfs_stream_wrapper.inc.php | 1908 +-------------- phpgwapi/inc/class.sqlfs_utils.inc.php | 469 +--- phpgwapi/inc/common_functions.inc.php | 38 +- 13 files changed, 5008 insertions(+), 4805 deletions(-) create mode 100644 api/src/Vfs.php rename phpgwapi/inc/class.filesystem_stream_wrapper.inc.php => api/src/Vfs/Filesystem/StreamWrapper.php (86%) rename phpgwapi/inc/class.links_stream_wrapper.inc.php => api/src/Vfs/Links/StreamWrapper.php (80%) create mode 100644 api/src/Vfs/Sqlfs/StreamWrapper.php create mode 100644 api/src/Vfs/Sqlfs/Utils.php rename phpgwapi/inc/class.vfs_stream_wrapper.inc.php => api/src/Vfs/StreamWrapper.php (95%) create mode 100644 api/src/Vfs/StreamWrapperIface.php diff --git a/api/src/Vfs.php b/api/src/Vfs.php new file mode 100644 index 0000000000..bfe0ab9523 --- /dev/null +++ b/api/src/Vfs.php @@ -0,0 +1,2116 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use common; +use config; +use html; +use egw_db; +use translation; +use HTTP_WebDAV_Server; +use egw_exception_assertion_failed; +use egw_exception_db; +use egw_exception_wrong_parameter; + +/** + * Class containing static methods to use the new eGW virtual file system + * + * This extension of the vfs stream-wrapper allows to use the following static functions, + * which only allow access to the eGW VFS and need no 'vfs://default' prefix for filenames: + * + * All examples require a: use EGroupware\Api\Vfs; + * + * - resource Vfs::fopen($path,$mode) like fopen, returned resource can be used with fwrite etc. + * - resource Vfs::opendir($path) like opendir, returned resource can be used with readdir etc. + * - boolean Vfs::copy($from,$to) like copy + * - boolean Vfs::rename($old,$new) renaming or moving a file in the vfs + * - boolean Vfs::mkdir($path) creating a new dir in the vfs + * - boolean Vfs::rmdir($path) removing (an empty) directory + * - boolean Vfs::unlink($path) removing a file + * - boolean Vfs::touch($path,$mtime=null) touch a file + * - boolean Vfs::stat($path) returning status of file like stat(), but only with string keys (no numerical indexes)! + * + * With the exception of Vfs::touch() (not yet part of the stream_wrapper interface) + * you can always use the standard php functions, if you add a 'vfs://default' prefix + * to every filename or path. Be sure to always add the prefix, as the user otherwise gains + * access to the real filesystem of the server! + * + * The two following methods can be used to persitently mount further filesystems (without editing the code): + * + * - boolean|array Vfs::mount($url,$path) to mount $ur on $path or to return the fstab when called without argument + * - boolean Vfs::umount($path) to unmount a path or url + * + * The stream wrapper interface allows to access hugh files in junks to not be limited by the + * memory_limit setting of php. To do you should pass the opened file as resource and not the content: + * + * $file = Vfs::fopen('/home/user/somefile','r'); + * $content = fread($file,1024); + * + * You can also attach stream filters, to eg. base64 encode or compress it on the fly, + * without the need to hold the content of the whole file in memmory. + * + * If you want to copy a file, you can use stream_copy_to_stream to do a copy of a file far bigger then + * php's memory_limit: + * + * $from = Vfs::fopen('/home/user/fromfile','r'); + * $to = Vfs::fopen('/home/user/tofile','w'); + * + * stream_copy_to_stream($from,$to); + * + * The static Vfs::copy() method does exactly that, but you have to do it eg. on your own, if + * you want to copy eg. an uploaded file into the vfs. + * + * Vfs::parse_url($url, $component=-1), Vfs::dirname($url) and Vfs::basename($url) work + * on urls containing utf-8 characters, which get NOT urlencoded in our VFS! + */ +class Vfs extends Vfs\StreamWrapper +{ + const PREFIX = 'vfs://default'; + /** + * Readable bit, for dirs traversable + */ + const READABLE = 4; + /** + * Writable bit, for dirs delete or create files in that dir + */ + const WRITABLE = 2; + /** + * Excecutable bit, here only use to check if user is allowed to search dirs + */ + const EXECUTABLE = 1; + /** + * Name of the lock table + */ + const LOCK_TABLE = 'egw_locks'; + /** + * Current user has root rights, no access checks performed! + * + * @var boolean + */ + static $is_root = false; + /** + * Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id'] + * + * @var int + */ + static $user; + /** + * Current user is an eGW admin + * + * @var boolean + */ + static $is_admin = false; + /** + * Total of last find call + * + * @var int + */ + static $find_total; + /** + * Reference to the global db object + * + * @var egw_db + */ + static $db; + + /** + * fopen working on just the eGW VFS + * + * @param string $path filename with absolute path in the eGW VFS + * @param string $mode 'r', 'w', ... like fopen + * @return resource + */ + static function fopen($path,$mode) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("Filename '$path' is not an absolute path!"); + } + return fopen(self::PREFIX.$path,$mode); + } + + /** + * opendir working on just the eGW VFS: returns resource for readdir() etc. + * + * @param string $path filename with absolute path in the eGW VFS + * @return resource + */ + static function opendir($path) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); + } + return opendir(self::PREFIX.$path); + } + + /** + * dir working on just the eGW VFS: returns directory object + * + * @param string $path filename with absolute path in the eGW VFS + * @return Directory + */ + static function dir($path) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); + } + return dir(self::PREFIX.$path); + } + + /** + * scandir working on just the eGW VFS: returns array with filenames as values + * + * @param string $path filename with absolute path in the eGW VFS + * @param int $sorting_order =0 !$sorting_order (default) alphabetical in ascending order, $sorting_order alphabetical in descending order. + * @return array + */ + static function scandir($path,$sorting_order=0) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); + } + return scandir(self::PREFIX.$path,$sorting_order); + } + + /** + * copy working on just the eGW VFS + * + * @param string $from + * @param string $to + * @return boolean + */ + static function copy($from,$to) + { + $old_props = self::file_exists($to) ? self::propfind($to,null) : array(); + // copy properties (eg. file comment), if there are any and evtl. existing old properties + $props = self::propfind($from,null); + + foreach($old_props as $prop) + { + if (!self::find_prop($props,$prop)) + { + $prop['val'] = null; // null = delete prop + $props[] = $prop; + } + } + // using self::copy_uploaded() to treat copying incl. properties as atomar operation in respect of notifications + return self::copy_uploaded(self::PREFIX.$from,$to,$props,false); // false = no is_uploaded_file check! + } + + /** + * Find a specific property in an array of properties (eg. returned by propfind) + * + * @param array &$props + * @param array|string $name property array or name + * @param string $ns =self::DEFAULT_PROP_NAMESPACE namespace, only if $prop is no array + * @return &array reference to property in $props or null if not found + */ + static function &find_prop(array &$props,$name,$ns=self::DEFAULT_PROP_NAMESPACE) + { + if (is_array($name)) + { + $ns = $name['ns']; + $name = $name['name']; + } + foreach($props as &$prop) + { + if ($prop['name'] == $name && $prop['ns'] == $ns) return $prop; + } + return null; + } + + /** + * stat working on just the eGW VFS (alias of url_stat) + * + * @param string $path filename with absolute path in the eGW VFS + * @param boolean $try_create_home =false should a non-existing home-directory be automatically created + * @return array + */ + static function stat($path,$try_create_home=false) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); + } + if (($stat = self::url_stat($path,0,$try_create_home))) + { + $stat = array_slice($stat,13); // remove numerical indices 0-12 + } + return $stat; + } + + /** + * lstat (not resolving symbolic links) working on just the eGW VFS (alias of url_stat) + * + * @param string $path filename with absolute path in the eGW VFS + * @param boolean $try_create_home =false should a non-existing home-directory be automatically created + * @return array + */ + static function lstat($path,$try_create_home=false) + { + if ($path[0] != '/') + { + throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); + } + if (($stat = self::url_stat($path,STREAM_URL_STAT_LINK,$try_create_home))) + { + $stat = array_slice($stat,13); // remove numerical indices 0-12 + } + return $stat; + } + + /** + * is_dir() version working only inside the vfs + * + * @param string $path + * @return boolean + */ + static function is_dir($path) + { + return $path[0] == '/' && is_dir(self::PREFIX.$path); + } + + /** + * is_link() version working only inside the vfs + * + * @param string $path + * @return boolean + */ + static function is_link($path) + { + return $path[0] == '/' && is_link(self::PREFIX.$path); + } + + /** + * file_exists() version working only inside the vfs + * + * @param string $path + * @return boolean + */ + static function file_exists($path) + { + return $path[0] == '/' && file_exists(self::PREFIX.$path); + } + + /** + * Mounts $url under $path in the vfs, called without parameter it returns the fstab + * + * The fstab is stored in the eGW configuration and used for all eGW users. + * + * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/ + * @param string $path =null path to mount the filesystem in the vfs, eg. / + * @param boolean $check_url =null check if url is an existing directory, before mounting it + * default null only checks if url does not contain a $ as used in $user or $pass + * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request + * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount + * @return array|boolean array with fstab, if called without parameter or true on successful mount + */ + static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false) + { + if (is_null($check_url)) $check_url = strpos($url,'$') === false; + + if (!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup + { + $api_config = config::read('phpgwapi'); + if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) + { + self::$fstab = $api_config['vfs_fstab']; + } + unset($api_config); + } + if (is_null($url) || is_null($path)) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); + return self::$fstab; + } + if (!self::$is_root) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); + return false; // only root can mount + } + if ($clear_fstab) + { + self::$fstab = array(); + } + if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); + return true; // already mounted + } + self::load_wrapper(self::parse_url($url,PHP_URL_SCHEME)); + + if ($check_url && (!file_exists($url) || opendir($url) === false)) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); + return false; // url does not exist + } + self::$fstab[$path] = $url; + + uksort(self::$fstab,create_function('$a,$b','return strlen($a)-strlen($b);')); + + if ($persitent_mount) + { + config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); + $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; + // invalidate session cache + if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited + { + $GLOBALS['egw']->invalidate_session_cache(); + } + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); + return true; + } + + /** + * Unmounts a filesystem part of the vfs + * + * @param string $path url or path of the filesystem to unmount + */ + static function umount($path) + { + if (!self::$is_root) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); + return false; // only root can mount + } + if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); + return false; // $path not mounted + } + unset(self::$fstab[$path]); + + config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); + $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; + // invalidate session cache + if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited + { + $GLOBALS['egw']->invalidate_session_cache(); + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); + return true; + } + + /** + * Check if file is hidden: name starts with a '.' or is Thumbs.db + * + * @param string $path + * @return boolean + */ + public static function is_hidden($path) + { + $file = self::basename($path); + + return $file[0] == '.' || $file == 'Thumbs.db'; + } + + /** + * find = recursive search over the filesystem + * + * @param string|array $base base of the search + * @param array $options =null the following keys are allowed: + * - type => {d|f|F} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), default all + * - depth => {true|false(default)} put the contents of a dir before the dir itself + * - dirsontop => {true(default)|false} allways return dirs before the files (two distinct blocks) + * - mindepth,maxdepth minimal or maximal depth to be returned + * - name,path => pattern with *,? wildcards, eg. "*.php" + * - name_preg,path_preg => preg regular expresion, eg. "/(vfs|wrapper)/" + * - uid,user,gid,group,nouser,nogroup file belongs to user/group with given name or (numerical) id + * - mime => type[/subtype] or perl regular expression starting with a "/" eg. "/^(image|video)\\//i" + * - empty,size => (+|-|)N + * - cmin/mmin => (+|-|)N file/dir create/modified in the last N minutes + * - ctime/mtime => (+|-|)N file/dir created/modified in the last N days + * - url => false(default),true allow (and return) full URL's instead of VFS pathes (only set it, if you know what you doing securitywise!) + * - need_mime => false(default),true should we return the mime type + * - order => name order rows by name column + * - sort => (ASC|DESC) sort, default ASC + * - limit => N,[n=0] return N entries from position n on, which defaults to 0 + * - follow => {true|false(default)} follow symlinks + * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db) + * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or + * true to return file => stat pairs + * @param array $exec_params =null further params for exec as array, path is always the first param and stat the last! + * @return array of pathes if no $exec, otherwise path => stat pairs + */ + static function find($base,$options=null,$exec=null,$exec_params=null) + { + //error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n"); + + $type = $options['type']; // 'd', 'f' or 'F' + $dirs_last = $options['depth']; // put content of dirs before the dir itself + // show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary) + $dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0); + if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work + + // process some of the options (need to be done only once) + if (isset($options['name']) && !isset($options['name_preg'])) // change from simple *,? wildcards to preg regular expression once + { + $options['name_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['name'])).'$/i'; + } + if (isset($options['path']) && !isset($options['preg_path'])) // change from simple *,? wildcards to preg regular expression once + { + $options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'])).'$/i'; + } + if (!isset($options['uid'])) + { + if (isset($options['user'])) + { + $options['uid'] = $GLOBALS['egw']->accounts->name2id($options['user'],'account_lid','u'); + } + elseif (isset($options['nouser'])) + { + $options['uid'] = 0; + } + } + if (!isset($options['gid'])) + { + if (isset($options['group'])) + { + $options['gid'] = abs($GLOBALS['egw']->accounts->name2id($options['group'],'account_lid','g')); + } + elseif (isset($options['nogroup'])) + { + $options['gid'] = 0; + } + } + if ($options['order'] == 'mime') + { + $options['need_mime'] = true; // we need to return the mime colum + } + $url = $options['url']; + + if (!is_array($base)) + { + $base = array($base); + } + $result = array(); + foreach($base as $path) + { + if (!$url) + { + if ($path[0] != '/' || !self::stat($path)) continue; + $path = self::PREFIX . $path; + } + if (!isset($options['remove'])) + { + $options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0; + } + $is_dir = is_dir($path); + if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir)) + { + self::_check_add($options,$path,$result); + } + if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 && $options['depth'] < $options['maxdepth'])) && ($dir = @opendir($path))) + { + while(($fname = readdir($dir)) !== false) + { + if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir! + + if (self::is_hidden($fname) && !$options['hidden']) continue; // ignore hidden files + + $file = self::concat($path, $fname); + + if ((int)$options['mindepth'] <= 1) + { + self::_check_add($options,$file,$result); + } + // only descend into subdirs, if it's a real dir (no link to a dir) or we should follow symlinks + if (is_dir($file) && ($options['follow'] || !is_link($file)) && (!isset($options['maxdepth']) || $options['maxdepth'] > 1)) + { + $opts = $options; + if ($opts['mindepth']) $opts['mindepth']--; + if ($opts['maxdepth']) $opts['depth']++; + unset($opts['order']); + unset($opts['limit']); + foreach(self::find($options['url']?$file:self::parse_url($file,PHP_URL_PATH),$opts,true) as $p => $s) + { + unset($result[$p]); + $result[$p] = $s; + } + } + } + closedir($dir); + } + if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last) + { + self::_check_add($options,$path,$result); + } + } + // sort code, to place directories before files, if $dirsontop enabled + $dirsfirst = $dirsontop ? '($a[mime]==\''.self::DIR_MIME_TYPE.'\')!==($b[mime]==\''.self::DIR_MIME_TYPE.'\')?'. + '($a[mime]==\''.self::DIR_MIME_TYPE.'\'?-1:1):' : ''; + // ordering of the rows + if (isset($options['order'])) + { + $sort = strtolower($options['sort']) == 'desc' ? '-' : ''; + switch($options['order']) + { + // sort numerical + case 'size': + case 'uid': + case 'gid': + case 'mode': + case 'ctime': + case 'mtime': + $code = $dirsfirst.$sort.'($a[\''.$options['order'].'\']-$b[\''.$options['order'].'\']);'; + // always use name as second sort criteria + $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; + $ok = uasort($result,create_function('$a,$b',$code)); + break; + + // sort alphanumerical + default: + $options['order'] = 'name'; + // fall throught + case 'name': + case 'mime': + $code = $dirsfirst.$sort.'strcasecmp($a[\''.$options['order'].'\'],$b[\''.$options['order'].'\']);'; + if ($options['order'] != 'name') + { + // always use name as second sort criteria + $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; + } + else + { + $code = 'return '.$code; + } + $ok = uasort($result,create_function('$a,$b',$code)); + break; + } + //echo "

order='$options[order]', sort='$options[sort]' --> uasort($result,create_function(,'$code'))=".array2string($ok)."

>\n"; + } + // limit resultset + self::$find_total = count($result); + if (isset($options['limit'])) + { + list($limit,$start) = explode(',',$options['limit']); + if (!$limit && !($limit = $GLOBALS['egw_info']['user']['preferences']['comman']['maxmatches'])) $limit = 15; + //echo "total=".self::$find_total.", limit=$options[limit] --> start=$start, limit=$limit
\n"; + + if ((int)$start || self::$find_total > $limit) + { + $result = array_slice($result,(int)$start,(int)$limit,true); + } + } + //echo $path; _debug_array($result); + if ($exec !== true && is_callable($exec)) + { + if (!is_array($exec_params)) + { + $exec_params = is_null($exec_params) ? array() : array($exec_params); + } + foreach($result as $path => &$stat) + { + $options = $exec_params; + array_unshift($options,$path); + array_push($options,$stat); + //echo "calling ".print_r($exec,true).print_r($options,true)."\n"; + $stat = call_user_func_array($exec,$options); + } + return $result; + } + //error_log("self::find($path)=".print_r(array_keys($result),true)); + if ($exec !== true) + { + return array_keys($result); + } + return $result; + } + + /** + * Function carying out the various (optional) checks, before files&dirs get returned as result of find + * + * @param array $options options, see self::find(,$options) + * @param string $path name of path to add + * @param array &$result here we add the stat for the key $path, if the checks are successful + */ + private static function _check_add($options,$path,&$result) + { + $type = $options['type']; // 'd' or 'f' + + if ($options['url']) + { + $stat = @lstat($path); + } + else + { + $stat = self::url_stat($path,STREAM_URL_STAT_LINK); + } + if (!$stat) + { + return; // not found, should not happen + } + if ($type && (($type == 'd') == !($stat['mode'] & Vfs\Sqlfs\StreamWrapper::MODE_DIR) || // != is_dir() which can be true for symlinks + $type == 'F' && is_dir($path))) // symlink to a directory + { + return; // wrong type + } + $stat = array_slice($stat,13); // remove numerical indices 0-12 + $stat['path'] = self::parse_url($path,PHP_URL_PATH); + $stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path); + + if ($options['mime'] || $options['need_mime']) + { + $stat['mime'] = self::mime_content_type($path); + } + if (isset($options['name_preg']) && !preg_match($options['name_preg'],$stat['name']) || + isset($options['path_preg']) && !preg_match($options['path_preg'],$path)) + { + //echo "

!preg_match('{$options['name_preg']}','{$stat['name']}')

\n"; + return; // wrong name or path + } + if (isset($options['gid']) && $stat['gid'] != $options['gid'] || + isset($options['uid']) && $stat['uid'] != $options['uid']) + { + return; // wrong user or group + } + if (isset($options['mime']) && $options['mime'] != $stat['mime']) + { + if ($options['mime'][0] == '/') // perl regular expression given + { + if (!preg_match($options['mime'], $stat['mime'])) + { + return; // wrong mime-type + } + } + else + { + list($type,$subtype) = explode('/',$options['mime']); + // no subtype (eg. 'image') --> check only the main type + if ($subtype || substr($stat['mime'],0,strlen($type)+1) != $type.'/') + { + return; // wrong mime-type + } + } + } + if (isset($options['size']) && !self::_check_num($stat['size'],$options['size']) || + (isset($options['empty']) && !!$options['empty'] !== !$stat['size'])) + { + return; // wrong size + } + if (isset($options['cmin']) && !self::_check_num(round((time()-$stat['ctime'])/60),$options['cmin']) || + isset($options['mmin']) && !self::_check_num(round((time()-$stat['mtime'])/60),$options['mmin']) || + isset($options['ctime']) && !self::_check_num(round((time()-$stat['ctime'])/86400),$options['ctime']) || + isset($options['mtime']) && !self::_check_num(round((time()-$stat['mtime'])/86400),$options['mtime'])) + { + return; // not create/modified in the spezified time + } + // do we return url or just vfs pathes + if (!$options['url']) + { + $path = self::parse_url($path,PHP_URL_PATH); + } + $result[$path] = $stat; + } + + private static function _check_num($value,$argument) + { + if (is_int($argument) && $argument >= 0 || $argument[0] != '-' && $argument[0] != '+') + { + //echo "_check_num($value,$argument) check = == ".(int)($value == $argument)."\n"; + return $value == $argument; + } + if ($argument < 0) + { + //echo "_check_num($value,$argument) check < == ".(int)($value < abs($argument))."\n"; + return $value < abs($argument); + } + //echo "_check_num($value,$argument) check > == ".(int)($value > (int)substr($argument,1))."\n"; + return $value > (int) substr($argument,1); + } + + /** + * Recursiv remove all given url's, including it's content if they are files + * + * @param string|array $urls url or array of url's + * @param boolean $allow_urls =false allow to use url's, default no only pathes (to stay within the vfs) + * @throws egw_exception_assertion_failed when trainig to remove /, /apps or /home + * @return array + */ + static function remove($urls,$allow_urls=false) + { + //error_log(__METHOD__.'('.array2string($urls).')'); + // some precaution to never allow to (recursivly) remove /, /apps or /home + foreach((array)$urls as $url) + { + if (preg_match('/^\/?(home|apps|)\/*$/',self::parse_url($url,PHP_URL_PATH))) + { + throw new egw_exception_assertion_failed(__METHOD__.'('.array2string($urls).") Cautiously rejecting to remove folder '$url'!"); + } + } + return self::find($urls,array('depth'=>true,'url'=>$allow_urls,'hidden'=>true),array(__CLASS__,'_rm_rmdir')); + } + + /** + * Helper function for remove: either rmdir or unlink given url (depending if it's a dir or file) + * + * @param string $url + * @return boolean + */ + static function _rm_rmdir($url) + { + if ($url[0] == '/') + { + $url = self::PREFIX . $url; + } + if (is_dir($url) && !is_link($url)) + { + return self::rmdir($url,0); + } + return self::unlink($url); + } + + /** + * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, + * which is wrong in case of our vfs, as we use the current users id and memberships + * + * @param string $path + * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE, + * 2 = self::WRITABLE, 1 = self::EXECUTABLE + * @return boolean + */ + static function is_readable($path,$check = self::READABLE) + { + return self::check_access($path,$check); + } + + /** + * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, + * which is wrong in case of our vfs, as we use the current users id and memberships + * + * @param string $path path + * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE, + * 2 = self::WRITABLE, 1 = self::EXECUTABLE + * @param array|boolean $stat =null stat array or false, to not query it again + * @param int $user =null user used for check, if not current user (self::$user) + * @return boolean + */ + static function check_access($path, $check, $stat=null, $user=null) + { + if (is_null($stat) && $user && $user != self::$user) + { + static $path_user_stat = array(); + + $backup_user = self::$user; + self::$user = $user; + + if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user])) + { + self::clearstatcache($path); + + $path_user_stat[$path][$user] = self::url_stat($path, 0); + + self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again! + } + if (($stat = $path_user_stat[$path][$user])) + { + // some backend mounts use $user:$pass in their url, for them we have to deny access! + if (strpos(self::resolve_url($path, false, false, false), '$user') !== false) + { + $ret = false; + } + else + { + $ret = self::check_access($path, $check, $stat); + } + } + else + { + $ret = false; // no access, if we can not stat the file + } + self::$user = $backup_user; + + // we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session + self::clearstatcache($path); + + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check,$user) ".array2string($ret)); + return $ret; + } + + if (self::$is_root) + { + return true; + } + + // throw exception if stat array is used insead of path, can be removed soon + if (is_array($path)) + { + throw new egw_exception_wrong_parameter('path has to be string, use check_access($path,$check,$stat=null)!'); + } + // query stat array, if not given + if (is_null($stat)) + { + $stat = self::url_stat($path,0); + } + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)"); + + if (!$stat) + { + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) no stat array!"); + return false; // file not found + } + // check if we use an EGroupwre stream wrapper, or a stock php one + // if it's not an EGroupware one, we can NOT use uid, gid and mode! + if (($scheme = self::parse_url($stat['url'],PHP_URL_SCHEME)) && !(class_exists(self::scheme2class($scheme)))) + { + switch($check) + { + case self::READABLE: + return is_readable($stat['url']); + case self::WRITABLE: + return is_writable($stat['url']); + case self::EXECUTABLE: + return is_executable($stat['url']); + } + } + // check if other rights grant access + if (($stat['mode'] & $check) == $check) + { + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!"); + return true; + } + // check if there's owner access and we are the owner + if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user) + { + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!"); + return true; + } + // check if there's a group access and we have the right membership + if (($stat['mode'] & ($check << 3)) == ($check << 3) && $stat['gid']) + { + if (($memberships = $GLOBALS['egw']->accounts->memberships(self::$user, true)) && in_array(-abs($stat['gid']), $memberships)) + { + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via group rights!"); + return true; + } + } + // check backend for extended acls (only if path given) + $ret = $path && self::_call_on_backend('check_extended_acl',array(isset($stat['url'])?$stat['url']:$path,$check),true); // true = fail silent if backend does not support + + //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) ".($ret ? 'backend extended acl granted access.' : 'no access!!!')); + return $ret; + } + + /** + * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, + * which is wrong in case of our vfs, as we use the current users id and memberships + * + * @param string $path + * @return boolean + */ + static function is_writable($path) + { + return self::is_readable($path,self::WRITABLE); + } + + /** + * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, + * which is wrong in case of our vfs, as we use the current users id and memberships + * + * @param string $path + * @return boolean + */ + static function is_executable($path) + { + return self::is_readable($path,self::EXECUTABLE); + } + + /** + * Check if path is a script and write access would be denied by backend + * + * @param string $path + * @return boolean true if $path is a script AND exec mount-option is NOT set, false otherwise + */ + static function deny_script($path) + { + return self::_call_on_backend('deny_script',array($path),true); + } + + /** + * Name of EACL array in session + */ + const SESSION_EACL = 'session-eacl'; + + /** + * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) + * + * Does NOT check if user has the rights to set the extended acl for the given url/path! + * + * @param string $url string with path + * @param int $rights =null rights to set, or null to delete the entry + * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path + * @param boolean $session_only =false true: set eacl only for this session, does NO further checks currently! + * @return boolean true if acl is set/deleted, false on error + */ + static function eacl($url,$rights=null,$owner=null,$session_only=false) + { + if ($session_only) + { + $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); + $session_eacls[] = array( + 'path' => $url[0] == '/' ? $url : self::parse_url($url, PHP_URL_PATH), + 'owner' => $owner ? $owner : self::$user, + 'rights' => $rights, + ); + return true; + } + return self::_call_on_backend('eacl',array($url,$rights,$owner)); + } + + /** + * Get all ext. ACL set for a path + * + * Calls itself recursive, to get the parent directories + * + * @param string $path + * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found + */ + static function get_eacl($path) + { + $eacls = self::_call_on_backend('get_eacl',array($path),true); // true = fail silent (no PHP Warning) + + $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); + if ($session_eacls) + { + // eacl is recursive, therefore we have to match all parent-dirs too + $paths = array($path); + while ($path && $path != '/') + { + $paths[] = $path = self::dirname($path); + } + foreach((array)$session_eacls as $eacl) + { + if (in_array($eacl['path'], $paths)) + { + $eacls[] = $eacl; + } + } + + // sort by length descending, to show precedence + usort($eacls, function($a, $b) { + return strlen($b['path']) - strlen($a['path']); + }); + } + return $eacls; + } + + /** + * Store properties for a single ressource (file or dir) + * + * @param string $path string with path + * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) + * @return boolean true if props are updated, false otherwise (eg. ressource not found) + */ + static function proppatch($path,array $props) + { + return self::_call_on_backend('proppatch',array($path,$props)); + } + + /** + * Default namespace for properties set by eGroupware: comment or custom fields (leading #) + * + */ + const DEFAULT_PROP_NAMESPACE = 'http://egroupware.org/'; + + /** + * Read properties for a ressource (file, dir or all files of a dir) + * + * @param array|string $path (array of) string with path + * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, otherwise use null + * @return array|boolean array with props (values for keys 'name', 'ns', 'val'), or path => array of props for is_array($path) + * false if $path does not exist + */ + static function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE) + { + return self::_call_on_backend('propfind',array($path,$ns),true); // true = fail silent (no PHP Warning) + } + + /** + * Private constructor to prevent instanciating this class, only it's static methods should be used + */ + private function __construct() + { + + } + + /** + * Convert a symbolic mode string or octal mode to an integer + * + * @param string|int $set comma separated mode string to set [ugo]+[+=-]+[rwx]+ + * @param int $mode =0 current mode of the file, necessary for +/- operation + * @return int + */ + static function mode2int($set,$mode=0) + { + if (is_int($set)) // already an integer + { + return $set; + } + if (is_numeric($set)) // octal string + { + //error_log(__METHOD__."($set,$mode) returning ".(int)base_convert($set,8,10)); + return (int)base_convert($set,8,10); // convert octal to decimal + } + foreach(explode(',',$set) as $s) + { + $matches = null; + if (!preg_match($use='/^([ugoa]*)([+=-]+)([rwx]+)$/',$s,$matches)) + { + $use = str_replace(array('/','^','$','(',')'),'',$use); + throw new egw_exception_wrong_userinput("$s is not an allowed mode, use $use !"); + } + $base = (strpos($matches[3],'r') !== false ? self::READABLE : 0) | + (strpos($matches[3],'w') !== false ? self::WRITABLE : 0) | + (strpos($matches[3],'x') !== false ? self::EXECUTABLE : 0); + + for($n = $m = 0; $n < strlen($matches[1]); $n++) + { + switch($matches[1][$n]) + { + case 'o': + $m |= $base; + break; + case 'g': + $m |= $base << 3; + break; + case 'u': + $m |= $base << 6; + break; + default: + case 'a': + $m = $base | ($base << 3) | ($base << 6); + } + } + switch($matches[2]) + { + case '+': + $mode |= $m; + break; + case '=': + $mode = $m; + break; + case '-': + $mode &= ~$m; + } + } + //error_log(__METHOD__."($set,) returning ".sprintf('%o',$mode)); + return $mode; + } + + /** + * Convert a numerical mode to a symbolic mode-string + * + * @param int $mode + * @return string + */ + static function int2mode( $mode ) + { + if(($mode & self::MODE_LINK) == self::MODE_LINK) // Symbolic Link + { + $sP = 'l'; + } + elseif(($mode & 0xC000) == 0xC000) // Socket + { + $sP = 's'; + } + elseif($mode & 0x1000) // FIFO pipe + { + $sP = 'p'; + } + elseif($mode & 0x2000) // Character special + { + $sP = 'c'; + } + elseif($mode & 0x4000) // Directory + { + $sP = 'd'; + } + elseif($mode & 0x6000) // Block special + { + $sP = 'b'; + } + elseif($mode & 0x8000) // Regular + { + $sP = '-'; + } + else // UNKNOWN + { + $sP = 'u'; + } + + // owner + $sP .= (($mode & 0x0100) ? 'r' : '-') . + (($mode & 0x0080) ? 'w' : '-') . + (($mode & 0x0040) ? (($mode & 0x0800) ? 's' : 'x' ) : + (($mode & 0x0800) ? 'S' : '-')); + + // group + $sP .= (($mode & 0x0020) ? 'r' : '-') . + (($mode & 0x0010) ? 'w' : '-') . + (($mode & 0x0008) ? (($mode & 0x0400) ? 's' : 'x' ) : + (($mode & 0x0400) ? 'S' : '-')); + + // world + $sP .= (($mode & 0x0004) ? 'r' : '-') . + (($mode & 0x0002) ? 'w' : '-') . + (($mode & 0x0001) ? (($mode & 0x0200) ? 't' : 'x' ) : + (($mode & 0x0200) ? 'T' : '-')); + + return $sP; + } + + /** + * Get the closest mime icon + * + * @param string $mime_type + * @param boolean $et_image =true return $app/$icon string for etemplate (default) or html img tag if false + * @param int $size =128 + * @return string + */ + static function mime_icon($mime_type, $et_image=true, $size=128) + { + if ($mime_type == self::DIR_MIME_TYPE) + { + $mime_type = 'Directory'; + } + if(!$mime_type) + { + $mime_type = 'unknown'; + } + $mime_full = strtolower(str_replace ('/','_',$mime_type)); + list($mime_part) = explode('_',$mime_full); + + if (!($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_full)) && + // check mime-alias-map before falling back to more generic icons + !(isset(mime_magic::$mime_alias_map[$mime_type]) && + ($img=common::image('etemplate',$icon='mime'.$size.'_'.str_replace('/','_',mime_magic::$mime_alias_map[$mime_full])))) && + !($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_part))) + { + $img = common::image('etemplate',$icon='mime'.$size.'_unknown'); + } + if ($et_image === 'url') + { + return $img; + } + if ($et_image) + { + return 'etemplate/'.$icon; + } + return html::image('etemplate',$icon,mime_magic::mime2label($mime_type)); + } + + /** + * Human readable size values in k, M or G + * + * @param int $size + * @return string + */ + static function hsize($size) + { + if ($size < 1024) return $size; + if ($size < 1024*1024) return sprintf('%3.1lfk',(float)$size/1024); + if ($size < 1024*1024*1024) return sprintf('%3.1lfM',(float)$size/(1024*1024)); + return sprintf('%3.1lfG',(float)$size/(1024*1024*1024)); + } + + /** + * Size in bytes, from human readable + * + * From PHP ini_get docs, Ivo Mandalski 15-Nov-2011 08:27 + */ + static function int_size($_val) + { + if(empty($_val))return 0; + + $val = trim($_val); + + $matches = null; + preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); + + $last = ''; + if(isset($matches[2])){ + $last = $matches[2]; + } + + if(isset($matches[1])){ + $val = (int) $matches[1]; + } + + switch (strtolower($last)) + { + case 'g': + case 'gb': + $val *= 1024; + case 'm': + case 'mb': + $val *= 1024; + case 'k': + case 'kb': + $val *= 1024; + } + + return (int) $val; + } + + /** + * like basename($path), but also working if the 1. char of the basename is non-ascii + * + * @param string $_path + * @return string + */ + static function basename($_path) + { + list($path) = explode('?',$_path); // remove query + $parts = explode('/',$path); + + return array_pop($parts); + } + + /** + * Get the directory / parent of a given path or url(!), return false for '/'! + * + * Also works around PHP under Windows returning dirname('/something') === '\\', which is NOT understood by EGroupware's VFS! + * + * @param string $_url path or url + * @return string|boolean parent or false if there's none ($path == '/') + */ + static function dirname($_url) + { + list($url,$query) = explode('?',$_url,2); // strip the query first, as it can contain slashes + + if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/') + { + //error_log(__METHOD__."($url) returning FALSE: already in root!"); + return false; + } + $parts = explode('/',$url); + if (substr($url,-1) == '/') array_pop($parts); + array_pop($parts); + if ($url[0] != '/' && count($parts) == 3 || count($parts) == 1 && $parts[0] === '') + { + array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/ + } + //error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : '')); + return implode('/',$parts).($query ? '?'.$query : ''); + } + + /** + * Check if the current use has owner rights for the given path or stat + * + * We define all eGW admins the owner of the group directories! + * + * @param string $path + * @param array $stat =null stat for path, default queried by this function + * @return boolean + */ + static function has_owner_rights($path,array $stat=null) + { + if (!$stat) $stat = self::url_stat($path,0); + + return $stat['uid'] == self::$user || // user is the owner + self::$is_root || // class runs with root rights + !$stat['uid'] && $stat['gid'] && self::$is_admin; // group directory and user is an eGW admin + } + + /** + * Concat a relative path to an url, taking into account, that the url might already end with a slash or the path starts with one or is empty + * + * Also normalizing the path, as the relative path can contain ../ + * + * @param string $_url base url or path, might end in a / + * @param string $relative relative path to add to $url + * @return string + */ + static function concat($_url,$relative) + { + list($url,$query) = explode('?',$_url,2); + if (substr($url,-1) == '/') $url = substr($url,0,-1); + $ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative); + + // now normalize the path (remove "/something/..") + while (strpos($ret,'/../') !== false) + { + list($a_str,$b_str) = explode('/../',$ret,2); + $a = explode('/',$a_str); + array_pop($a); + $b = explode('/',$b_str); + $ret = implode('/',array_merge($a,$b)); + } + return $ret.($query ? (strpos($url,'?')===false ? '?' : '&').$query : ''); + } + + /** + * Build an url from it's components (reverse of parse_url) + * + * @param array $url_parts values for keys 'scheme', 'host', 'user', 'pass', 'query', 'fragment' (all but 'path' are optional) + * @return string + */ + static function build_url(array $url_parts) + { + $url = (!isset($url_parts['scheme'])?'':$url_parts['scheme'].'://'. + (!isset($url_parts['user'])?'':$url_parts['user'].(!isset($url_parts['pass'])?'':':'.$url_parts['pass']).'@'). + $url_parts['host']).$url_parts['path']. + (!isset($url_parts['query'])?'':'?'.$url_parts['query']). + (!isset($url_parts['fragment'])?'':'?'.$url_parts['fragment']); + //error_log(__METHOD__.'('.array2string($url_parts).") = '".$url."'"); + return $url; + } + + /** + * URL to download a file + * + * We use our webdav handler as download url instead of an own download method. + * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. + * + * Please note: If you dont use eTemplate or the html class, you have to run this url throught egw::link() to get a full url + * + * @param string $path + * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! + * @todo get $force_download working through webdav + * @return string + */ + static function download_url($path,$force_download=false) + { + if (($url = self::_call_on_backend('download_url',array($path,$force_download),true))) + { + return $url; + } + if ($path[0] != '/') + { + $path = self::parse_url($path,PHP_URL_PATH); + } + // we do NOT need to encode % itself, as our path are already url encoded, with the exception of ' ' and '+' + // we urlencode double quotes '"', as that fixes many problems in html markup + return '/webdav.php'.strtr($path,array('+' => '%2B',' ' => '%20','"' => '%22')).($force_download ? '?download' : ''); + } + + /** + * Download the given file list as a ZIP + * + * @param array $_files List of files to include in the zip + * @param string $name optional Zip file name. If not provided, it will be determined automatically from the files + * + * @return undefined + */ + public static function download_zip(Array $_files, $name = false) + { + error_log(__METHOD__ . ': '.implode(',',$_files)); + + // Create zip file + $zip_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip'); + + $zip = new ZipArchive(); + if (!$zip->open($zip_file, ZipArchive::OVERWRITE)) + { + throw new egw_exception("Cannot open zip file for writing."); + } + + // Find lowest common directory, to use relative paths + // eg: User selected /home/nathan/picture.jpg, /home/Pictures/logo.jpg + // We want /home + $dirs = array(); + foreach($_files as $file) + { + $dirs[] = self::dirname($file); + } + $paths = array_unique($dirs); + if(count($paths) > 0) + { + // Shortest to longest + usort($paths, function($a, $b) { + return strlen($a) - strlen($b); + }); + + // Start with shortest, pop off sub-directories that don't match + $parts = explode('/',$paths[0]); + foreach($paths as $path) + { + $dirs = explode('/',$path); + foreach($dirs as $dir_index => $dir) + { + if($parts[$dir_index] && $parts[$dir_index] != $dir) + { + unset($parts[$dir_index]); + } + } + } + $base_dir = implode('/', $parts); + } + else + { + $base_dir = $paths[0]; + } + + // Remove 'unsafe' filename characters + // (en.wikipedia.org/wiki/Filename#Reserved_characters_and_words) + $replace = array( + // Linux + '/', + // Windows + '\\','?','%','*',':','|',/*'.',*/ '"','<','>' + ); + + // A nice name for the user, + $filename = $GLOBALS['egw_info']['server']['site_title'] . '_' . + str_replace($replace,'_',( + $name ? $name : ( + count($_files) == 1 ? + // Just one file (hopefully a directory?) selected + self::basename($_files[0]) : + // Use the lowest common directory (eg: Infolog, Open, nathan) + self::basename($base_dir)) + )) . '.zip'; + + // Make sure basename is a dir + if(substr($base_dir, -1) != '/') + { + $base_dir .='/'; + } + + // Go into directories, find them all + $files = self::find($_files); + $links = array(); + + // We need to remove them _after_ we're done + $tempfiles = array(); + + // Give 1 second per file + set_time_limit(count($files)); + + // Add files to archive + foreach($files as &$addfile) + { + // Use relative paths inside zip + $relative = substr($addfile, strlen($base_dir)); + + // Use safe names - replace unsafe chars, convert to ASCII (ZIP spec says CP437, but we'll try) + $path = explode('/',$relative); + $_name = translation::convert(translation::to_ascii(implode('/', str_replace($replace,'_',$path))),false,'ASCII'); + + // Don't go infinite with app entries + if(self::is_link($addfile)) + { + if(in_array($addfile, $links)) continue; + $links[] = $addfile; + } + // Add directory - if empty, client app might not show it though + if(self::is_dir($addfile)) + { + // Zip directories + $zip->addEmptyDir($addfile); + } + else if(self::is_readable($addfile)) + { + // Copy to temp file, as ZipArchive fails to read VFS + $temp = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip_'); + $from = self::fopen($addfile,'r'); + $to = fopen($temp,'w'); + if(!stream_copy_to_stream($from,$to) || !$zip->addFile($temp, $_name)) + { + unlink($temp); + trigger_error("Could not add $addfile to ZIP file", E_USER_ERROR); + continue; + } + // Keep temp file until _after_ zipping is done + $tempfiles[] = $temp; + + // Add comment in + $props = self::propfind($addfile); + if($props) + { + $comment = self::find_prop($props,'comment'); + if($comment) + { + $zip->setCommentName($_name, $comment); + } + } + unset($props); + } + } + + // Set a comment to help tell them apart + $zip->setArchiveComment(lang('Created by %1', $GLOBALS['egw_info']['user']['account_lid']) . ' ' .egw_time::to()); + + // Record total for debug, not available after close() + $total_files = $zip->numFiles; + + $result = $zip->close(); + if(!$result || !filesize($zip_file)) + { + error_log('close() result: '.array2string($result)); + return 'Error creating zip file'; + } + + error_log("Total files: " . $total_files . " Peak memory to zip: " . self::hsize(memory_get_peak_usage(true))); + + // Stop any buffering + while(ob_get_level() > 0) + { + ob_end_clean(); + } + + // Stream the file to the client + header("Content-Type: application/zip"); + header("Content-Length: " . filesize($zip_file)); + header("Content-Disposition: attachment; filename=\"$filename\""); + readfile($zip_file); + + unlink($zip_file); + foreach($tempfiles as $temp_file) + { + unlink($temp_file); + } + + // Make sure to exit after, if you don't want to add to the ZIP + } + + /** + * We cache locks within a request, as HTTP_WebDAV_Server generates so many, that it can be a bottleneck + * + * @var array + */ + static protected $lock_cache; + + /** + * Log (to error log) all calls to lock(), unlock() or checkLock() + * + */ + const LOCK_DEBUG = false; + + /** + * lock a ressource/path + * + * @param string $path path or url + * @param string &$token + * @param int &$timeout + * @param string &$owner + * @param string &$scope + * @param string &$type + * @param boolean $update =false + * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes + * @return boolean true on success + */ + static function lock($path,&$token,&$timeout,&$owner,&$scope,&$type,$update=false,$check_writable=true) + { + // we require write rights to lock/unlock a resource + if (!$path || $update && !$token || $check_writable && + !(self::is_writable($path) || !self::file_exists($path) && self::is_writable(self::dirname($path)))) + { + return false; + } + // remove the lock info evtl. set in the cache + unset(self::$lock_cache[$path]); + + if ($timeout < 1000000) // < 1000000 is a relative timestamp, so we add the current time + { + $timeout += time(); + } + + if ($update) // Lock Update + { + if (($ret = (boolean)($row = self::$db->select(self::LOCK_TABLE,array('lock_owner','lock_exclusive','lock_write'),array( + 'lock_path' => $path, + 'lock_token' => $token, + ),__LINE__,__FILE__)->fetch()))) + { + $owner = $row['lock_owner']; + $scope = egw_db::from_bool($row['lock_exclusive']) ? 'exclusive' : 'shared'; + $type = egw_db::from_bool($row['lock_write']) ? 'write' : 'read'; + + self::$db->update(self::LOCK_TABLE,array( + 'lock_expires' => $timeout, + 'lock_modified' => time(), + ),array( + 'lock_path' => $path, + 'lock_token' => $token, + ),__LINE__,__FILE__); + } + } + // HTTP_WebDAV_Server does this check before calling LOCK, but we want to be complete and usable outside WebDAV + elseif(($lock = self::checkLock($path)) && ($lock['scope'] == 'exclusive' || $scope == 'exclusive')) + { + $ret = false; // there's alread a lock + } + else + { + // HTTP_WebDAV_Server sets owner and token, but we want to be complete and usable outside WebDAV + if (!$owner || $owner == 'unknown') + { + $owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; + } + if (!$token) + { + if (strpos(ini_get('include_path'), EGW_API_INC) === false) + { + ini_set('include_path', EGW_API_INC.PATH_SEPARATOR.ini_get('include_path')); + } + require_once('HTTP/WebDAV/Server.php'); + $token = HTTP_WebDAV_Server::_new_locktoken(); + } + try { + self::$db->insert(self::LOCK_TABLE,array( + 'lock_token' => $token, + 'lock_path' => $path, + 'lock_created' => time(), + 'lock_modified' => time(), + 'lock_owner' => $owner, + 'lock_expires' => $timeout, + 'lock_exclusive' => $scope == 'exclusive', + 'lock_write' => $type == 'write', + ),false,__LINE__,__FILE__); + $ret = true; + } + catch(egw_exception_db $e) { + unset($e); + $ret = false; // there's already a lock + } + } + if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$timeout,$owner,$scope,$type,update=$update,check_writable=$check_writable) returns ".($ret ? 'true' : 'false')); + return $ret; + } + + /** + * unlock a ressource/path + * + * @param string $path path to unlock + * @param string $token locktoken + * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes + * @return boolean true on success + */ + static function unlock($path,$token,$check_writable=true) + { + // we require write rights to lock/unlock a resource + if ($check_writable && !self::is_writable($path)) + { + return false; + } + if (($ret = self::$db->delete(self::LOCK_TABLE,array( + 'lock_path' => $path, + 'lock_token' => $token, + ),__LINE__,__FILE__) && self::$db->affected_rows())) + { + // remove the lock from the cache too + unset(self::$lock_cache[$path]); + } + if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$check_writable) returns ".($ret ? 'true' : 'false')); + return $ret; + } + + /** + * checkLock() helper + * + * @param string resource path to check for locks + * @return array|boolean false if there's no lock, else array with lock info + */ + static function checkLock($path) + { + if (isset(self::$lock_cache[$path])) + { + if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$path],true))); + return self::$lock_cache[$path]; + } + $where = 'lock_path='.self::$db->quote($path); + // ToDo: additional check parent dirs for locks and children of the requested directory + //$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%'); + // ToDo: shared locks can return multiple rows + if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch())) + { + $result = egw_db::strip_array_keys($result,'lock_'); + $result['type'] = egw_db::from_bool($result['write']) ? 'write' : 'read'; + $result['scope'] = egw_db::from_bool($result['exclusive']) ? 'exclusive' : 'shared'; + $result['depth'] = egw_db::from_bool($result['recursive']) ? 'infinite' : 0; + } + if ($result && $result['expires'] < time()) // lock is expired --> remove it + { + self::$db->delete(self::LOCK_TABLE,array( + 'lock_path' => $result['path'], + 'lock_token' => $result['token'], + ),__LINE__,__FILE__); + + if (self::LOCK_DEBUG) error_log(__METHOD__."($path) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed"); + $result = false; + } + if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns ".($result?array2string($result):'false')); + return self::$lock_cache[$path] = $result; + } + + /** + * Get backend specific information (data and etemplate), to integrate as tab in filemanagers settings dialog + * + * @param string $path + * @param array $content =null + * @return array|boolean array with values for keys 'data','etemplate','name','label','help' or false if not supported by backend + */ + static function getExtraInfo($path,array $content=null) + { + $extra = array(); + if (($extra_info = self::_call_on_backend('extra_info',array($path,$content),true))) // true = fail silent if backend does NOT support it + { + $extra[] = $extra_info; + } + + if (($vfs_extra = $GLOBALS['egw']->hooks->process(array( + 'location' => 'vfs_extra', + 'path' => $path, + 'content' => $content, + )))) + { + foreach($vfs_extra as $data) + { + $extra = $extra ? array_merge($extra, $data) : $data; + } + } + return $extra; + } + + /** + * Mapps entries of applications to a path for the locking + * + * @param string $app + * @param int|string $id + * @return string + */ + static function app_entry_lock_path($app,$id) + { + return "/apps/$app/entry/$id"; + } + + /** + * Encoding of various special characters, which can NOT be unencoded in file-names, as they have special meanings in URL's + * + * @var array + */ + static public $encode = array( + //'%' => '%25', // % should be encoded, but easily leads to double encoding, therefore better NOT encodig it + '#' => '%23', + '?' => '%3F', + '/' => '', // better remove it completly + ); + + /** + * Encode a path component: replacing certain chars with their urlencoded counterparts + * + * Not all chars get encoded, slashes '/' are silently removed! + * + * To reverse the encoding, eg. to display a filename to the user, you have to use self::decodePath() + * + * @param string|array $component + * @return string|array + */ + static public function encodePathComponent($component) + { + return str_replace(array_keys(self::$encode),array_values(self::$encode),$component); + } + + /** + * Encode a path: replacing certain chars with their urlencoded counterparts + * + * To reverse the encoding, eg. to display a filename to the user, you have to use self::decodePath() + * + * @param string $path + * @return string + */ + static public function encodePath($path) + { + 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 + */ + static function init_static() + { + // if special user/vfs_user given (eg. from sharing) use it instead default user/account_id + self::$user = (int)(isset($GLOBALS['egw_info']['user']['vfs_user']) ? + $GLOBALS['egw_info']['user']['vfs_user'] : $GLOBALS['egw_info']['user']['account_id']); + self::$is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']); + self::$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; + self::$lock_cache = array(); + } + + /** + * Returns the URL to the thumbnail of the given file. The thumbnail may simply + * be the mime-type icon, or - if activated - the preview with the given thsize. + * + * @param string $file name of the file + * @param int $thsize the size of the preview - false if the default should be used. + * @param string $mime if you already know the mime type of the file, you can supply + * it here. Otherwise supply "false". + */ + public static function thumbnail_url($file, $thsize = false, $mime = false) + { + // Retrive the mime-type of the file + if (!$mime) + { + $mime = self::mime_content_type($file); + } + + $image = ""; + + // Seperate the mime type into the primary and the secondary part + list($mime_main, $mime_sub) = explode('/', $mime); + + if ($mime_main == 'egw') + { + $image = common::image($mime_sub, 'navbar'); + } + else if ($file && $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' && + ($stat = self::stat($file)) && $stat['size'] < 1500000) + { + if (substr($file, 0, 6) == '/apps/') + { + $file = self::parse_url(self::resolve_url_symlinks($file), PHP_URL_PATH); + } + + //Assemble the thumbnail parameters + $thparams = array(); + $thparams['path'] = $file; + if ($thsize) + { + $thparams['thsize'] = $thsize; + } + $image = $GLOBALS['egw']->link('/etemplate/thumbnail.php', $thparams); + } + else + { + list($app, $name) = explode("/", self::mime_icon($mime), 2); + $image = common::image($app, $name); + } + + return $image; + } + + /** + * Get the configured start directory for the current user + * + * @return string + */ + static public function get_home_dir() + { + $start = '/home/'.$GLOBALS['egw_info']['user']['account_lid']; + + // check if user specified a valid startpath in his prefs --> use it + if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) && + $path[0] == '/' && self::is_dir($path) && self::check_access($path, self::READABLE)) + { + $start = $path; + } + return $start; + } + + /** + * Copies the files given in $src to $dst. + * + * @param array $src contains the source file + * @param string $dst is the destination directory + */ + static public function copy_files(array $src, $dst, &$errs, array &$copied) + { + if (self::is_dir($dst)) + { + foreach ($src as $file) + { + // Check whether the file has already been copied - prevents from + // recursion + if (!in_array($file, $copied)) + { + // Calculate the target filename + $target = self::concat($dst, self::basename($file)); + + if (self::is_dir($file)) + { + if ($file !== $target) + { + // Create the target directory + self::mkdir($target,null,STREAM_MKDIR_RECURSIVE); + + $copied[] = $file; + $copied[] = $target; // < newly created folder must not be copied again! + if (self::copy_files(self::find($file), $target, + $errs, $copied)) + { + continue; + } + } + + $errs++; + } + else + { + // Copy a single file - check whether the file should be + // copied onto itself. + // TODO: Check whether target file already exists and give + // return those files so that a dialog might be displayed + // on the client side which lets the user decide. + if ($target !== $file && self::copy($file, $target)) + { + $copied[] = $file; + } + else + { + $errs++; + } + } + } + } + } + + return $errs == 0; + } + + /** + * Moves the files given in src to dst + */ + static public function move_files(array $src, $dst, &$errs, array &$moved) + { + if (self::is_dir($dst)) + { + foreach($src as $file) + { + $target = self::concat($dst, self::basename($file)); + + if ($file != $target && self::rename($file, $target)) + { + $moved[] = $file; + } + else + { + ++$errs; + } + } + + return $errs == 0; + } + + return false; + } + + /** + * Copy an uploaded file into the vfs, optionally set some properties (eg. comment or other cf's) + * + * Treat copying incl. properties as atomar operation in respect of notifications (one notification about an added file). + * + * @param array|string $src path to uploaded file or etemplate file array (value for key 'tmp_name') + * @param string $target path or directory to copy uploaded file + * @param array|string $props =null array with properties (name => value pairs, eg. 'comment' => 'FooBar','#cfname' => 'something'), + * array as for proppatch (array of array with values for keys 'name', 'val' and optional 'ns') or string with comment + * @param boolean $check_is_uploaded_file =true should method perform an is_uploaded_file check, default yes + * @return boolean|array stat array on success, false on error + */ + static public function copy_uploaded($src,$target,$props=null,$check_is_uploaded_file=true) + { + $tmp_name = is_array($src) ? $src['tmp_name'] : $src; + + if (self::stat($target) && self::is_dir($target)) + { + $target = self::concat($target, self::encodePathComponent(is_array($src) ? $src['name'] : basename($tmp_name))); + } + if ($check_is_uploaded_file && !is_uploaded_file($tmp_name)) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !is_uploaded_file()"); + return false; + } + if (!(self::is_writable($target) || self::is_writable(self::dirname($target)))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !writable"); + return false; + } + if ($props) + { + if (!is_array($props)) $props = array(array('name' => 'comment','val' => $props)); + + // if $props is name => value pairs, convert it to internal array or array with values for keys 'name', 'val' and optional 'ns' + if (!isset($props[0])) + { + foreach($props as $name => $val) + { + if (($name == 'comment' || $name[0] == '#') && $val) // only copy 'comment' and cfs + { + $vfs_props[] = array( + 'name' => $name, + 'val' => $val, + ); + } + } + $props = $vfs_props; + } + } + if ($props) + { + // set props before copying the file, so notifications already contain them + if (!self::stat($target)) + { + self::touch($target); // create empty file, to be able to attach properties + self::$treat_as_new = true; // notify as new + } + self::proppatch($target, $props); + } + $ret = copy($tmp_name,self::PREFIX.$target) ? self::stat($target) : false; + if (self::LOG_LEVEL > 1 || !$ret && self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).") returning ".array2string($ret)); + return $ret; + } + + /** + * Compare two files from vfs or local file-system for identical content + * + * VFS files must use URL, to be able to distinguish them eg. from temp. files! + * + * @param string $file1 vfs-url or local path, eg. /tmp/some-file.txt or vfs://default/home/user/some-file.txt + * @param string $file2 -- " -- + * @return boolean true: if files are identical, false: if not or file not found + */ + public static function compare($file1, $file2) + { + if (filesize($file1) != filesize($file2) || + !($fp1 = fopen($file1, 'r')) || !($fp2 = fopen($file2, 'r'))) + { + //error_log(__METHOD__."($file1, $file2) returning FALSE (different size)"); + return false; + } + while (($read1 = fread($fp1, 8192)) !== false && + ($read2 = fread($fp2, 8192)) !== false && + $read1 === $read2 && !feof($fp1) && !feof($fp2)) + { + // just loop until we find a difference + } + + fclose($fp1); + fclose($fp2); + //error_log(__METHOD__."($file1, $file2) returning ".array2string($read1 === $read2)." (content differs)"); + return $read1 === $read2; + } +} + +Vfs::init_static(); diff --git a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php b/api/src/Vfs/Filesystem/StreamWrapper.php similarity index 86% rename from phpgwapi/inc/class.filesystem_stream_wrapper.inc.php rename to api/src/Vfs/Filesystem/StreamWrapper.php index 8be319a0e4..edd21f2708 100644 --- a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php +++ b/api/src/Vfs/Filesystem/StreamWrapper.php @@ -1,16 +1,20 @@ - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +namespace EGroupware\Api\Vfs\Filesystem; + +use EGroupware\Api\Vfs; + /** * eGroupWare API: VFS - stream wrapper to access the regular filesystem (setting a given user, group and mode) * @@ -33,12 +37,12 @@ * * (admin / secret is username / password of setup user, "root_" prefix differenciate from regular EGw-user!) * - * To correctly support characters with special meaning in url's (#?%), we urlencode them with egw_vfs::encodePathComponent + * To correctly support characters with special meaning in url's (#?%), we urlencode them with Vfs::encodePathComponent * and urldecode all path again, before passing them to php's filesystem functions. * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class filesystem_stream_wrapper implements iface_stream_wrapper +class StreamWrapper implements Vfs\StreamWrapperIface { /** * Scheme / protocol used for this stream-wrapper @@ -47,7 +51,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper /** * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' */ - const DIR_MIME_TYPE = egw_vfs::DIR_MIME_TYPE ; + const DIR_MIME_TYPE = Vfs::DIR_MIME_TYPE ; /** * mode-bits, which have to be set for files @@ -121,16 +125,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ function stream_open ( $url, $mode, $options, &$opened_path ) { + unset($opened_path); // not used, but required by interface + $this->opened_stream = $this->opened_stream_url = null; $read_only = str_replace('b','',$mode) == 'r'; // check access rights, based on the eGW mount perms if (!($stat = self::url_stat($url,0)) || $mode[0] == 'x') // file not found or file should NOT exist { - $dir = egw_vfs::dirname($url); + $dir = Vfs::dirname($url); if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) $mode[0] == 'x' || // or file should not exist, but does - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) // or we are not allowed to create it + !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) // or we are not allowed to create it { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); if (!($options & STREAM_URL_STAT_QUIET)) @@ -140,7 +146,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper return false; } } - elseif (!$read_only && !egw_vfs::check_access($url,egw_vfs::WRITABLE,$stat)) // we are not allowed to edit it + elseif (!$read_only && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // we are not allowed to edit it { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be edited!"); if (!($options & STREAM_URL_STAT_QUIET)) @@ -160,7 +166,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper } // open the "real" file - if (!($this->opened_stream = fopen($path=egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)),$mode,$options))) + if (!($this->opened_stream = fopen($path=Vfs::decodePath(Vfs::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; @@ -302,10 +308,10 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function unlink ( $url ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::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)) + if (!file_exists($path) || is_dir($path) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; // no permission or file does not exist @@ -327,17 +333,17 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function rename ( $url_from, $url_to ) { - $from = egw_vfs::parse_url($url_from); - $to = egw_vfs::parse_url($url_to); + $from = Vfs::parse_url($url_from); + $to = Vfs::parse_url($url_to); // check access rights - if (!($from_stat = self::url_stat($url_from,0)) || !egw_vfs::check_access(egw_vfs::dirname($url_from),egw_vfs::WRITABLE)) + if (!($from_stat = self::url_stat($url_from,0)) || !Vfs::check_access(Vfs::dirname($url_from),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $from[path] permission denied!"); return false; // no permission or file does not exist } - $to_dir = egw_vfs::dirname($url_to); - if (!egw_vfs::check_access($to_dir,egw_vfs::WRITABLE,$to_dir_stat = self::url_stat($to_dir,0))) + $to_dir = Vfs::dirname($url_to); + if (!Vfs::check_access($to_dir,Vfs::WRITABLE,$to_dir_stat = self::url_stat($to_dir,0))) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $to_dir permission denied!"); return false; // no permission or parent-dir does not exist @@ -357,12 +363,12 @@ class filesystem_stream_wrapper implements iface_stream_wrapper return false; // no permission or file does not exist } // if destination file already exists, delete it - if ($to_stat && !self::unlink($url_to,$operation)) + if ($to_stat && !self::unlink($url_to)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); return false; } - return rename(egw_vfs::decodePath($from['path']),egw_vfs::decodePath($to['path'])); + return rename(Vfs::decodePath($from['path']),Vfs::decodePath($to['path'])); } /** @@ -378,24 +384,26 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function mkdir ( $url, $mode, $options ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + unset($mode); // not used, but required by interface + + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE); // find the real parent (might be more then one level if $recursive!) do { $parent = dirname($parent ? $parent : $path); - $parent_url = egw_vfs::dirname($parent_url ? $parent_url : $url); + $parent_url = Vfs::dirname($parent_url ? $parent_url : $url); } while ($recursive && $parent != '/' && !file_exists($parent)); - //echo __METHOD__."($url,$mode,$options) path=$path, recursive=$recursive, parent=$parent, egw_vfs::check_access(parent_url=$parent_url,egw_vfs::WRITABLE)=".(int)egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)."\n"; + //echo __METHOD__."($url,$mode,$options) path=$path, recursive=$recursive, parent=$parent, Vfs::check_access(parent_url=$parent_url,Vfs::WRITABLE)=".(int)Vfs::check_access($parent_url,Vfs::WRITABLE)."\n"; // check access rights (in real filesystem AND by mount perms) - if (file_exists($path) || !file_exists($parent) || !is_writable($parent) || !egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)) + if (file_exists($path) || !file_exists($parent) || !is_writable($parent) || !Vfs::check_access($parent_url,Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; } - return mkdir($path,$mode=0700,$recursive); // setting mode 0700 allows (only) apache to write into the dir + return mkdir($path, 0700, $recursive); // setting mode 0700 allows (only) apache to write into the dir } /** @@ -410,11 +418,13 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function rmdir ( $url, $options ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + unset($options); // not used, but required by interface + + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) + if (!file_exists($path) || !is_writable($parent) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; @@ -425,18 +435,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper /** * This is not (yet) a stream-wrapper function, but it's necessary and can be used static * - * @param string $path - * @param int $time=null modification time (unix timestamp), default null = current time - * @param int $atime=null access time (unix timestamp), default null = current time, not implemented in the vfs! + * @param string $url + * @param int $time =null modification time (unix timestamp), default null = current time + * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs! * @return boolean true on success, false otherwise */ static function touch($url,$time=null,$atime=null) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) + if (!file_exists($path) || !is_writable($parent) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; @@ -450,11 +460,13 @@ class filesystem_stream_wrapper implements iface_stream_wrapper * Not supported, as it would require root rights! * * @param string $path - * @param string $mode mode string see egw_vfs::mode2int + * @param string $mode mode string see Vfs::mode2int * @return boolean true on success, false otherwise */ static function chmod($path,$mode) { + unset($path, $mode); // not used, but required by interface + return false; } @@ -469,6 +481,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function chown($path,$owner) { + unset($path, $owner); // not used, but required by interface + return false; } @@ -483,6 +497,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function chgrp($path,$group) { + unset($path, $group); // not used, but required by interface + return false; } @@ -499,7 +515,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $this->opened_dir = null; - $path = egw_vfs::decodePath(egw_vfs::parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); // ToDo: check access rights @@ -539,18 +555,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function url_stat ( $url, $flags ) { - $parts = egw_vfs::parse_url($url); - $path = egw_vfs::decodePath($parts['path']); + $parts = Vfs::parse_url($url); + $path = Vfs::decodePath($parts['path']); $stat = @stat($path); // suppressed the stat failed warnings if ($stat) { // set owner, group and mode from mount options + $uid = $gid = $mode = null; if (!self::parse_query($parts['query'],$uid,$gid,$mode)) { return false; - if (self::LOG_LEVEL > 0) error_log(__METHOD__."($url,$flags) can NOT self::parse_query('$parts[query]')!"); } $stat['uid'] = $stat[4] = $uid; $stat['gid'] = $stat[5] = $gid; @@ -561,7 +577,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $stat['mode'] = $stat[2] = $stat['mode'] & ~0222; } } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags) path=$path, mount_mode=".sprintf('0%o',$mode).", mode=".sprintf('0%o',$stat['mode']).'='.egw_vfs::int2mode($stat['mode'])); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags) path=$path, mount_mode=".sprintf('0%o',$mode).", mode=".sprintf('0%o',$stat['mode']).'='.Vfs::int2mode($stat['mode'])); return $stat; } @@ -587,7 +603,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper while ($ignore); // encode special chars messing up url's - if ($file !== false) $file = egw_vfs::encodePathComponent($file); + if ($file !== false) $file = Vfs::encodePathComponent($file); if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() returning '.array2string($file)); @@ -634,6 +650,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function parse_query($query,&$uid,&$gid,&$mode) { + $params = null; parse_str(is_array($query) ? $query['query'] : $query,$params); // setting the default perms root.root r-x for other @@ -690,7 +707,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $gid = (int)$value; break; case 'mode': - $mode = egw_vfs::mode2int($value); + $mode = Vfs::mode2int($value); break; case 'url': // ignored, only used for download_url method @@ -712,7 +729,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function deny_script($url) { - $parts = egw_vfs::parse_url($url); + $parts = Vfs::parse_url($url); + $get = null; parse_str($parts['query'],$get); $deny = !$get['exec'] && preg_match(self::SCRIPT_EXTENSIONS_PREG,$parts['path']); @@ -730,24 +748,26 @@ class filesystem_stream_wrapper implements iface_stream_wrapper * We use our webdav handler as download url instead of an own download method. * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. * - * @param string $url - * @param boolean $force_download=false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! + * @param string $_url + * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! * @todo get $force_download working through webdav * @return string|false string with full download url or false to use default webdav.php url */ - static function download_url($url,$force_download=false) + static function download_url($_url,$force_download=false) { - list(,$query) = explode('?',$url,2); + unset($force_download); // not used, but required by interface + + list($url,$query) = explode('?',$_url,2); + $get = null; 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 + if (!($mount_url = 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); + $download_url = Vfs::concat($get['url'],$relpath); if ($download_url[0] == '/') { $download_url = ($_SERVER['HTTPS'] ? 'https://' : 'http://'). @@ -757,6 +777,14 @@ class filesystem_stream_wrapper implements iface_stream_wrapper //die(__METHOD__."('$url') --> relpath = $relpath --> $download_url"); return $download_url; } + + /** + * Register our stream-wrapper + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } } -stream_register_wrapper(filesystem_stream_wrapper::SCHEME ,'filesystem_stream_wrapper'); +StreamWrapper::register(); diff --git a/phpgwapi/inc/class.links_stream_wrapper.inc.php b/api/src/Vfs/Links/StreamWrapper.php similarity index 80% rename from phpgwapi/inc/class.links_stream_wrapper.inc.php rename to api/src/Vfs/Links/StreamWrapper.php index 5ebeb33dc7..bf72f10331 100644 --- a/phpgwapi/inc/class.links_stream_wrapper.inc.php +++ b/api/src/Vfs/Links/StreamWrapper.php @@ -7,18 +7,26 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $ */ +namespace EGroupware\Api\Vfs\Links; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use egw_link; +use addressbook_vcal; + /** - * Define parent for links_stream_wrapper, if not already defined + * Define parent for Vfs\Links\StreamWrapper, if not already defined * - * Allows to base links_stream_wrapper on an other wrapper + * Allows to base Vfs\Links\StreamWrapper on an other wrapper */ -if (!class_exists('links_stream_wrapper_parent',false)) +if (!class_exists('EGroupware\\Api\\Vfs\\Links\\LinksParent', false)) { - class links_stream_wrapper_parent extends sqlfs_stream_wrapper {} + class LinksParent extends Vfs\Sqlfs\StreamWrapper {} } /** @@ -42,7 +50,7 @@ if (!class_exists('links_stream_wrapper_parent',false)) * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class links_stream_wrapper extends links_stream_wrapper_parent +class StreamWrapper extends LinksParent { /** * Scheme / protocoll used for this stream-wrapper @@ -70,11 +78,11 @@ class links_stream_wrapper extends links_stream_wrapper_parent */ static function check_extended_acl($url,$check) { - if (egw_vfs::$is_root) + if (Vfs::$is_root) { return true; } - $path = egw_vfs::parse_url($url,PHP_URL_PATH); + $path = Vfs::parse_url($url,PHP_URL_PATH); list(,$apps,$app,$id,$rel_path) = explode('/',$path,5); @@ -85,7 +93,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent } elseif (!$app) { - $access = !($check & egw_vfs::WRITABLE); // always grant read access to /apps + $access = !($check & Vfs::WRITABLE); // always grant read access to /apps $what = '!$app'; } elseif(!isset($GLOBALS['egw_info']['user']['apps'][$app])) @@ -104,11 +112,11 @@ class links_stream_wrapper extends links_stream_wrapper_parent else { // vfs & stream-wrapper use posix rights, egw_link::file_access uses EGW_ACL_{EDIT|READ}! - $required = $check & egw_vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; - $access = egw_link::file_access($app,$id,$required,$rel_path,egw_vfs::$user); - $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".egw_vfs::$user.")"; + $required = $check & Vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; + $access = egw_link::file_access($app,$id,$required,$rel_path,Vfs::$user); + $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".Vfs::$user.")"; } - if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".egw_vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); + if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".Vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); return $access; } @@ -118,7 +126,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent * Reimplemented from sqlfs, as we have to pass the value of check_extends_acl(), due to the lack of late static binding. * And to return vcard for url /apps/addressbook/$id/.entry * - * @param string $path + * @param string $url * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, * or a filesystem symlink). This flag specified that only information about the link itself should be returned, @@ -131,7 +139,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent */ static function url_stat ( $url, $flags ) { - $eacl_check=self::check_extended_acl($url,egw_vfs::READABLE); + $eacl_check=self::check_extended_acl($url,Vfs::READABLE); // return vCard as /.entry if ( $eacl_check && substr($url,-7) == '/.entry' && @@ -140,7 +148,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent $ret = array( 'ino' => md5($url), 'name' => '.entry', - 'mode' => self::MODE_FILE|egw_vfs::READABLE, // required by the stream wrapper + 'mode' => self::MODE_FILE|Vfs::READABLE, // required by the stream wrapper 'size' => 1024, // fmail does NOT attach files with size 0! 'uid' => 0, 'gid' => 0, @@ -154,7 +162,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent // if entry directory does not exist --> return fake directory elseif (!($ret = parent::url_stat($url,$flags,$eacl_check)) && $eacl_check) { - list(,/*$apps*/,/*$app*/,$id,$rel_path) = explode('/', egw_vfs::parse_url($url, PHP_URL_PATH), 5); + list(,/*$apps*/,/*$app*/,$id,$rel_path) = explode('/', Vfs::parse_url($url, PHP_URL_PATH), 5); if ($id && !isset($rel_path)) { $ret = array( @@ -168,7 +176,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent 'ctime' => time(), 'nlink' => 2, // eGW addition to return some extra values - 'mime' => egw_vfs::DIR_MIME_TYPE, + 'mime' => Vfs::DIR_MIME_TYPE, ); } } @@ -182,9 +190,9 @@ class links_stream_wrapper extends links_stream_wrapper_parent * Reimplemented, to NOT call the sqlfs functions, as we dont allow to modify the ACL (defined by the apps) * * @param string $path string with path - * @param int $rights=null rights to set, or null to delete the entry - * @param int/boolean $owner=null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param int $fs_id=null fs_id to use, to not query it again (eg. because it's already deleted) + * @param int $rights =null rights to set, or null to delete the entry + * @param int/boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path + * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted) * @return boolean true if acl is set/deleted, false on error */ static function eacl($path,$rights=null,$owner=null,$fs_id=null) @@ -230,22 +238,22 @@ class links_stream_wrapper extends links_stream_wrapper_parent if($path[0] != '/') { - if (strpos($path,'?') !== false) $query = egw_vfs::parse_url($path,PHP_URL_QUERY); - $path = egw_vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); + if (strpos($path,'?') !== false) $query = Vfs::parse_url($path,PHP_URL_QUERY); + $path = Vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); } list(,$apps,$app,$id) = explode('/',$path); $ret = false; - if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,egw_vfs::WRITABLE)) // app directory itself is allways ok + if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok { - $current_is_root = egw_vfs::$is_root; egw_vfs::$is_root = true; - $current_user = egw_vfs::$user; egw_vfs::$user = 0; + $current_is_root = Vfs::$is_root; Vfs::$is_root = true; + $current_user = Vfs::$user; Vfs::$user = 0; $ret = parent::mkdir($path,0,$options|STREAM_MKDIR_RECURSIVE); if ($id) parent::chmod($path,0); // no other rights - egw_vfs::$user = $current_user; - egw_vfs::$is_root = $current_is_root; + Vfs::$user = $current_user; + Vfs::$is_root = $current_is_root; } //error_log(__METHOD__."($path,$mode,$options) apps=$apps, app=$app, id=$id: returning $ret"); return $ret; @@ -277,21 +285,20 @@ class links_stream_wrapper extends links_stream_wrapper_parent (list($app) = array_slice(explode('/',$url),-3,1)) && $app === 'addressbook') { list($id) = array_slice(explode('/',$url),-2,1); - $name = md5($url); $ab_vcard = new addressbook_vcal('addressbook','text/vcard'); - if (!($GLOBALS[$name] =& $ab_vcard->getVCard($id))) + if (!($vcard =& $ab_vcard->getVCard($id))) { error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned false!"); return false; } //error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned ".$GLOBALS[$name]); - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $this->opened_stream = fopen('global://'.$name,'r'); - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed + $this->opened_stream = fopen('php://temp', 'wb'); + fwrite($this->opened_stream, $vcard); + fseek($this->opened_stream, 0, SEEK_SET); return true; } // create not existing entry directories on the fly - if ($mode[0] != 'r' && !parent::url_stat($dir = egw_vfs::dirname($url),0) && self::check_extended_acl($dir,egw_vfs::WRITABLE)) + if ($mode[0] != 'r' && !parent::url_stat($dir = Vfs::dirname($url),0) && self::check_extended_acl($dir,Vfs::WRITABLE)) { self::mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } @@ -303,7 +310,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent * * Reimplemented to give no error, if entry directory does not exist. * - * @param string $path URL that was passed to opendir() and that this object is expected to explore. + * @param string $url URL that was passed to opendir() and that this object is expected to explore. * @param $options * @return booelan */ @@ -316,6 +323,14 @@ class links_stream_wrapper extends links_stream_wrapper_parent } return parent::dir_opendir($url, $options); } + + /** + * Register this stream-wrapper + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } } -stream_register_wrapper(links_stream_wrapper::SCHEME ,'links_stream_wrapper'); +StreamWrapper::register(); diff --git a/api/src/Vfs/Sqlfs/StreamWrapper.php b/api/src/Vfs/Sqlfs/StreamWrapper.php new file mode 100644 index 0000000000..f9976d73d6 --- /dev/null +++ b/api/src/Vfs/Sqlfs/StreamWrapper.php @@ -0,0 +1,1939 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api\Vfs\Sqlfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use egw_cache; +use mime_magic; +use config; +use translation; +use egw_exception_db; +use egw_exception_wrong_parameter; +use egw_exception_assertion_failed; + + +/** + * EGroupware API: VFS - new DB based VFS stream wrapper + * + * The sqlfs stream wrapper has 2 operation modi: + * - content of files is stored in the filesystem (eGW's files_dir) (default) + * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db) + * please note the current (php5.2.6) problems: + * a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,\PDO::PARAM_LOB) does NOT work, string returned) + * (there's a workaround implemented, but it requires to allocate memory for the whole file!) + * b) uploading/writing files > 1M fail on PDOStatement::execute() (setting \PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help) + * (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation) + * + * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). + * + * The stream wrapper interface is according to the docu on php.net + * + * @link http://www.php.net/manual/en/function.stream-wrapper-register.php + */ +class StreamWrapper implements Vfs\StreamWrapperIface +{ + /** + * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' + */ + const DIR_MIME_TYPE = 'httpd/unix-directory'; + /** + * Mime type for symlinks + */ + const SYMLINK_MIME_TYPE = 'application/x-symlink'; + /** + * Scheme / protocoll used for this stream-wrapper + */ + const SCHEME = 'sqlfs'; + /** + * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) + */ + const STAT_RETURN_MIME_TYPE = 'mime'; + /** + * Our tablename + */ + const TABLE = 'egw_sqlfs'; + /** + * Name of our property table + */ + const PROPS_TABLE = 'egw_sqlfs_props'; + /** + * mode-bits, which have to be set for files + */ + const MODE_FILE = 0100000; + /** + * mode-bits, which have to be set for directories + */ + const MODE_DIR = 040000; + /** + * mode-bits, which have to be set for links + */ + const MODE_LINK = 0120000; + /** + * How much should be logged to the apache error-log + * + * 0 = Nothing + * 1 = only errors + * 2 = all function calls and errors (contains passwords too!) + * 3 = log line numbers in sql statements + */ + const LOG_LEVEL = 1; + + /** + * We store the content in the DB (no versioning) + */ + const STORE2DB = 1; + /** + * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) + */ + const STORE2FS = 2; + /** + * default for operation, change that if you want to test with STORE2DB atm + */ + const DEFAULT_OPERATION = self::STORE2FS; + + /** + * operation mode of the opened file + * + * @var int + */ + protected $operation = self::DEFAULT_OPERATION; + + /** + * optional context param when opening the stream, null if no context passed + * + * @var mixed + */ + var $context; + + /** + * Path off the file opened by stream_open + * + * @var string + */ + protected $opened_path; + /** + * Mode of the file opened by stream_open + * + * @var int + */ + protected $opened_mode; + /** + * Stream of the opened file, either from the DB via PDO or the filesystem + * + * @var resource + */ + protected $opened_stream; + /** + * fs_id of opened file + * + * @var int + */ + protected $opened_fs_id; + /** + * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls + * + * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat! + * + * @var array $path => info-array pairs + */ + static protected $stat_cache = array(); + /** + * Reference to the PDO object we use + * + * @var \PDO + */ + static protected $pdo; + /** + * Array with filenames of dir opened with dir_opendir + * + * @var array + */ + protected $opened_dir; + + /** + * Extra columns added since the intitial introduction of sqlfs + * + * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates + * + * @var string; + */ + static public $extra_columns = ',fs_link'; + + /** + * Clears our stat-cache + * + * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! + * + * @param string $path ='/' + */ + public static function clearstatcache($path='/') + { + //error_log(__METHOD__."('$path')"); + unset($path); // not used + + self::$stat_cache = array(); + + egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null); + } + + /** + * This method is called immediately after your stream object is created. + * + * @param string $url URL that was passed to fopen() and that this object is expected to retrieve + * @param string $mode mode used to open the file, as detailed for fopen() + * @param int $options additional flags set by the streams API (or'ed together): + * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. + * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. + * If this flag is not set, you should not raise any errors. + * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set + * @param array $overwrite_new =null if set create new file with values overwriten by the given ones + * @return boolean true if the ressource was opened successful, otherwise false + */ + function stream_open ( $url, $mode, $options, &$opened_path, array $overwrite_new=null ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + $this->operation = self::url2operation($url); + $dir = Vfs::dirname($url); + + $this->opened_path = $opened_path = $path; + $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system + $this->opened_stream = null; + + if (!is_null($overwrite_new) || !($stat = static::url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x') // file not found or file should NOT exist + { + if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) + $mode[0] == 'x' && $stat || // or file should not exist, but does + !($dir_stat=static::url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it + !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + // new file --> create it in the DB + $new_file = true; + $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)'; + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + $stmt = self::$pdo->prepare($query); + $values = array( + 'fs_name' => Vfs::basename($path), + 'fs_dir' => $dir_stat['ino'], + // we use the mode of the dir, so files in group dirs stay accessible by all members + 'fs_mode' => $dir_stat['mode'] & 0666, + // for the uid we use the uid of the dir if not 0=root or the current user otherwise + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + // we allways use the group of the dir + 'fs_gid' => $dir_stat['gid'], + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => Vfs::$user, + 'fs_mime' => 'application/octet-stream', // required NOT NULL! + 'fs_size' => 0, + 'fs_active' => self::_pdo_boolean(true), + ); + if ($overwrite_new) $values = array_merge($values,$overwrite_new); + if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'))) + { + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo()); + return false; + } + if ($this->operation == self::STORE2DB) + { + // we buffer all write operations in a temporary file, which get's written on close + $this->opened_stream = tmpfile(); + } + // create the hash-dirs, if they not yet exist + elseif(!file_exists($fs_dir=Vfs::dirname(self::_fs_path($this->opened_fs_id)))) + { + $umaskbefore = umask(); + if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); + self::mkdir_recursive($fs_dir,0700,true); + } + } + // check if opend file is a directory + elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + else + { + if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat) ||// we are not allowed to read + $mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // or edit it + { + self::_remove_password($url); + $op = $mode == 'r' ? 'read' : 'edited'; + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + $this->opened_fs_id = $stat['ino']; + + if ($this->operation == self::STORE2DB) + { + $stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?'); + $stmt->execute(array($stat['ino'])); + $stmt->bindColumn(1,$this->opened_stream,\PDO::PARAM_LOB); + $stmt->fetch(\PDO::FETCH_BOUND); + // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) + // PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( + if (is_string($this->opened_stream)) + { + $tmp = fopen('php://temp', 'wb'); + fwrite($tmp, $this->opened_stream); + fseek($tmp, 0, SEEK_SET); + unset($this->opened_stream); + $this->opened_stream = $tmp; + } + //echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream); + } + } + // do we operate directly on the filesystem --> open file from there + if ($this->operation == self::STORE2FS) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); + if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) + { + // delete db entry again, if we are not able to open a new(!) file + unset($stmt); + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + $stmt->execute(array('fs_id' => $this->opened_fs_id)); + } + } + if ($mode[0] == 'a') // append modes: a, a+ + { + $this->stream_seek(0,SEEK_END); + } + if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!"); + + return is_resource($this->opened_stream); + } + + /** + * This method is called when the stream is closed, using fclose(). + * + * You must release any resources that were locked or allocated by the stream. + */ + function stream_close ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id) + { + return false; + } + + if ($this->opened_mode != 'r') + { + $this->stream_seek(0,SEEK_END); + + // we need to update the mime-type, size and content (if STORE2DB) + $values = array( + 'fs_size' => $this->stream_tell(), + // todo: analyse the file for the mime-type + 'fs_mime' => mime_magic::filename2mime($this->opened_path), + 'fs_id' => $this->opened_fs_id, + 'fs_modifier' => Vfs::$user, + 'fs_modified' => self::_pdo_timestamp(time()), + ); + + if ($this->operation == self::STORE2FS) + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id'); + if (!($ret = $stmt->execute($values))) + { + error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); + } + } + else + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id'); + $this->stream_seek(0,SEEK_SET); // rewind to the start + foreach($values as $name => &$value) + { + $stmt->bindParam($name,$value); + } + $stmt->bindParam('fs_content', $this->opened_stream, \PDO::PARAM_LOB); + if (!($ret = $stmt->execute())) + { + error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); + } + } + } + else + { + $ret = true; + } + $ret = fclose($this->opened_stream) && $ret; + + unset(self::$stat_cache[$this->opened_path]); + $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opend_fs_id = null; + $this->operation = self::DEFAULT_OPERATION; + + return $ret; + } + + /** + * This method is called in response to fread() and fgets() calls on the stream. + * + * You must return up-to count bytes of data from the current read/write position as a string. + * If there are less than count bytes available, return as many as are available. + * If no more data is available, return either FALSE or an empty string. + * You must also update the read/write position of the stream by the number of bytes that were successfully read. + * + * @param int $count + * @return string/false up to count bytes read or false on EOF + */ + function stream_read ( $count ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); + + if (is_resource($this->opened_stream)) + { + return fread($this->opened_stream,$count); + } + return false; + } + + /** + * This method is called in response to fwrite() calls on the stream. + * + * You should store data into the underlying storage used by your stream. + * If there is not enough room, try to store as many bytes as possible. + * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. + * You must also update the read/write position of the stream by the number of bytes that were successfully written. + * + * @param string $data + * @return integer + */ + function stream_write ( $data ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); + + if (is_resource($this->opened_stream)) + { + return fwrite($this->opened_stream,$data); + } + return false; + } + + /** + * This method is called in response to feof() calls on the stream. + * + * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! + * + * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) + * { + * $eof = !$eof; + * } + * + * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise + */ + function stream_eof ( ) + { + if (is_resource($this->opened_stream)) + { + return feof($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to ftell() calls on the stream. + * + * @return integer current read/write position of the stream + */ + function stream_tell ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return ftell($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fseek() calls on the stream. + * + * You should update the read/write position of the stream according to offset and whence. + * See fseek() for more information about these parameters. + * + * @param integer $offset + * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes + * SEEK_CUR - 1 - Set position to current location plus offset. + * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) + * @return boolean TRUE if the position was updated, FALSE otherwise. + */ + function stream_seek ( $offset, $whence ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); + + if (is_resource($this->opened_stream)) + { + return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure + } + return false; + } + + /** + * This method is called in response to fflush() calls on the stream. + * + * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. + * + * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. + */ + function stream_flush ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return fflush($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fstat() calls on the stream. + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @return array containing the same values as appropriate for the stream. + */ + function stream_stat ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); + + return $this->url_stat($this->opened_path,0); + } + + /** + * This method is called in response to unlink() calls on URL paths associated with the wrapper. + * + * It should attempt to delete the item specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! + * + * @param string $url + * @return boolean TRUE on success or FALSE on failure + */ + static function unlink ( $url, $parent_stat=null ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,STREAM_URL_STAT_LINK)) || !Vfs::check_access(Vfs::dirname($path),Vfs::WRITABLE, $parent_stat)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); + return false; // no permission or file does not exist + } + if ($stat['mime'] == self::DIR_MIME_TYPE) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!"); + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + unset(self::$stat_cache[$path]); + + if (($ret = $stmt->execute(array('fs_id' => $stat['ino'])))) + { + if (self::url2operation($url) == self::STORE2FS && + ($stat['mode'] & self::MODE_LINK) != self::MODE_LINK) + { + unlink(self::_fs_path($stat['ino'])); + } + // delete props + unset($stmt); + $stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); + $stmt->execute(array($stat['ino'])); + } + return $ret; + } + + /** + * This method is called in response to rename() calls on URL paths associated with the wrapper. + * + * It should attempt to rename the item specified by path_from to the specification given by path_to. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. + * + * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! + * + * @param string $url_from + * @param string $url_to + * @return boolean TRUE on success or FALSE on failure + */ + static function rename ( $url_from, $url_to) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); + + $path_from = Vfs::parse_url($url_from,PHP_URL_PATH); + $from_dir = Vfs::dirname($path_from); + $path_to = Vfs::parse_url($url_to,PHP_URL_PATH); + $to_dir = Vfs::dirname($path_to); + $operation = self::url2operation($url_from); + + // we have to use array($class,'url_stat'), as $class.'::url_stat' requires PHP 5.2.3 and we currently only require 5.2+ + if (!($from_stat = static::url_stat($path_from, 0)) || + !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = static::url_stat($from_dir, 0))) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); + return false; // no permission or file does not exist + } + if (!Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = static::url_stat($to_dir, 0))) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); + return false; // no permission or parent-dir does not exist + } + // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems + // for our vfs too, we abort here with an error, like the filesystem one does + if (($to_stat = static::url_stat($path_to, 0)) && + ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; + if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); + return false; // no permission or file does not exist + } + // if destination file already exists, delete it + if ($to_stat && !static::unlink($url_to,$operation)) + { + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); + return false; + } + unset(self::$stat_cache[$path_from]); + unset(self::$stat_cache[$path_to]); + + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name WHERE fs_dir=:old_dir AND fs_name=:old_name'); + $ok = $stmt->execute(array( + 'fs_dir' => $to_dir_stat['ino'], + 'fs_name' => Vfs::basename($path_to), + 'old_dir' => $from_dir_stat['ino'], + 'old_name' => $from_stat['name'], + )); + unset($stmt); + + // check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!) + // fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc + if ($ok && ($new_mime = Vfs::mime_content_type($url_to,true)) != Vfs::mime_content_type($url_to)) + { + //echo "

Vfs::nime_content_type($url_to,true) = $new_mime

\n"; + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id'); + $stmt->execute(array( + 'fs_mime' => $new_mime, + 'fs_id' => $from_stat['ino'], + )); + unset(self::$stat_cache[$path_to]); + } + return $ok; + } + + /** + * due to problems with recursive directory creation, we have our own here + */ + private static function mkdir_recursive($pathname, $mode, $depth=0) + { + $maxdepth=10; + $depth2propagate = (int)$depth + 1; + if ($depth2propagate > $maxdepth) return is_dir($pathname); + is_dir(Vfs::dirname($pathname)) || self::mkdir_recursive(Vfs::dirname($pathname), $mode, $depth2propagate); + return is_dir($pathname) || @mkdir($pathname, $mode); + } + + /** + * This method is called in response to mkdir() calls on URL paths associated with the wrapper. + * + * It should attempt to create the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. + * + * @param string $url + * @param int $mode + * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE + * @return boolean TRUE on success or FALSE on failure + */ + static function mkdir ( $url, $mode, $options ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); + if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (self::url_stat($path,STREAM_URL_STAT_QUIET)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); + if (!($options & STREAM_REPORT_ERRORS)) + { + //throw new Exception(__METHOD__."('$url',$mode,$options) already exist!"); + trigger_error(__METHOD__."('$url',$mode,$options) already exist!",E_USER_WARNING); + } + return false; + } + $parent_path = Vfs::dirname($path); + if (($query = Vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'.$query; + $parent = self::url_stat($parent_path,STREAM_URL_STAT_QUIET); + + // check if we should also create all non-existing path components and our parent does not exist, + // if yes call ourself recursive with the parent directory + if (($options & STREAM_MKDIR_RECURSIVE) && $parent_path != '/' && !$parent) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__." creating parents: $parent_path, $mode"); + if (!self::mkdir($parent_path,$mode,$options)) + { + return false; + } + $parent = self::url_stat($parent_path,0); + } + if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE,$parent)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); + if (!($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING); + } + return false; // no permission or file does not exist + } + unset(self::$stat_cache[$path]); + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); + if (($ok = $stmt->execute(array( + 'fs_name' => Vfs::basename($path), + 'fs_dir' => $parent['ino'], + 'fs_mode' => $parent['mode'], + 'fs_uid' => $parent['uid'], + 'fs_gid' => $parent['gid'], + 'fs_size' => 0, + 'fs_mime' => self::DIR_MIME_TYPE, + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => Vfs::$user, + )))) + { + // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) + $new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'); + + unset($stmt); // free statement object, on some installs a new prepare fails otherwise! + + $stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE. + ' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name'); + if ($stmt->execute(array( + 'fs_dir' => $parent['ino'], + 'fs_active' => self::_pdo_boolean(true), + 'fs_name' => Vfs::basename($path), + )) && $stmt->fetchColumn() > 1) // if there's more then one --> remove our new dir + { + self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id); + } + } + return $ok; + } + + /** + * This method is called in response to rmdir() calls on URL paths associated with the wrapper. + * + * It should attempt to remove the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. + * + * @param string $url + * @param int $options Possible values include STREAM_REPORT_ERRORS. + * @return boolean TRUE on success or FALSE on failure. + */ + static function rmdir ( $url, $options ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + $parent = Vfs::dirname($path); + + if (!($stat = self::url_stat($path,0)) || $stat['mime'] != self::DIR_MIME_TYPE || + !Vfs::check_access($parent,Vfs::WRITABLE)) + { + self::_remove_password($url); + $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : + ($stat['mime'] != self::DIR_MIME_TYPE ? 'not a directory!' : 'permission denied!')); + if (self::LOG_LEVEL) error_log($err_msg); + if (!($options & STREAM_REPORT_ERRORS)) + { + trigger_error($err_msg,E_USER_WARNING); + } + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?'); + $stmt->execute(array($stat['ino'])); + if ($stmt->fetchColumn()) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!"); + if (!($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING); + } + return false; + } + unset(self::$stat_cache[$path]); + unset($stmt); // free statement object, on some installs a new prepare fails otherwise! + + $del_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?'); + if (($ret = $del_stmt->execute(array($stat['ino'])))) + { + self::eacl($path,null,false,$stat['ino']); // remove all (=false) evtl. existing extended acl for that dir + // delete props + unset($del_stmt); + $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); + $del_stmt->execute(array($stat['ino'])); + } + return $ret; + } + + /** + * This is not (yet) a stream-wrapper function, but it's necessary and can be used static + * + * @param string $url + * @param int $time =null modification time (unix timestamp), default null = current time + * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs! + */ + static function touch($url,$time=null,$atime=null) + { + unset($atime); // not used + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $time)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,STREAM_URL_STAT_QUIET))) + { + // file does not exist --> create an empty one + if (!($f = fopen(self::SCHEME.'://default'.$path,'w')) || !fclose($f)) + { + return false; + } + if (is_null($time)) + { + return true; // new (empty) file created with current mod time + } + $stat = self::url_stat($path,0); + } + unset(self::$stat_cache[$path]); + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified,fs_modifier=:fs_modifier WHERE fs_id=:fs_id'); + + return $stmt->execute(array( + 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), + 'fs_modifier' => Vfs::$user, + 'fs_id' => $stat['ino'], + )); + } + + /** + * Chown command, not yet a stream-wrapper function, but necessary + * + * @param string $url + * @param int $owner + * @return boolean + */ + static function chown($url,$owner) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); + trigger_error("No such file or directory $url !",E_USER_WARNING); + return false; + } + if (!Vfs::$is_root) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only root can do that!"); + trigger_error("Only root can do that!",E_USER_WARNING); + return false; + } + if ($owner < 0 || $owner && !$GLOBALS['egw']->accounts->id2name($owner)) // not a user (0 == root) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) user id!"); + trigger_error(__METHOD__."($url,$owner) Unknown (numeric) user id!",E_USER_WARNING); + //throw new Exception(__METHOD__."($url,$owner) Unknown (numeric) user id!"); + return false; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_id=:fs_id'); + + // update stat-cache + if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); + self::$stat_cache[$path]['fs_uid'] = $owner; + + return $stmt->execute(array( + 'fs_uid' => (int) $owner, + 'fs_id' => $stat['ino'], + )); + } + + /** + * Chgrp command, not yet a stream-wrapper function, but necessary + * + * @param string $url + * @param int $owner + * @return boolean + */ + static function chgrp($url,$owner) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); + trigger_error("No such file or directory $url !",E_USER_WARNING); + return false; + } + if (!Vfs::has_owner_rights($path,$stat)) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only owner or root can do that!"); + trigger_error("Only owner or root can do that!",E_USER_WARNING); + return false; + } + if ($owner < 0) $owner = -$owner; // sqlfs uses a positiv group id's! + + if ($owner && !$GLOBALS['egw']->accounts->id2name(-$owner)) // not a group + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) group id!"); + trigger_error("Unknown (numeric) group id!",E_USER_WARNING); + return false; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_gid=:fs_gid WHERE fs_id=:fs_id'); + + // update stat-cache + if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); + self::$stat_cache[$path]['fs_gid'] = $owner; + + return $stmt->execute(array( + 'fs_gid' => $owner, + 'fs_id' => $stat['ino'], + )); + } + + /** + * Chmod command, not yet a stream-wrapper function, but necessary + * + * @param string $url + * @param int $mode + * @return boolean + */ + static function chmod($url,$mode) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $mode)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no such file or directory!"); + trigger_error("No such file or directory $url !",E_USER_WARNING); + return false; + } + if (!Vfs::has_owner_rights($path,$stat)) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) only owner or root can do that!"); + trigger_error("Only owner or root can do that!",E_USER_WARNING); + return false; + } + if (!is_numeric($mode)) // not a mode + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no (numeric) mode!"); + trigger_error("No (numeric) mode!",E_USER_WARNING); + return false; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); + + // update stat cache + if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); + self::$stat_cache[$path]['fs_mode'] = ((int) $mode) & 0777; + + return $stmt->execute(array( + 'fs_mode' => ((int) $mode) & 0777, // we dont store the file and dir bits, give int overflow! + 'fs_id' => $stat['ino'], + )); + } + + + /** + * This method is called immediately when your stream object is created for examining directory contents with opendir(). + * + * @param string $url URL that was passed to opendir() and that this object is expected to explore. + * @param int $options + * @return booelan + */ + function dir_opendir ( $url, $options ) + { + $this->opened_dir = null; + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($url,0)) || // dir not found + $stat['mime'] != self::DIR_MIME_TYPE || // no dir + !Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE,$stat)) // no access + { + self::_remove_password($url); + $msg = $stat['mime'] != self::DIR_MIME_TYPE ? "$url is no directory" : 'permission denied'; + if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!"); + $this->opened_dir = null; + return false; + } + $this->opened_dir = array(); + $query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. + ' FROM '.self::TABLE.' WHERE fs_dir=? AND fs_active='.self::_pdo_boolean(true). + " ORDER BY fs_mime='httpd/unix-directory' DESC, fs_name ASC"; + //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$options)".' */ '.$query; + + $stmt = self::$pdo->prepare($query); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + if ($stmt->execute(array($stat['ino']))) + { + foreach($stmt as $file) + { + $this->opened_dir[] = $file['fs_name']; + self::$stat_cache[Vfs::concat($path,$file['fs_name'])] = $file; + } + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options): ".implode(', ',$this->opened_dir)); + reset($this->opened_dir); + + return true; + } + + /** + * This method is called in response to stat() calls on the URL paths associated with the wrapper. + * + * It should return as many elements in common with the system function as possible. + * Unknown or unavailable values should be set to a rational value (usually 0). + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @param string $url + * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: + * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, + * or a filesystem symlink). This flag specified that only information about the link itself should be returned, + * not the resource pointed to by the link. + * This flag is set in response to calls to lstat(), is_link(), or filetype(). + * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, + * you are responsible for reporting errors using the trigger_error() function during stating of the path. + * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! + * @param boolean $eacl_access =null allows extending classes to pass the value of their check_extended_acl() method (no lsb!) + * @return array + */ + static function url_stat ( $url, $flags, $eacl_access=null ) + { + static $max_subquery_depth=null; + if (is_null($max_subquery_depth)) + { + $max_subquery_depth = $GLOBALS['egw_info']['server']['max_subquery_depth']; + if (!$max_subquery_depth) $max_subquery_depth = 7; // setting current default of 7, if nothing set + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags,$eacl_access)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise + if ($path != '/' && substr($path,-1) == '/') + { + $path = substr($path,0,-1); + } + if (empty($path)) + { + return false; // is invalid and gives sql error + } + // check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db + if (self::$stat_cache && isset(self::$stat_cache[$path]) && (is_null($eacl_access) || self::$stat_cache[$path] !== false)) + { + return self::$stat_cache[$path] ? self::_vfsinfo2stat(self::$stat_cache[$path]) : false; + } + + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. + ' FROM '.self::TABLE.' WHERE fs_active='.self::_pdo_boolean(true). + ' AND fs_name'.self::$case_sensitive_equal.'? AND fs_dir='; + $parts = explode('/',$path); + + // if we have extendes acl access to the url, we dont need and can NOT include the sql for the readable check + if (is_null($eacl_access)) + { + $eacl_access = self::check_extended_acl($path,Vfs::READABLE); // should be static::check_extended_acl, but no lsb! + } + + try { + foreach($parts as $n => $name) + { + if ($n == 0) + { + $query = (int) ($path != '/'); // / always has fs_id == 1, no need to query it ($path=='/' needs fs_dir=0!) + } + elseif ($n < count($parts)-1) + { + // MySQL 5.0 has a nesting limit for subqueries + // --> we replace the so far cumulated subqueries with their result + // no idea about the other DBMS, but this does NOT hurt ... + // --> depth limit of subqueries is now dynamicly decremented in catch + if ($n > 1 && !(($n-1) % $max_subquery_depth) && !($query = self::$pdo->query($query)->fetchColumn())) + { + if (self::LOG_LEVEL > 1) + { + self::_remove_password($url); + error_log(__METHOD__."('$url',$flags) file or directory not found!"); + } + // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) + return self::$stat_cache[$path] = false; + } + $query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_active='. + self::_pdo_boolean(true).' AND fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name); + + // if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights) + if (!Vfs::$is_root && !$eacl_access) + { + if (!Vfs::$user) + { + self::_remove_password($url); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!"); + return false; + } + $query .= ' AND '.self::_sql_readable(); + } + } + else + { + $query = str_replace('fs_name'.self::$case_sensitive_equal.'?','fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name),$base_query).'('.$query.')'; + } + } + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$flags,$eacl_access)".' */ '.$query; + //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + + if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(\PDO::FETCH_ASSOC))) + { + if (self::LOG_LEVEL > 1) + { + self::_remove_password($url); + error_log(__METHOD__."('$url',$flags) file or directory not found!"); + } + // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) + return self::$stat_cache[$path] = false; + } + } + catch (\PDOException $e) { + // decrement subquery limit by 1 and try again, if not already smaller then 3 + if ($max_subquery_depth < 3) + { + throw new egw_exception_db($e->getMessage()); + } + $GLOBALS['egw_info']['server']['max_subquery_depth'] = --$max_subquery_depth; + error_log(__METHOD__."() decremented max_subquery_depth to $max_subquery_depth"); + config::save_value('max_subquery_depth', $max_subquery_depth, 'phpgwapi'); + if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache(); + return self::url_stat($url, $flags, $eacl_access); + } + self::$stat_cache[$path] = $info; + + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info)); + return self::_vfsinfo2stat($info); + } + + /** + * Return readable check as sql (to be AND'ed into the query), only use if !Vfs::$is_root + * + * @return string + */ + protected function _sql_readable() + { + static $sql_read_acl=null; + + if (is_null($sql_read_acl)) + { + foreach($GLOBALS['egw']->accounts->memberships(Vfs::$user,true) as $gid) + { + $memberships[] = abs($gid); // sqlfs stores the gid's positiv + } + // using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400) + // 256 = 0400, 32 = 040 + $sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)Vfs::$user. + ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; + //error_log(__METHOD__."() Vfs::\$user=".array2string(Vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); + } + return $sql_read_acl; + } + + /** + * This method is called in response to readdir(). + * + * It should return a string representing the next filename in the location opened by dir_opendir(). + * + * @return string + */ + function dir_readdir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + $file = current($this->opened_dir); next($this->opened_dir); + + return $file; + } + + /** + * This method is called in response to rewinddir(). + * + * It should reset the output generated by dir_readdir(). i.e.: + * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). + * + * @return boolean + */ + function dir_rewinddir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + reset($this->opened_dir); + + return true; + } + + /** + * This method is called in response to closedir(). + * + * You should release any resources which were locked or allocated during the opening and use of the directory stream. + * + * @return boolean + */ + function dir_closedir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + $this->opened_dir = null; + + return true; + } + + /** + * This method is called in response to readlink(). + * + * The readlink value is read by url_stat or dir_opendir and therefore cached in the stat-cache. + * + * @param string $path + * @return string|boolean content of the symlink or false if $url is no symlink (or not found) + */ + static function readlink($path) + { + $link = !($lstat = self::url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; + + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); + + return $link; + } + + /** + * Method called for symlink() + * + * @param string $target + * @param string $link + * @return boolean true on success false on error + */ + static function symlink($target,$link) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); + + if (self::url_stat($link,0) || !($dir = Vfs::dirname($link)) || + !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!stat('$link') || !is_writable('$dir'))"); + return false; // $link already exists or parent dir does not + } + $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_link'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_link)'; + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + $stmt = self::$pdo->prepare($query); + unset(self::$stat_cache[Vfs::parse_url($link,PHP_URL_PATH)]); + + return !!$stmt->execute(array( + 'fs_name' => Vfs::basename($link), + 'fs_dir' => $dir_stat['ino'], + 'fs_mode' => ($dir_stat['mode'] & 0666), + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + 'fs_gid' => $dir_stat['gid'], + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => Vfs::$user, + 'fs_mime' => self::SYMLINK_MIME_TYPE, + 'fs_size' => bytes($target), + 'fs_link' => $target, + )); + } + + private static $extended_acl; + + /** + * Check if extendes ACL (stored in eGW's ACL table) grants access + * + * The extended ACL is inherited, so it's valid for all subdirs and the included files! + * The used algorithm break on the first match. It could be used, to disallow further access. + * + * @param string $url url to check + * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable + * @return boolean + */ + static function check_extended_acl($url,$check) + { + $url_path = Vfs::parse_url($url,PHP_URL_PATH); + + if (is_null(self::$extended_acl)) + { + self::_read_extended_acl(); + } + $access = false; + foreach(self::$extended_acl as $path => $rights) + { + if ($path == $url_path || substr($url_path,0,strlen($path)+1) == $path.'/') + { + $access = ($rights & $check) == $check; + break; + } + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$check) ".($access?"access granted by $path=$rights":'no access!!!')); + return $access; + } + + /** + * Read the extended acl via acl::get_grants('sqlfs') + * + */ + static protected function _read_extended_acl() + { + if ((self::$extended_acl = egw_cache::getSession(self::EACL_APPNAME, 'extended_acl'))) + { + return; // ext. ACL read from session. + } + self::$extended_acl = array(); + if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(Vfs::$user,self::EACL_APPNAME))) + { + $pathes = self::id2path(array_keys($rights)); + } + foreach($rights as $fs_id => $right) + { + $path = $pathes[$fs_id]; + if (isset($path)) + { + self::$extended_acl[$path] = (int)$right; + } + } + // sort by length descending, to allow more specific pathes to have precedence + uksort(self::$extended_acl, function($a,$b) { + return strlen($b)-strlen($a); + }); + egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() '.array2string(self::$extended_acl)); + } + + /** + * Appname used with the acl class to store the extended acl + */ + const EACL_APPNAME = 'sqlfs'; + + /** + * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) + * + * Only root, the owner of the path or an eGW admin (only if there's no owner but a group) are allowd to set eACL's! + * + * @param string $path string with path + * @param int $rights =null rights to set, or null to delete the entry + * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path + * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted) + * @return boolean true if acl is set/deleted, false on error + */ + static function eacl($path,$rights=null,$owner=null,$fs_id=null) + { + if ($path[0] != '/') + { + $path = Vfs::parse_url($path,PHP_URL_PATH); + } + if (is_null($fs_id)) + { + if (!($stat = self::url_stat($path,0))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) no such file or directory!"); + return false; // $path not found + } + if (!Vfs::has_owner_rights($path,$stat)) // not group dir and user is eGW admin + { + if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) permission denied!"); + return false; // permission denied + } + $fs_id = $stat['ino']; + } + if (is_null($owner)) + { + $owner = Vfs::$user; + } + if (is_null($rights) || $owner === false) + { + // delete eacl + if (is_null($owner) || $owner == Vfs::$user || + $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true))) + { + self::$extended_acl = null; // force new read of eACL, as there could be multiple eACL for that path + } + $ret = $GLOBALS['egw']->acl->delete_repository(self::EACL_APPNAME,$fs_id,(int)$owner); + } + else + { + if (isset(self::$extended_acl) && ($owner == Vfs::$user || + $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true)))) + { + // set rights for this class, if applicable + self::$extended_acl[$path] |= $rights; + } + $ret = $GLOBALS['egw']->acl->add_repository(self::EACL_APPNAME,$fs_id,$owner,$rights); + } + if ($ret) + { + egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$rights,$owner,$fs_id)=".(int)$ret); + return $ret; + } + + /** + * Get all ext. ACL set for a path + * + * Calls itself recursive, to get the parent directories + * + * @param string $path + * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found + */ + function get_eacl($path) + { + if (!($stat = static::url_stat($path, STREAM_URL_STAT_QUIET))) + { + error_log(__METHOD__.__LINE__.' '.array2string($path).' not found!'); + return false; // not found + } + $eacls = array(); + foreach($GLOBALS['egw']->acl->get_all_rights($stat['ino'],self::EACL_APPNAME) as $owner => $rights) + { + $eacls[] = array( + 'path' => $path, + 'owner' => $owner, + 'rights' => $rights, + 'ino' => $stat['ino'], + ); + } + if (($path = Vfs::dirname($path))) + { + $eacls = array_merge((array)self::get_eacl($path),$eacls); + } + // sort by length descending, to show precedence + usort($eacls, function($a, $b) { + return strlen($b['path']) - strlen($a['path']); + }); + //error_log(__METHOD__."('$_path') returning ".array2string($eacls)); + return $eacls; + } + + /** + * Return the path of given fs_id(s) + * + * Searches the stat_cache first and then the db. + * Calls itself recursive to to determine the path of the parent/directory + * + * @param int|array $fs_ids integer fs_id or array of them + * @return string|array path or array or pathes indexed by fs_id + */ + static function id2path($fs_ids) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')'); + $ids = (array)$fs_ids; + $pathes = array(); + // first check our stat-cache for the ids + foreach(self::$stat_cache as $path => $stat) + { + if (($key = array_search($stat['fs_id'],$ids)) !== false) + { + $pathes[$stat['fs_id']] = $path; + unset($ids[$key]); + if (!$ids) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes).' *from stat_cache*'); + return is_array($fs_ids) ? $pathes : array_shift($pathes); + } + } + } + // now search via the database + if (count($ids) > 1) array_map(function(&$v) { $v = (int)$v; },$ids); + $query = 'SELECT fs_id,fs_dir,fs_name FROM '.self::TABLE.' WHERE fs_id'. + (count($ids) == 1 ? '='.(int)$ids[0] : ' IN ('.implode(',',$ids).')'); + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $stmt = self::$pdo->prepare($query); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + if (!$stmt->execute()) + { + return false; // not found + } + $parents = array(); + foreach($stmt as $row) + { + if ($row['fs_dir'] > 1 && !in_array($row['fs_dir'],$parents)) + { + $parents[] = $row['fs_dir']; + } + $rows[$row['fs_id']] = $row; + } + unset($stmt); + + if ($parents && !($parents = self::id2path($parents))) + { + return false; // parent not found, should never happen ... + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__." trying foreach with:".print_r($rows,true)."#"); + foreach((array)$rows as $fs_id => $row) + { + $parent = $row['fs_dir'] > 1 ? $parents[$row['fs_dir']] : ''; + + $pathes[$fs_id] = $parent . '/' . $row['fs_name']; + } + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes)); + return is_array($fs_ids) ? $pathes : array_shift($pathes); + } + + /** + * Convert a sqlfs-file-info into a stat array + * + * @param array $info + * @return array + */ + static protected function _vfsinfo2stat($info) + { + $stat = array( + 'ino' => $info['fs_id'], + 'name' => $info['fs_name'], + 'mode' => $info['fs_mode'] | + ($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR : + ($info['fs_mime'] == self::SYMLINK_MIME_TYPE ? self::MODE_LINK : self::MODE_FILE)), // required by the stream wrapper + 'size' => $info['fs_size'], + 'uid' => $info['fs_uid'], + 'gid' => $info['fs_gid'], + 'mtime' => strtotime($info['fs_modified']), + 'ctime' => strtotime($info['fs_created']), + 'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1, + // eGW addition to return some extra values + 'mime' => $info['fs_mime'], + 'readlink' => $info['fs_link'], + ); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat)); + return $stat; + } + + public static $pdo_type; + /** + * Case sensitive comparison operator, for mysql we use ' COLLATE utf8_bin =' + * + * @var string + */ + public static $case_sensitive_equal = '='; + + /** + * Reconnect to database + */ + static public function reconnect() + { + self::$pdo = self::_pdo(); + } + + /** + * Create pdo object / connection, as long as pdo is not generally used in eGW + * + * @return \PDO + */ + static protected function _pdo() + { + $egw_db = isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; + + switch($egw_db->Type) + { + case 'mysqli': + case 'mysqlt': + case 'mysql': + self::$case_sensitive_equal = '= BINARY '; + self::$pdo_type = 'mysql'; + break; + default: + self::$pdo_type = $egw_db->Type; + break; + } + $dsn = self::$pdo_type.':dbname='.$egw_db->Database.($egw_db->Host ? ';host='.$egw_db->Host.($egw_db->Port ? ';port='.$egw_db->Port : '') : ''); + // check once if pdo extension and DB specific driver is loaded or can be loaded + static $pdo_available=null; + if (is_null($pdo_available)) + { + foreach(array('pdo','pdo_'.self::$pdo_type) as $ext) + { + check_load_extension($ext,true); // true = throw Exception + } + $pdo_available = true; + } + try { + self::$pdo = new \PDO($dsn,$egw_db->User,$egw_db->Password,array( + \PDO::ATTR_ERRMODE=>\PDO::ERRMODE_EXCEPTION, + )); + } + catch(Exception $e) + { + unset($e); + // Exception reveals password, so we ignore the exception and connect again without pw, to get the right exception without pw + self::$pdo = new \PDO($dsn,$egw_db->User,'$egw_db->Password'); + } + // set client charset of the connection + $charset = translation::charset(); + switch(self::$pdo_type) + { + case 'mysql': + if (isset($egw_db->Link_ID->charset2mysql[$charset])) $charset = $egw_db->Link_ID->charset2mysql[$charset]; + // fall throught + case 'pgsql': + $query = "SET NAMES '$charset'"; + break; + } + if ($query) + { + self::$pdo->exec($query); + } + return self::$pdo; + } + + /** + * Just a little abstration 'til I know how to organise stuff like that with PDO + * + * @param mixed $time + * @return string Y-m-d H:i:s + */ + static protected function _pdo_timestamp($time) + { + if (is_numeric($time)) + { + $time = date('Y-m-d H:i:s',$time); + } + return $time; + } + + /** + * Just a little abstration 'til I know how to organise stuff like that with PDO + * + * @param boolean $val + * @return string '1' or '0' for mysql, 'true' or 'false' for everyone else + */ + static protected function _pdo_boolean($val) + { + if (self::$pdo_type == 'mysql') + { + return $val ? '1' : '0'; + } + return $val ? 'true' : 'false'; + } + + /** + * Maximum value for a single hash element (should be 10^N): 10, 100 (default), 1000, ... + * + * DONT change this value, once you have files stored, they will no longer be found! + */ + const HASH_MAX = 100; + + /** + * Return the path of the stored content of a file if $this->operation == self::STORE2FS + * + * To limit the number of files stored in one directory, we create a hash from the fs_id: + * 1 --> /00/1 + * 34 --> /00/34 + * 123 --> /01/123 + * 4567 --> /45/4567 + * 99999 --> /09/99/99999 + * --> so one directory contains maximum 2 * HASH_MAY entries (HASH_MAX dirs + HASH_MAX files) + * @param int $id id of the file + * @return string + */ + static function _fs_path($id) + { + if (!is_numeric($id)) + { + throw new egw_exception_wrong_parameter(__METHOD__."(id=$id) id has to be an integer!"); + } + if (!isset($GLOBALS['egw_info']['server']['files_dir'])) + { + if (is_object($GLOBALS['egw_setup']->db)) // if we run under setup, query the db for the files dir + { + $GLOBALS['egw_info']['server']['files_dir'] = $GLOBALS['egw_setup']->db->select('egw_config','config_value',array( + 'config_name' => 'files_dir', + 'config_app' => 'phpgwapi', + ),__LINE__,__FILE__)->fetchColumn(); + } + } + if (!$GLOBALS['egw_info']['server']['files_dir']) + { + throw new egw_exception_assertion_failed("\$GLOBALS['egw_info']['server']['files_dir'] not set!"); + } + $hash = array(); + $n = $id; + while(($n = (int) ($n / self::HASH_MAX))) + { + $hash[] = sprintf('%02d',$n % self::HASH_MAX); + } + if (!$hash) $hash[] = '00'; // we need at least one directory, to not conflict with the dir-names + array_unshift($hash,$id); + + $path = '/sqlfs/'.implode('/',array_reverse($hash)); + //error_log(__METHOD__."($id) = '$path'"); + return $GLOBALS['egw_info']['server']['files_dir'].$path; + } + + /** + * Replace the password of an url with '...' for error messages + * + * @param string &$url + */ + static protected function _remove_password(&$url) + { + $parts = Vfs::parse_url($url); + + if ($parts['pass'] || $parts['scheme']) + { + $url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : ''). + $parts['host'].$parts['path']; + } + } + + /** + * Get storage mode from url (get parameter 'storage', eg. ?storage=db) + * + * @param string|array $url complete url or array of url-parts from parse_url + * @return int self::STORE2FS or self::STORE2DB + */ + static function url2operation($url) + { + $operation = self::DEFAULT_OPERATION; + + if (strpos(is_array($url) ? $url['query'] : $url,'storage=') !== false) + { + $query = null; + parse_str(is_array($url) ? $url['query'] : Vfs::parse_url($url,PHP_URL_QUERY), $query); + switch ($query['storage']) + { + case 'db': + $operation = self::STORE2DB; + break; + case 'fs': + default: + $operation = self::STORE2FS; + break; + } + } + //error_log(__METHOD__."('$url') = $operation (1=DB, 2=FS)"); + return $operation; + } + + /** + * Store properties for a single ressource (file or dir) + * + * @param string|int $path string with path or integer fs_id + * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) + * @return boolean true if props are updated, false otherwise (eg. ressource not found) + */ + static function proppatch($path,array $props) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); + if (!is_numeric($path)) + { + if (!($stat = self::url_stat($path,0))) + { + return false; + } + $id = $stat['ino']; + } + elseif(!($path = self::id2path($id=$path))) + { + return false; + } + if (!Vfs::check_access($path,EGW_ACL_EDIT,$stat)) + { + return false; // permission denied + } + $ins_stmt = $del_stmt = null; + foreach($props as &$prop) + { + if (!isset($prop['ns'])) $prop['ns'] = Vfs::DEFAULT_PROP_NAMESPACE; + + if (!isset($prop['val']) || self::$pdo_type != 'mysql') // for non mysql, we have to delete the prop anyway, as there's no REPLACE! + { + if (!isset($del_stmt)) + { + $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id AND prop_namespace=:prop_namespace AND prop_name=:prop_name'); + } + $del_stmt->execute(array( + 'fs_id' => $id, + 'prop_namespace' => $prop['ns'], + 'prop_name' => $prop['name'], + )); + } + if (isset($prop['val'])) + { + if (!isset($ins_stmt)) + { + $ins_stmt = self::$pdo->prepare((self::$pdo_type == 'mysql' ? 'REPLACE' : 'INSERT'). + ' INTO '.self::PROPS_TABLE.' (fs_id,prop_namespace,prop_name,prop_value) VALUES (:fs_id,:prop_namespace,:prop_name,:prop_value)'); + } + if (!$ins_stmt->execute(array( + 'fs_id' => $id, + 'prop_namespace' => $prop['ns'], + 'prop_name' => $prop['name'], + 'prop_value' => $prop['val'], + ))) + { + return false; + } + } + } + return true; + } + + /** + * Read properties for a ressource (file, dir or all files of a dir) + * + * @param array|string|int $path_ids (array of) string with path or integer fs_id + * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, use null for all + * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or + * fs_id/path => array of props for $depth==1 or is_array($path_ids) + */ + static function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE) + { + $ids = is_array($path_ids) ? $path_ids : array($path_ids); + foreach($ids as &$id) + { + if (!is_numeric($id)) + { + if (!($stat = self::url_stat($id,0))) + { + if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); + return false; + } + $id = $stat['ino']; + } + } + if (count($ids) >= 1) array_map(function(&$v) { $v = (int)$v; },$ids); + $query = 'SELECT * FROM '.self::PROPS_TABLE.' WHERE (fs_id'. + (count($ids) == 1 ? '='.(int)implode('',$ids) : ' IN ('.implode(',',$ids).')').')'. + (!is_null($ns) ? ' AND prop_namespace=?' : ''); + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + + $stmt = self::$pdo->prepare($query); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + $stmt->execute(!is_null($ns) ? array($ns) : array()); + + $props = array(); + foreach($stmt as $row) + { + $props[$row['fs_id']][] = array( + 'val' => $row['prop_value'], + 'name' => $row['prop_name'], + 'ns' => $row['prop_namespace'], + ); + } + if (!is_array($path_ids)) + { + $props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array(); // return empty array for no props + } + elseif ($props && isset($stat)) // need to map fs_id's to pathes + { + foreach(self::id2path(array_keys($props)) as $id => $path) + { + $props[$path] =& $props[$id]; + unset($props[$id]); + } + } + if (self::LOG_LEVEL > 1) + { + foreach((array)$props as $k => $v) + { + error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v)); + } + } + return $props; + } + + /** + * Register __CLASS__ for self::SCHEMA + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } +} + +StreamWrapper::register(); diff --git a/api/src/Vfs/Sqlfs/Utils.php b/api/src/Vfs/Sqlfs/Utils.php new file mode 100644 index 0000000000..ed8343aac3 --- /dev/null +++ b/api/src/Vfs/Sqlfs/Utils.php @@ -0,0 +1,483 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api\Vfs\Sqlfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use egw_exception_assertion_failed; + +/** + * sqlfs stream wrapper utilities: migration db-fs, fsck + */ +class Utils extends StreamWrapper +{ + /** + * Migrate SQLFS content from DB to filesystem + * + * @param boolean $debug true to echo a message for each copied file + */ + static function migrate_db2fs($debug=false) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. + ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; + + $fs_id = $fs_name = $fs_size = $fs_content = null; + $stmt = self::$pdo->prepare($query); + $stmt->bindColumn(1,$fs_id); + $stmt->bindColumn(2,$fs_name); + $stmt->bindColumn(3,$fs_size); + $stmt->bindColumn(4,$fs_content,\PDO::PARAM_LOB); + + if ($stmt->execute()) + { + $n = 0; + foreach($stmt as $row) + { + // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) + // PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( + if (is_string($fs_content)) + { + $content = fopen('php://temp', 'wb'); + fwrite($content, $fs_content); + fseek($content, 0, SEEK_SET); + unset($fs_content); + } + else + { + $content = $fs_content; + } + if (!is_resource($content)) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); + } + $filename = self::_fs_path($fs_id); + if (!file_exists($fs_dir=Vfs::dirname($filename))) + { + self::mkdir_recursive($fs_dir,0700,true); + } + if (!($dest = fopen($filename,'w'))) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); + } + if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); + } + if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; + fclose($dest); + fclose($content); unset($content); + + ++$n; + unset($row); // not used, as we access bound variables + } + unset($stmt); + + if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) + { + $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; + $stmt = self::$pdo->prepare($query); + $stmt->execute(); + } + } + return $n; + } + + /** + * Check and optionaly fix corruption in sqlfs + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + public static function fsck($check_only=true) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $msgs = array(); + foreach(array( + self::fsck_fix_required_nodes($check_only), + self::fsck_fix_multiple_active($check_only), + self::fsck_fix_unconnected($check_only), + self::fsck_fix_no_content($check_only), + ) as $check_msgs) + { + if ($check_msgs) $msgs = array_merge($msgs, $check_msgs); + } + + foreach ($GLOBALS['egw']->hooks->process(array( + 'location' => 'fsck', + 'check_only' => $check_only) + ) as $app_msgs) + { + if ($app_msgs) $msgs = array_merge($msgs, $app_msgs); + } + return $msgs; + } + + /** + * Check and optionally create required nodes: /, /home, /apps + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_required_nodes($check_only=true) + { + static $dirs = array( + '/' => 1, + '/home' => 2, + '/apps' => 3, + ); + $stmt = $delete_stmt = null; + $msgs = array(); + foreach($dirs as $path => $id) + { + if (!($stat = self::url_stat($path, STREAM_URL_STAT_LINK))) + { + if ($check_only) + { + $msgs[] = lang('Required directory "%1" not found!', $path); + } + else + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_id,fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. + ') VALUES (:fs_id,:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); + } + try { + $ok = $stmt->execute($data = array( + 'fs_id' => $id, + 'fs_name' => substr($path,1), + 'fs_dir' => $path == '/' ? 0 : $dirs['/'], + 'fs_mode' => 05, + 'fs_uid' => 0, + 'fs_gid' => 0, + 'fs_size' => 0, + 'fs_mime' => 'httpd/unix-directory', + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => 0, + )); + } + catch (\PDOException $e) + { + $ok = false; + unset($e); // ignore exception + } + if (!$ok) // can not insert it, try deleting it first + { + if (!isset($delete_stmt)) + { + $delete_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + } + try { + $ok = $delete_stmt->execute(array('fs_id' => $id)) && $stmt->execute($data); + } + catch (\PDOException $e) + { + unset($e); // ignore exception + } + } + $msgs[] = $ok ? lang('Required directory "%1" created.', $path) : + lang('Failed to create required directory "%1"!', $path); + } + } + // check if directory is at least world readable and executable (r-x), we allow more but not less + elseif (($stat['mode'] & 05) != 05) + { + if ($check_only) + { + $msgs[] = lang('Required directory "%1" has wrong mode %2 instead of %3!', + $path, Vfs::int2mode($stat['mode']), Vfs::int2mode(05|0x4000)); + } + else + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); + if (($ok = $stmt->execute(array( + 'fs_id' => $id, + 'fs_mode' => 05, + )))) + { + $msgs[] = lang('Mode of required directory "%1" changed to %2.', $path, Vfs::int2mode(05|0x4000)); + } + else + { + $msgs[] = lang('Failed to change mode of required directory "%1" to %2!', $path, Vfs::int2mode(05|0x4000)); + } + } + } + } + if (!$check_only && $msgs) + { + global $oProc; + if (!isset($oProc)) $oProc = new schema_proc(); + // PostgreSQL seems to require to update the sequenz, after manually inserting id's + $oProc->UpdateSequence('egw_sqlfs', 'fs_id'); + } + return $msgs; + } + + /** + * Check and optionally remove files without content part in physical filesystem + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_no_content($check_only=true) + { + $stmt = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. + " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) + { + if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) + { + $path = self::id2path($row['fs_id']); + if ($check_only) + { + $msgs[] = lang('File %1 has no content in physical filesystem %2!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + else + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + $stmt_props = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id'); + } + if ($stmt->execute(array('fs_id' => $row['fs_id'])) && + $stmt_props->execute(array('fs_id' => $row['fs_id']))) + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); + } + else + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + } + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Files without content in physical filesystem will be removed.'); + } + return $msgs; + } + + /** + * Name of lost+found directory for unconnected nodes + */ + const LOST_N_FOUND = '/lost+found'; + const LOST_N_FOUND_MOD = 070; + const LOST_N_FOUND_GRP = 'Admins'; + + /** + * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: + * + * SELECT fs.* + * FROM egw_sqlfs fs + * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir + * WHERE fs.fs_id > 1 && dir.fs_id IS NULL + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_unconnected($check_only=true) + { + $lostnfound = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. + ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. + ' WHERE fs.fs_id > 1 AND dir.fs_id IS NULL') as $row) + { + if ($check_only) + { + $msgs[] = lang('Found unconnected %1 %2!', + mime_magic::mime2label($row['fs_mime']), + Vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); + continue; + } + if (!isset($lostnfound)) + { + // check if we already have /lost+found, create it if not + if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + Vfs::$is_root = true; + if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || + !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || + self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || + !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); + } + Vfs::$is_root = false; + if (!$lostnfound) break; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); + } + if ($stmt->execute(array( + 'fs_dir' => $lostnfound['ino'], + 'fs_id' => $row['fs_id'], + ))) + { + $msgs[] = lang('Moved unconnected %1 %2 to %3.', + mime_magic::mime2label($row['fs_mime']), + Vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', + self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Failed to move unconnected %1 %2 to %3!', + mime_magic::mime2label($row['fs_mime']), Vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); + } + return $msgs; + } + + /** + * Check and optionally fix multiple active files and directories with identical path + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_multiple_active($check_only=true) + { + $stmt = $inactivate_msg_added = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. + ' WHERE fs_active='.self::_pdo_boolean(true). + ' GROUP BY fs_dir,'.(self::$pdo_type == 'mysql' ? 'BINARY ' : '').'fs_name'. // fs_name is casesensitive! + ' HAVING COUNT(*) > 1') as $row) + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. + ' FROM '.self::TABLE.' fs'. + ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. + " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); + $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. + ' SET fs_active='.self::_pdo_boolean(false). + ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). + ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); + } + //$msgs[] = array2string($row); + $cnt = 0; + $stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + )); + foreach($stmt as $n => $entry) + { + if ($entry['fs_mime'] == 'httpd/unix-directory') + { + if (!$n) + { + $dir = $entry; // directory to keep + $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); + if ($check_only) break; + } + else + { + if ($entry['children']) + { + $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', + $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. + ' WHERE fs_dir='.(int)$entry['fs_id']), + $entry['fs_id'], $dir['fs_id']); + + $dir['children'] += $children; + } + self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); + $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); + } + } + elseif (isset($dir)) // file and directory with same name exist! + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $dir['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = ucfirst(lang('none of %1', $row[2]-1)); + } + $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); + break; + } + else // newest file --> set for all other fs_active=false + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $entry['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = lang('none of %1', $row[2]-1); + } + $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', + self::id2path($entry['fs_id']), $cnt); + break; + } + } + unset($dir); + if ($cnt && !isset($inactivate_msg_added)) + { + $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); + $inactivate_msg_added = true; + } + } + return $msgs; + } +} + +// fsck testcode, if this file is called via it's URL (you need to uncomment it!) +/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) +{ + $GLOBALS['egw_info'] = array( + 'flags' => array( + 'currentapp' => 'admin', + 'nonavbar' => true, + ), + ); + include_once '../../header.inc.php'; + + $msgs = Utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); + echo '

'.implode("

\n

", (array)$msgs)."

\n"; +}*/ \ No newline at end of file diff --git a/phpgwapi/inc/class.vfs_stream_wrapper.inc.php b/api/src/Vfs/StreamWrapper.php similarity index 95% rename from phpgwapi/inc/class.vfs_stream_wrapper.inc.php rename to api/src/Vfs/StreamWrapper.php index 6aae8f2ab8..f4864904f8 100644 --- a/phpgwapi/inc/class.vfs_stream_wrapper.inc.php +++ b/api/src/Vfs/StreamWrapper.php @@ -1,16 +1,24 @@ - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +namespace EGroupware\Api\Vfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use egw_exception_db; + /** * eGroupWare API: VFS - stream wrapper interface * @@ -19,7 +27,7 @@ * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class vfs_stream_wrapper implements iface_stream_wrapper +class StreamWrapper implements StreamWrapperIface { /** * Scheme / protocol used for this stream-wrapper @@ -224,7 +232,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper { self::load_wrapper($scheme); } - $url = egw_vfs::concat($url,substr($parts['path'],strlen($mounted))); + $url = Vfs::concat($url,substr($parts['path'],strlen($mounted))); if ($replace_user_pass_host) { @@ -703,7 +711,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper * Requires owner or root rights! * * @param string $path - * @param string $mode mode string see egw_vfs::mode2int + * @param string $mode mode string see Vfs::mode2int * @return boolean true on success, false otherwise */ static function chmod($path,$mode) @@ -845,15 +853,15 @@ class vfs_stream_wrapper implements iface_stream_wrapper if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) opendir($this->opened_dir_url) failed!"); return false; } - $this->opened_dir_writable = egw_vfs::check_access($this->opened_dir_url,egw_vfs::WRITABLE); + $this->opened_dir_writable = Vfs::check_access($this->opened_dir_url,Vfs::WRITABLE); // check our fstab if we need to add some of the mountpoints $basepath = self::parse_url($path,PHP_URL_PATH); foreach(array_keys(self::$fstab) as $mounted) { - if (((egw_vfs::dirname($mounted) == $basepath || egw_vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') && + if (((Vfs::dirname($mounted) == $basepath || Vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') && // only return children readable by the user, if dir is not writable (!self::HIDE_UNREADABLES || $this->opened_dir_writable || - egw_vfs::check_access($mounted,egw_vfs::READABLE))) + Vfs::check_access($mounted,Vfs::READABLE))) { $this->extra_dirs[] = basename($mounted); } @@ -918,9 +926,9 @@ class vfs_stream_wrapper implements iface_stream_wrapper { if ($lpath[0] != '/') // concat relative path { - $lpath = egw_vfs::concat(self::parse_url($path,PHP_URL_PATH),'../'.$lpath); + $lpath = Vfs::concat(self::parse_url($path,PHP_URL_PATH),'../'.$lpath); } - $url = egw_vfs::PREFIX.$lpath; + $url = Vfs::PREFIX.$lpath; if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$flags) symlif (substr($path,-1) == '/' && $path != '/') $path = substr($path,0,-1); // remove trailing slash eg. added by WebDAVink found and resolved to $url"); // try reading the stat of the link if (($stat = self::url_stat($lpath, STREAM_URL_STAT_QUIET, false, true, $check_symlink_depth-1))) @@ -934,19 +942,19 @@ class vfs_stream_wrapper implements iface_stream_wrapper } catch (egw_exception_db $e) { // some long running operations, eg. merge-print, run into situation that DB closes our separate sqlfs connection - // we try now to reconnect sqlfs_stream_wrapper once + // we try now to reconnect Vfs\Sqlfs\StreamWrapper once // it's done here in vfs_stream_wrapper as situation can happen in sqlfs, links, stylite.links or stylite.versioning if ($try_reconnect) { // reconnect to db - sqlfs_stream_wrapper::reconnect(); + Vfs\Sqlfs\StreamWrapper::reconnect(); return self::url_stat($path, $flags, $try_create_home, $check_symlink_components, $check_symlink_depth, false); } // if numer of tries is exceeded, re-throw exception throw $e; } // check if a failed url_stat was for a home dir, in that case silently create it - if (!$stat && $try_create_home && egw_vfs::dirname(self::parse_url($path,PHP_URL_PATH)) == '/home' && + if (!$stat && $try_create_home && Vfs::dirname(self::parse_url($path,PHP_URL_PATH)) == '/home' && ($id = $GLOBALS['egw']->accounts->name2id(basename($path))) && $GLOBALS['egw']->accounts->id2name($id) == basename($path)) // make sure path has the right case! { @@ -979,8 +987,8 @@ class vfs_stream_wrapper implements iface_stream_wrapper /* Todo: if we hide non readables, we should return false on url_stat for consitency (if dir is not writabel) // Problem: this does NOT stop (calles itself infinit recursive)! - if (self::HIDE_UNREADABLES && !egw_vfs::check_access($path,egw_vfs::READABLE,$stat) && - !egw_vfs::check_access(egw_vfs::dirname($path,egw_vfs::WRITABLE))) + if (self::HIDE_UNREADABLES && !Vfs::check_access($path,Vfs::READABLE,$stat) && + !Vfs::check_access(Vfs::dirname($path,Vfs::WRITABLE))) { return false; } @@ -1004,8 +1012,8 @@ class vfs_stream_wrapper implements iface_stream_wrapper } if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url'): ".function_backtrace(1)); - while (($rel_path = egw_vfs::basename($url).($rel_path ? '/'.$rel_path : '')) && - ($url = egw_vfs::dirname($url))) + while (($rel_path = Vfs::basename($url).($rel_path ? '/'.$rel_path : '')) && + ($url = Vfs::dirname($url))) { if (($stat = self::url_stat($url,0,false,false))) { @@ -1015,14 +1023,14 @@ class vfs_stream_wrapper implements iface_stream_wrapper if ($lpath[0] != '/') { - $lpath = egw_vfs::concat(self::parse_url($url,PHP_URL_PATH),'../'.$lpath); + $lpath = Vfs::concat(self::parse_url($url,PHP_URL_PATH),'../'.$lpath); } - //self::symlinkCache_add($path,egw_vfs::PREFIX.$lpath); - $url = egw_vfs::PREFIX.egw_vfs::concat($lpath,$rel_path); + //self::symlinkCache_add($path,Vfs::PREFIX.$lpath); + $url = Vfs::PREFIX.Vfs::concat($lpath,$rel_path); if (self::LOG_LEVEL > 1) error_log("$log --> lpath='$lpath', url='$url'"); return self::url_stat($url,$flags); } - $url = egw_vfs::concat($url,$rel_path); + $url = Vfs::concat($url,$rel_path); if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url') returning null"); return null; } @@ -1110,10 +1118,10 @@ class vfs_stream_wrapper implements iface_stream_wrapper /** * Clears our internal stat and symlink cache * - * Normaly not necessary, as it is automatically cleared/updated, UNLESS egw_vfs::$user changes! + * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! * * We have to clear the symlink cache before AND after calling the backend, - * because auf traversal rights may be different when egw_vfs::$user changes! + * because auf traversal rights may be different when Vfs::$user changes! * * @param string $path ='/' path of backend, whos cache to clear */ @@ -1150,7 +1158,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper while($file !== false && (is_array($this->extra_dirs) && in_array($file,$this->extra_dirs) || // do NOT return extra_dirs twice self::HIDE_UNREADABLES && !$this->opened_dir_writable && - !egw_vfs::check_access(egw_vfs::concat($this->opened_dir_url,$file),egw_vfs::READABLE))); + !Vfs::check_access(Vfs::concat($this->opened_dir_url,$file),Vfs::READABLE))); } if (self::LOG_LEVEL > 1) error_log(__METHOD__."( $this->opened_dir ) = '$file'"); return $file; @@ -1247,7 +1255,16 @@ class vfs_stream_wrapper implements iface_stream_wrapper */ static function scheme2class($scheme) { - return str_replace('.','_',$scheme).'_stream_wrapper'; + list($app, $app_scheme) = explode('.', $scheme); + foreach(array( + empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\'.ucfirst($scheme).'\\StreamWrapper' : // streamwrapper in Api\Vfs + 'EGroupware\\'.ucfirst($app).'\\Vfs\\'.ucfirst($app_scheme).'\\StreamWrapper', // streamwrapper in $app\Vfs + str_replace('.','_',$scheme).'_stream_wrapper', // old (flat) name + ) as $class) + { + //error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class))); + if (class_exists($class)) return $class; + } } /** @@ -1360,4 +1377,4 @@ class vfs_stream_wrapper implements iface_stream_wrapper } } -vfs_stream_wrapper::init_static(); +StreamWrapper::init_static(); diff --git a/api/src/Vfs/StreamWrapperIface.php b/api/src/Vfs/StreamWrapperIface.php new file mode 100644 index 0000000000..e9ac4e05b9 --- /dev/null +++ b/api/src/Vfs/StreamWrapperIface.php @@ -0,0 +1,250 @@ +=') && version_compare(PHP_VERSION,'5.1','<')) + * { + * $eof = !$eof; + * } + * + * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise + */ + function stream_eof ( ); + + /** + * This method is called in response to ftell() calls on the stream. + * + * @return integer current read/write position of the stream + */ + function stream_tell ( ); + + /** + * This method is called in response to fseek() calls on the stream. + * + * You should update the read/write position of the stream according to offset and whence. + * See fseek() for more information about these parameters. + * + * @param integer $offset + * @param integer $whence SEEK_SET - Set position equal to offset bytes + * SEEK_CUR - Set position to current location plus offset. + * SEEK_END - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) + * @return boolean TRUE if the position was updated, FALSE otherwise. + */ + function stream_seek ( $offset, $whence ); + + /** + * This method is called in response to fflush() calls on the stream. + * + * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. + * + * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. + */ + function stream_flush ( ); + + /** + * This method is called in response to fstat() calls on the stream. + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @return array containing the same values as appropriate for the stream. + */ + function stream_stat ( ); + + /** + * This method is called in response to unlink() calls on URL paths associated with the wrapper. + * + * It should attempt to delete the item specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! + * + * @param string $path + * @return boolean TRUE on success or FALSE on failure + */ + static function unlink ( $path ); + + /** + * This method is called in response to rename() calls on URL paths associated with the wrapper. + * + * It should attempt to rename the item specified by path_from to the specification given by path_to. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. + * + * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! + * + * @param string $path_from + * @param string $path_to + * @return boolean TRUE on success or FALSE on failure + */ + static function rename ( $path_from, $path_to ); + + /** + * This method is called in response to mkdir() calls on URL paths associated with the wrapper. + * + * It should attempt to create the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. + * + * @param string $path + * @param int $mode + * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE + * @return boolean TRUE on success or FALSE on failure + */ + static function mkdir ( $path, $mode, $options ); + + /** + * This method is called in response to rmdir() calls on URL paths associated with the wrapper. + * + * It should attempt to remove the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. + * + * @param string $path + * @param int $options Possible values include STREAM_REPORT_ERRORS. + * @return boolean TRUE on success or FALSE on failure. + */ + static function rmdir ( $path, $options ); + + /** + * This method is called immediately when your stream object is created for examining directory contents with opendir(). + * + * @param string $path URL that was passed to opendir() and that this object is expected to explore. + * @return booelan + */ + function dir_opendir ( $path, $options ); + + /** + * This method is called in response to stat() calls on the URL paths associated with the wrapper. + * + * It should return as many elements in common with the system function as possible. + * Unknown or unavailable values should be set to a rational value (usually 0). + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @param string $path + * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: + * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, + * or a filesystem symlink). This flag specified that only information about the link itself should be returned, + * not the resource pointed to by the link. + * This flag is set in response to calls to lstat(), is_link(), or filetype(). + * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, + * you are responsible for reporting errors using the trigger_error() function during stating of the path. + * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! + * @return array + */ + static function url_stat ( $path, $flags ); + + /** + * This method is called in response to readdir(). + * + * It should return a string representing the next filename in the location opened by dir_opendir(). + * + * @return string + */ + function dir_readdir ( ); + + /** + * This method is called in response to rewinddir(). + * + * It should reset the output generated by dir_readdir(). i.e.: + * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). + * + * @return boolean + */ + function dir_rewinddir ( ); + + /** + * This method is called in response to closedir(). + * + * You should release any resources which were locked or allocated during the opening and use of the directory stream. + * + * @return boolean + */ + function dir_closedir ( ); +} diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index f907de8cf5..74e08e3ddd 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -624,7 +624,7 @@ foreach($widgets as $app => $list) { try { - __autoload($class); + class_exists($class); // trigger autoloader } catch(Exception $e) { diff --git a/phpgwapi/inc/class.egw_vfs.inc.php b/phpgwapi/inc/class.egw_vfs.inc.php index 9135c9d0ca..a2af4f07b9 100644 --- a/phpgwapi/inc/class.egw_vfs.inc.php +++ b/phpgwapi/inc/class.egw_vfs.inc.php @@ -11,2090 +11,9 @@ * @version $Id$ */ +use EGroupware\Api\Vfs; + /** - * Class containing static methods to use the new eGW virtual file system - * - * This extension of the vfs stream-wrapper allows to use the following static functions, - * which only allow access to the eGW VFS and need no 'vfs://default' prefix for filenames: - * - * - resource egw_vfs::fopen($path,$mode) like fopen, returned resource can be used with fwrite etc. - * - resource egw_vfs::opendir($path) like opendir, returned resource can be used with readdir etc. - * - boolean egw_vfs::copy($from,$to) like copy - * - boolean egw_vfs::rename($old,$new) renaming or moving a file in the vfs - * - boolean egw_vfs::mkdir($path) creating a new dir in the vfs - * - boolean egw_vfs::rmdir($path) removing (an empty) directory - * - boolean egw_vfs::unlink($path) removing a file - * - boolean egw_vfs::touch($path,$mtime=null) touch a file - * - boolean egw_vfs::stat($path) returning status of file like stat(), but only with string keys (no numerical indexes)! - * - * With the exception of egw_vfs::touch() (not yet part of the stream_wrapper interface) - * you can always use the standard php functions, if you add a 'vfs://default' prefix - * to every filename or path. Be sure to always add the prefix, as the user otherwise gains - * access to the real filesystem of the server! - * - * The two following methods can be used to persitently mount further filesystems (without editing the code): - * - * - boolean|array egw_vfs::mount($url,$path) to mount $ur on $path or to return the fstab when called without argument - * - boolean egw_vfs::umount($path) to unmount a path or url - * - * The stream wrapper interface allows to access hugh files in junks to not be limited by the - * memory_limit setting of php. To do you should pass the opened file as resource and not the content: - * - * $file = egw_vfs::fopen('/home/user/somefile','r'); - * $content = fread($file,1024); - * - * You can also attach stream filters, to eg. base64 encode or compress it on the fly, - * without the need to hold the content of the whole file in memmory. - * - * If you want to copy a file, you can use stream_copy_to_stream to do a copy of a file far bigger then - * php's memory_limit: - * - * $from = egw_vfs::fopen('/home/user/fromfile','r'); - * $to = egw_vfs::fopen('/home/user/tofile','w'); - * - * stream_copy_to_stream($from,$to); - * - * The static egw_vfs::copy() method does exactly that, but you have to do it eg. on your own, if - * you want to copy eg. an uploaded file into the vfs. - * - * egw_vfs::parse_url($url, $component=-1), egw_vfs::dirname($url) and egw_vfs::basename($url) work - * on urls containing utf-8 characters, which get NOT urlencoded in our VFS! + * @deprecated use EGroupware\Api\Vfs */ -class egw_vfs extends vfs_stream_wrapper -{ - const PREFIX = 'vfs://default'; - /** - * Readable bit, for dirs traversable - */ - const READABLE = 4; - /** - * Writable bit, for dirs delete or create files in that dir - */ - const WRITABLE = 2; - /** - * Excecutable bit, here only use to check if user is allowed to search dirs - */ - const EXECUTABLE = 1; - /** - * Name of the lock table - */ - const LOCK_TABLE = 'egw_locks'; - /** - * Current user has root rights, no access checks performed! - * - * @var boolean - */ - static $is_root = false; - /** - * Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id'] - * - * @var int - */ - static $user; - /** - * Current user is an eGW admin - * - * @var boolean - */ - static $is_admin = false; - /** - * Total of last find call - * - * @var int - */ - static $find_total; - /** - * Reference to the global db object - * - * @var egw_db - */ - static $db; - - /** - * fopen working on just the eGW VFS - * - * @param string $path filename with absolute path in the eGW VFS - * @param string $mode 'r', 'w', ... like fopen - * @return resource - */ - static function fopen($path,$mode) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Filename '$path' is not an absolute path!"); - } - return fopen(self::PREFIX.$path,$mode); - } - - /** - * opendir working on just the eGW VFS: returns resource for readdir() etc. - * - * @param string $path filename with absolute path in the eGW VFS - * @return resource - */ - static function opendir($path) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return opendir(self::PREFIX.$path); - } - - /** - * dir working on just the eGW VFS: returns directory object - * - * @param string $path filename with absolute path in the eGW VFS - * @return Directory - */ - static function dir($path) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return dir(self::PREFIX.$path); - } - - /** - * scandir working on just the eGW VFS: returns array with filenames as values - * - * @param string $path filename with absolute path in the eGW VFS - * @param int $sorting_order =0 !$sorting_order (default) alphabetical in ascending order, $sorting_order alphabetical in descending order. - * @return array - */ - static function scandir($path,$sorting_order=0) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return scandir(self::PREFIX.$path,$sorting_order); - } - - /** - * copy working on just the eGW VFS - * - * @param string $from - * @param string $to - * @return boolean - */ - static function copy($from,$to) - { - $old_props = self::file_exists($to) ? self::propfind($to,null) : array(); - // copy properties (eg. file comment), if there are any and evtl. existing old properties - $props = self::propfind($from,null); - - foreach($old_props as $prop) - { - if (!self::find_prop($props,$prop)) - { - $prop['val'] = null; // null = delete prop - $props[] = $prop; - } - } - // using self::copy_uploaded() to treat copying incl. properties as atomar operation in respect of notifications - return self::copy_uploaded(self::PREFIX.$from,$to,$props,false); // false = no is_uploaded_file check! - } - - /** - * Find a specific property in an array of properties (eg. returned by propfind) - * - * @param array &$props - * @param array|string $name property array or name - * @param string $ns =self::DEFAULT_PROP_NAMESPACE namespace, only if $prop is no array - * @return &array reference to property in $props or null if not found - */ - static function &find_prop(array &$props,$name,$ns=self::DEFAULT_PROP_NAMESPACE) - { - if (is_array($name)) - { - $ns = $name['ns']; - $name = $name['name']; - } - foreach($props as &$prop) - { - if ($prop['name'] == $name && $prop['ns'] == $ns) return $prop; - } - return null; - } - - /** - * stat working on just the eGW VFS (alias of url_stat) - * - * @param string $path filename with absolute path in the eGW VFS - * @param boolean $try_create_home =false should a non-existing home-directory be automatically created - * @return array - */ - static function stat($path,$try_create_home=false) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); - } - if (($stat = self::url_stat($path,0,$try_create_home))) - { - $stat = array_slice($stat,13); // remove numerical indices 0-12 - } - return $stat; - } - - /** - * lstat (not resolving symbolic links) working on just the eGW VFS (alias of url_stat) - * - * @param string $path filename with absolute path in the eGW VFS - * @param boolean $try_create_home =false should a non-existing home-directory be automatically created - * @return array - */ - static function lstat($path,$try_create_home=false) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); - } - if (($stat = self::url_stat($path,STREAM_URL_STAT_LINK,$try_create_home))) - { - $stat = array_slice($stat,13); // remove numerical indices 0-12 - } - return $stat; - } - - /** - * is_dir() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function is_dir($path) - { - return $path[0] == '/' && is_dir(self::PREFIX.$path); - } - - /** - * is_link() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function is_link($path) - { - return $path[0] == '/' && is_link(self::PREFIX.$path); - } - - /** - * file_exists() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function file_exists($path) - { - return $path[0] == '/' && file_exists(self::PREFIX.$path); - } - - /** - * Mounts $url under $path in the vfs, called without parameter it returns the fstab - * - * The fstab is stored in the eGW configuration and used for all eGW users. - * - * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/ - * @param string $path =null path to mount the filesystem in the vfs, eg. / - * @param boolean $check_url =null check if url is an existing directory, before mounting it - * default null only checks if url does not contain a $ as used in $user or $pass - * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request - * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount - * @return array|boolean array with fstab, if called without parameter or true on successful mount - */ - static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false) - { - if (is_null($check_url)) $check_url = strpos($url,'$') === false; - - if (!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup - { - $api_config = config::read('phpgwapi'); - if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) - { - self::$fstab = $api_config['vfs_fstab']; - } - unset($api_config); - } - if (is_null($url) || is_null($path)) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); - return self::$fstab; - } - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if ($clear_fstab) - { - self::$fstab = array(); - } - if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); - return true; // already mounted - } - self::load_wrapper(self::parse_url($url,PHP_URL_SCHEME)); - - if ($check_url && (!file_exists($url) || opendir($url) === false)) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); - return false; // url does not exist - } - self::$fstab[$path] = $url; - - uksort(self::$fstab,create_function('$a,$b','return strlen($a)-strlen($b);')); - - if ($persitent_mount) - { - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); - return true; - } - - /** - * Unmounts a filesystem part of the vfs - * - * @param string $path url or path of the filesystem to unmount - */ - static function umount($path) - { - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); - return false; // $path not mounted - } - unset(self::$fstab[$path]); - - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); - return true; - } - - /** - * Check if file is hidden: name starts with a '.' or is Thumbs.db - * - * @param string $path - * @return boolean - */ - public static function is_hidden($path) - { - $file = self::basename($path); - - return $file[0] == '.' || $file == 'Thumbs.db'; - } - - /** - * find = recursive search over the filesystem - * - * @param string|array $base base of the search - * @param array $options =null the following keys are allowed: - * - type => {d|f|F} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), default all - * - depth => {true|false(default)} put the contents of a dir before the dir itself - * - dirsontop => {true(default)|false} allways return dirs before the files (two distinct blocks) - * - mindepth,maxdepth minimal or maximal depth to be returned - * - name,path => pattern with *,? wildcards, eg. "*.php" - * - name_preg,path_preg => preg regular expresion, eg. "/(vfs|wrapper)/" - * - uid,user,gid,group,nouser,nogroup file belongs to user/group with given name or (numerical) id - * - mime => type[/subtype] or perl regular expression starting with a "/" eg. "/^(image|video)\\//i" - * - empty,size => (+|-|)N - * - cmin/mmin => (+|-|)N file/dir create/modified in the last N minutes - * - ctime/mtime => (+|-|)N file/dir created/modified in the last N days - * - url => false(default),true allow (and return) full URL's instead of VFS pathes (only set it, if you know what you doing securitywise!) - * - need_mime => false(default),true should we return the mime type - * - order => name order rows by name column - * - sort => (ASC|DESC) sort, default ASC - * - limit => N,[n=0] return N entries from position n on, which defaults to 0 - * - follow => {true|false(default)} follow symlinks - * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db) - * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or - * true to return file => stat pairs - * @param array $exec_params =null further params for exec as array, path is always the first param and stat the last! - * @return array of pathes if no $exec, otherwise path => stat pairs - */ - static function find($base,$options=null,$exec=null,$exec_params=null) - { - //error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n"); - - $type = $options['type']; // 'd', 'f' or 'F' - $dirs_last = $options['depth']; // put content of dirs before the dir itself - // show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary) - $dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0); - if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work - - // process some of the options (need to be done only once) - if (isset($options['name']) && !isset($options['name_preg'])) // change from simple *,? wildcards to preg regular expression once - { - $options['name_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['name'])).'$/i'; - } - if (isset($options['path']) && !isset($options['preg_path'])) // change from simple *,? wildcards to preg regular expression once - { - $options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'])).'$/i'; - } - if (!isset($options['uid'])) - { - if (isset($options['user'])) - { - $options['uid'] = $GLOBALS['egw']->accounts->name2id($options['user'],'account_lid','u'); - } - elseif (isset($options['nouser'])) - { - $options['uid'] = 0; - } - } - if (!isset($options['gid'])) - { - if (isset($options['group'])) - { - $options['gid'] = abs($GLOBALS['egw']->accounts->name2id($options['group'],'account_lid','g')); - } - elseif (isset($options['nogroup'])) - { - $options['gid'] = 0; - } - } - if ($options['order'] == 'mime') - { - $options['need_mime'] = true; // we need to return the mime colum - } - $url = $options['url']; - - if (!is_array($base)) - { - $base = array($base); - } - $result = array(); - foreach($base as $path) - { - if (!$url) - { - if ($path[0] != '/' || !egw_vfs::stat($path)) continue; - $path = egw_vfs::PREFIX . $path; - } - if (!isset($options['remove'])) - { - $options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0; - } - $is_dir = is_dir($path); - if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir)) - { - self::_check_add($options,$path,$result); - } - if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 && $options['depth'] < $options['maxdepth'])) && ($dir = @opendir($path))) - { - while(($fname = readdir($dir)) !== false) - { - if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir! - - if (self::is_hidden($fname) && !$options['hidden']) continue; // ignore hidden files - - $file = self::concat($path, $fname); - - if ((int)$options['mindepth'] <= 1) - { - self::_check_add($options,$file,$result); - } - // only descend into subdirs, if it's a real dir (no link to a dir) or we should follow symlinks - if (is_dir($file) && ($options['follow'] || !is_link($file)) && (!isset($options['maxdepth']) || $options['maxdepth'] > 1)) - { - $opts = $options; - if ($opts['mindepth']) $opts['mindepth']--; - if ($opts['maxdepth']) $opts['depth']++; - unset($opts['order']); - unset($opts['limit']); - foreach(self::find($options['url']?$file:self::parse_url($file,PHP_URL_PATH),$opts,true) as $p => $s) - { - unset($result[$p]); - $result[$p] = $s; - } - } - } - closedir($dir); - } - if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last) - { - self::_check_add($options,$path,$result); - } - } - // sort code, to place directories before files, if $dirsontop enabled - $dirsfirst = $dirsontop ? '($a[mime]==\''.self::DIR_MIME_TYPE.'\')!==($b[mime]==\''.self::DIR_MIME_TYPE.'\')?'. - '($a[mime]==\''.self::DIR_MIME_TYPE.'\'?-1:1):' : ''; - // ordering of the rows - if (isset($options['order'])) - { - $sort = strtolower($options['sort']) == 'desc' ? '-' : ''; - switch($options['order']) - { - // sort numerical - case 'size': - case 'uid': - case 'gid': - case 'mode': - case 'ctime': - case 'mtime': - $code = $dirsfirst.$sort.'($a[\''.$options['order'].'\']-$b[\''.$options['order'].'\']);'; - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - $ok = uasort($result,create_function('$a,$b',$code)); - break; - - // sort alphanumerical - default: - $options['order'] = 'name'; - // fall throught - case 'name': - case 'mime': - $code = $dirsfirst.$sort.'strcasecmp($a[\''.$options['order'].'\'],$b[\''.$options['order'].'\']);'; - if ($options['order'] != 'name') - { - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - } - else - { - $code = 'return '.$code; - } - $ok = uasort($result,create_function('$a,$b',$code)); - break; - } - //echo "

order='$options[order]', sort='$options[sort]' --> uasort($result,create_function(,'$code'))=".array2string($ok)."

>\n"; - } - // limit resultset - self::$find_total = count($result); - if (isset($options['limit'])) - { - list($limit,$start) = explode(',',$options['limit']); - if (!$limit && !($limit = $GLOBALS['egw_info']['user']['preferences']['comman']['maxmatches'])) $limit = 15; - //echo "total=".egw_vfs::$find_total.", limit=$options[limit] --> start=$start, limit=$limit
\n"; - - if ((int)$start || self::$find_total > $limit) - { - $result = array_slice($result,(int)$start,(int)$limit,true); - } - } - //echo $path; _debug_array($result); - if ($exec !== true && is_callable($exec)) - { - if (!is_array($exec_params)) - { - $exec_params = is_null($exec_params) ? array() : array($exec_params); - } - foreach($result as $path => &$stat) - { - $options = $exec_params; - array_unshift($options,$path); - array_push($options,$stat); - //echo "calling ".print_r($exec,true).print_r($options,true)."\n"; - $stat = call_user_func_array($exec,$options); - } - return $result; - } - //error_log("egw_vfs::find($path)=".print_r(array_keys($result),true)); - if ($exec !== true) - { - return array_keys($result); - } - return $result; - } - - /** - * Function carying out the various (optional) checks, before files&dirs get returned as result of find - * - * @param array $options options, see egw_vfs::find(,$options) - * @param string $path name of path to add - * @param array &$result here we add the stat for the key $path, if the checks are successful - */ - private static function _check_add($options,$path,&$result) - { - $type = $options['type']; // 'd' or 'f' - - if ($options['url']) - { - $stat = @lstat($path); - } - else - { - $stat = self::url_stat($path,STREAM_URL_STAT_LINK); - } - if (!$stat) - { - return; // not found, should not happen - } - if ($type && (($type == 'd') == !($stat['mode'] & sqlfs_stream_wrapper::MODE_DIR) || // != is_dir() which can be true for symlinks - $type == 'F' && is_dir($path))) // symlink to a directory - { - return; // wrong type - } - $stat = array_slice($stat,13); // remove numerical indices 0-12 - $stat['path'] = self::parse_url($path,PHP_URL_PATH); - $stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path); - - if ($options['mime'] || $options['need_mime']) - { - $stat['mime'] = self::mime_content_type($path); - } - if (isset($options['name_preg']) && !preg_match($options['name_preg'],$stat['name']) || - isset($options['path_preg']) && !preg_match($options['path_preg'],$path)) - { - //echo "

!preg_match('{$options['name_preg']}','{$stat['name']}')

\n"; - return; // wrong name or path - } - if (isset($options['gid']) && $stat['gid'] != $options['gid'] || - isset($options['uid']) && $stat['uid'] != $options['uid']) - { - return; // wrong user or group - } - if (isset($options['mime']) && $options['mime'] != $stat['mime']) - { - if ($options['mime'][0] == '/') // perl regular expression given - { - if (!preg_match($options['mime'], $stat['mime'])) - { - return; // wrong mime-type - } - } - else - { - list($type,$subtype) = explode('/',$options['mime']); - // no subtype (eg. 'image') --> check only the main type - if ($subtype || substr($stat['mime'],0,strlen($type)+1) != $type.'/') - { - return; // wrong mime-type - } - } - } - if (isset($options['size']) && !self::_check_num($stat['size'],$options['size']) || - (isset($options['empty']) && !!$options['empty'] !== !$stat['size'])) - { - return; // wrong size - } - if (isset($options['cmin']) && !self::_check_num(round((time()-$stat['ctime'])/60),$options['cmin']) || - isset($options['mmin']) && !self::_check_num(round((time()-$stat['mtime'])/60),$options['mmin']) || - isset($options['ctime']) && !self::_check_num(round((time()-$stat['ctime'])/86400),$options['ctime']) || - isset($options['mtime']) && !self::_check_num(round((time()-$stat['mtime'])/86400),$options['mtime'])) - { - return; // not create/modified in the spezified time - } - // do we return url or just vfs pathes - if (!$options['url']) - { - $path = self::parse_url($path,PHP_URL_PATH); - } - $result[$path] = $stat; - } - - private static function _check_num($value,$argument) - { - if (is_int($argument) && $argument >= 0 || $argument[0] != '-' && $argument[0] != '+') - { - //echo "_check_num($value,$argument) check = == ".(int)($value == $argument)."\n"; - return $value == $argument; - } - if ($argument < 0) - { - //echo "_check_num($value,$argument) check < == ".(int)($value < abs($argument))."\n"; - return $value < abs($argument); - } - //echo "_check_num($value,$argument) check > == ".(int)($value > (int)substr($argument,1))."\n"; - return $value > (int) substr($argument,1); - } - - /** - * Recursiv remove all given url's, including it's content if they are files - * - * @param string|array $urls url or array of url's - * @param boolean $allow_urls =false allow to use url's, default no only pathes (to stay within the vfs) - * @throws egw_exception_assertion_failed when trainig to remove /, /apps or /home - * @return array - */ - static function remove($urls,$allow_urls=false) - { - //error_log(__METHOD__.'('.array2string($urls).')'); - // some precaution to never allow to (recursivly) remove /, /apps or /home - foreach((array)$urls as $url) - { - if (preg_match('/^\/?(home|apps|)\/*$/',self::parse_url($url,PHP_URL_PATH))) - { - throw new egw_exception_assertion_failed(__METHOD__.'('.array2string($urls).") Cautiously rejecting to remove folder '$url'!"); - } - } - return self::find($urls,array('depth'=>true,'url'=>$allow_urls,'hidden'=>true),array(__CLASS__,'_rm_rmdir')); - } - - /** - * Helper function for remove: either rmdir or unlink given url (depending if it's a dir or file) - * - * @param string $url - * @return boolean - */ - static function _rm_rmdir($url) - { - if ($url[0] == '/') - { - $url = self::PREFIX . $url; - } - if (is_dir($url) && !is_link($url)) - { - return egw_vfs::rmdir($url,0); - } - return egw_vfs::unlink($url); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @param int $check mode to check: one or more or'ed together of: 4 = egw_vfs::READABLE, - * 2 = egw_vfs::WRITABLE, 1 = egw_vfs::EXECUTABLE - * @return boolean - */ - static function is_readable($path,$check = self::READABLE) - { - return self::check_access($path,$check); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path path - * @param int $check mode to check: one or more or'ed together of: 4 = egw_vfs::READABLE, - * 2 = egw_vfs::WRITABLE, 1 = egw_vfs::EXECUTABLE - * @param array|boolean $stat =null stat array or false, to not query it again - * @param int $user =null user used for check, if not current user (egw_vfs::$user) - * @return boolean - */ - static function check_access($path, $check, $stat=null, $user=null) - { - if (is_null($stat) && $user && $user != self::$user) - { - static $path_user_stat = array(); - - $backup_user = self::$user; - self::$user = $user; - - if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user])) - { - self::clearstatcache($path); - - $path_user_stat[$path][$user] = self::url_stat($path, 0); - - self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again! - } - if (($stat = $path_user_stat[$path][$user])) - { - // some backend mounts use $user:$pass in their url, for them we have to deny access! - if (strpos(self::resolve_url($path, false, false, false), '$user') !== false) - { - $ret = false; - } - else - { - $ret = self::check_access($path, $check, $stat); - } - } - else - { - $ret = false; // no access, if we can not stat the file - } - self::$user = $backup_user; - - // we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session - self::clearstatcache($path); - - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check,$user) ".array2string($ret)); - return $ret; - } - - if (self::$is_root) - { - return true; - } - - // throw exception if stat array is used insead of path, can be removed soon - if (is_array($path)) - { - throw new egw_exception_wrong_parameter('path has to be string, use check_access($path,$check,$stat=null)!'); - } - // query stat array, if not given - if (is_null($stat)) - { - $stat = self::url_stat($path,0); - } - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)"); - - if (!$stat) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) no stat array!"); - return false; // file not found - } - // check if we use an EGroupwre stream wrapper, or a stock php one - // if it's not an EGroupware one, we can NOT use uid, gid and mode! - if (($scheme = self::parse_url($stat['url'],PHP_URL_SCHEME)) && !(class_exists(self::scheme2class($scheme)))) - { - switch($check) - { - case self::READABLE: - return is_readable($stat['url']); - case self::WRITABLE: - return is_writable($stat['url']); - case self::EXECUTABLE: - return is_executable($stat['url']); - } - } - // check if other rights grant access - if (($stat['mode'] & $check) == $check) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!"); - return true; - } - // check if there's owner access and we are the owner - if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!"); - return true; - } - // check if there's a group access and we have the right membership - if (($stat['mode'] & ($check << 3)) == ($check << 3) && $stat['gid']) - { - if (($memberships = $GLOBALS['egw']->accounts->memberships(self::$user, true)) && in_array(-abs($stat['gid']), $memberships)) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via group rights!"); - return true; - } - } - // check backend for extended acls (only if path given) - $ret = $path && self::_call_on_backend('check_extended_acl',array(isset($stat['url'])?$stat['url']:$path,$check),true); // true = fail silent if backend does not support - - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) ".($ret ? 'backend extended acl granted access.' : 'no access!!!')); - return $ret; - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @return boolean - */ - static function is_writable($path) - { - return self::is_readable($path,self::WRITABLE); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @return boolean - */ - static function is_executable($path) - { - return self::is_readable($path,self::EXECUTABLE); - } - - /** - * Check if path is a script and write access would be denied by backend - * - * @param string $path - * @return boolean true if $path is a script AND exec mount-option is NOT set, false otherwise - */ - static function deny_script($path) - { - return self::_call_on_backend('deny_script',array($path),true); - } - - /** - * Name of EACL array in session - */ - const SESSION_EACL = 'session-eacl'; - - /** - * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) - * - * Does NOT check if user has the rights to set the extended acl for the given url/path! - * - * @param string $url string with path - * @param int $rights =null rights to set, or null to delete the entry - * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param boolean $session_only =false true: set eacl only for this session, does NO further checks currently! - * @return boolean true if acl is set/deleted, false on error - */ - static function eacl($url,$rights=null,$owner=null,$session_only=false) - { - if ($session_only) - { - $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); - $session_eacls[] = array( - 'path' => $url[0] == '/' ? $url : egw_vfs::parse_url($url, PHP_URL_PATH), - 'owner' => $owner ? $owner : egw_vfs::$user, - 'rights' => $rights, - ); - return true; - } - return self::_call_on_backend('eacl',array($url,$rights,$owner)); - } - - /** - * Get all ext. ACL set for a path - * - * Calls itself recursive, to get the parent directories - * - * @param string $path - * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found - */ - static function get_eacl($path) - { - $eacls = self::_call_on_backend('get_eacl',array($path),true); // true = fail silent (no PHP Warning) - - $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); - if ($session_eacls) - { - // eacl is recursive, therefore we have to match all parent-dirs too - $paths = array($path); - while ($path && $path != '/') - { - $paths[] = $path = egw_vfs::dirname($path); - } - foreach((array)$session_eacls as $eacl) - { - if (in_array($eacl['path'], $paths)) - { - $eacls[] = $eacl; - } - } - - // sort by length descending, to show precedence - usort($eacls, function($a, $b) { - return strlen($b['path']) - strlen($a['path']); - }); - } - return $eacls; - } - - /** - * Store properties for a single ressource (file or dir) - * - * @param string $path string with path - * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) - * @return boolean true if props are updated, false otherwise (eg. ressource not found) - */ - static function proppatch($path,array $props) - { - return self::_call_on_backend('proppatch',array($path,$props)); - } - - /** - * Default namespace for properties set by eGroupware: comment or custom fields (leading #) - * - */ - const DEFAULT_PROP_NAMESPACE = 'http://egroupware.org/'; - - /** - * Read properties for a ressource (file, dir or all files of a dir) - * - * @param array|string $path (array of) string with path - * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, otherwise use null - * @return array|boolean array with props (values for keys 'name', 'ns', 'val'), or path => array of props for is_array($path) - * false if $path does not exist - */ - static function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE) - { - return self::_call_on_backend('propfind',array($path,$ns),true); // true = fail silent (no PHP Warning) - } - - /** - * Private constructor to prevent instanciating this class, only it's static methods should be used - */ - private function __construct() - { - - } - - /** - * Convert a symbolic mode string or octal mode to an integer - * - * @param string|int $set comma separated mode string to set [ugo]+[+=-]+[rwx]+ - * @param int $mode =0 current mode of the file, necessary for +/- operation - * @return int - */ - static function mode2int($set,$mode=0) - { - if (is_int($set)) // already an integer - { - return $set; - } - if (is_numeric($set)) // octal string - { - //error_log(__METHOD__."($set,$mode) returning ".(int)base_convert($set,8,10)); - return (int)base_convert($set,8,10); // convert octal to decimal - } - foreach(explode(',',$set) as $s) - { - $matches = null; - if (!preg_match($use='/^([ugoa]*)([+=-]+)([rwx]+)$/',$s,$matches)) - { - $use = str_replace(array('/','^','$','(',')'),'',$use); - throw new egw_exception_wrong_userinput("$s is not an allowed mode, use $use !"); - } - $base = (strpos($matches[3],'r') !== false ? self::READABLE : 0) | - (strpos($matches[3],'w') !== false ? self::WRITABLE : 0) | - (strpos($matches[3],'x') !== false ? self::EXECUTABLE : 0); - - for($n = $m = 0; $n < strlen($matches[1]); $n++) - { - switch($matches[1][$n]) - { - case 'o': - $m |= $base; - break; - case 'g': - $m |= $base << 3; - break; - case 'u': - $m |= $base << 6; - break; - default: - case 'a': - $m = $base | ($base << 3) | ($base << 6); - } - } - switch($matches[2]) - { - case '+': - $mode |= $m; - break; - case '=': - $mode = $m; - break; - case '-': - $mode &= ~$m; - } - } - //error_log(__METHOD__."($set,) returning ".sprintf('%o',$mode)); - return $mode; - } - - /** - * Convert a numerical mode to a symbolic mode-string - * - * @param int $mode - * @return string - */ - static function int2mode( $mode ) - { - if(($mode & self::MODE_LINK) == self::MODE_LINK) // Symbolic Link - { - $sP = 'l'; - } - elseif(($mode & 0xC000) == 0xC000) // Socket - { - $sP = 's'; - } - elseif($mode & 0x1000) // FIFO pipe - { - $sP = 'p'; - } - elseif($mode & 0x2000) // Character special - { - $sP = 'c'; - } - elseif($mode & 0x4000) // Directory - { - $sP = 'd'; - } - elseif($mode & 0x6000) // Block special - { - $sP = 'b'; - } - elseif($mode & 0x8000) // Regular - { - $sP = '-'; - } - else // UNKNOWN - { - $sP = 'u'; - } - - // owner - $sP .= (($mode & 0x0100) ? 'r' : '-') . - (($mode & 0x0080) ? 'w' : '-') . - (($mode & 0x0040) ? (($mode & 0x0800) ? 's' : 'x' ) : - (($mode & 0x0800) ? 'S' : '-')); - - // group - $sP .= (($mode & 0x0020) ? 'r' : '-') . - (($mode & 0x0010) ? 'w' : '-') . - (($mode & 0x0008) ? (($mode & 0x0400) ? 's' : 'x' ) : - (($mode & 0x0400) ? 'S' : '-')); - - // world - $sP .= (($mode & 0x0004) ? 'r' : '-') . - (($mode & 0x0002) ? 'w' : '-') . - (($mode & 0x0001) ? (($mode & 0x0200) ? 't' : 'x' ) : - (($mode & 0x0200) ? 'T' : '-')); - - return $sP; - } - - /** - * Get the closest mime icon - * - * @param string $mime_type - * @param boolean $et_image =true return $app/$icon string for etemplate (default) or html img tag if false - * @param int $size =128 - * @return string - */ - static function mime_icon($mime_type, $et_image=true, $size=128) - { - if ($mime_type == egw_vfs::DIR_MIME_TYPE) - { - $mime_type = 'Directory'; - } - if(!$mime_type) - { - $mime_type = 'unknown'; - } - $mime_full = strtolower(str_replace ('/','_',$mime_type)); - list($mime_part) = explode('_',$mime_full); - - if (!($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_full)) && - // check mime-alias-map before falling back to more generic icons - !(isset(mime_magic::$mime_alias_map[$mime_type]) && - ($img=common::image('etemplate',$icon='mime'.$size.'_'.str_replace('/','_',mime_magic::$mime_alias_map[$mime_full])))) && - !($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_part))) - { - $img = common::image('etemplate',$icon='mime'.$size.'_unknown'); - } - if ($et_image === 'url') - { - return $img; - } - if ($et_image) - { - return 'etemplate/'.$icon; - } - return html::image('etemplate',$icon,mime_magic::mime2label($mime_type)); - } - - /** - * Human readable size values in k, M or G - * - * @param int $size - * @return string - */ - static function hsize($size) - { - if ($size < 1024) return $size; - if ($size < 1024*1024) return sprintf('%3.1lfk',(float)$size/1024); - if ($size < 1024*1024*1024) return sprintf('%3.1lfM',(float)$size/(1024*1024)); - return sprintf('%3.1lfG',(float)$size/(1024*1024*1024)); - } - - /** - * Size in bytes, from human readable - * - * From PHP ini_get docs, Ivo Mandalski 15-Nov-2011 08:27 - */ - static function int_size($_val) - { - if(empty($_val))return 0; - - $val = trim($_val); - - $matches = null; - preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); - - $last = ''; - if(isset($matches[2])){ - $last = $matches[2]; - } - - if(isset($matches[1])){ - $val = (int) $matches[1]; - } - - switch (strtolower($last)) - { - case 'g': - case 'gb': - $val *= 1024; - case 'm': - case 'mb': - $val *= 1024; - case 'k': - case 'kb': - $val *= 1024; - } - - return (int) $val; - } - - /** - * like basename($path), but also working if the 1. char of the basename is non-ascii - * - * @param string $_path - * @return string - */ - static function basename($_path) - { - list($path) = explode('?',$_path); // remove query - $parts = explode('/',$path); - - return array_pop($parts); - } - - /** - * Get the directory / parent of a given path or url(!), return false for '/'! - * - * Also works around PHP under Windows returning dirname('/something') === '\\', which is NOT understood by EGroupware's VFS! - * - * @param string $_url path or url - * @return string|boolean parent or false if there's none ($path == '/') - */ - static function dirname($_url) - { - list($url,$query) = explode('?',$_url,2); // strip the query first, as it can contain slashes - - if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/') - { - //error_log(__METHOD__."($url) returning FALSE: already in root!"); - return false; - } - $parts = explode('/',$url); - if (substr($url,-1) == '/') array_pop($parts); - array_pop($parts); - if ($url[0] != '/' && count($parts) == 3 || count($parts) == 1 && $parts[0] === '') - { - array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/ - } - //error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : '')); - return implode('/',$parts).($query ? '?'.$query : ''); - } - - /** - * Check if the current use has owner rights for the given path or stat - * - * We define all eGW admins the owner of the group directories! - * - * @param string $path - * @param array $stat =null stat for path, default queried by this function - * @return boolean - */ - static function has_owner_rights($path,array $stat=null) - { - if (!$stat) $stat = self::url_stat($path,0); - - return $stat['uid'] == self::$user || // user is the owner - self::$is_root || // class runs with root rights - !$stat['uid'] && $stat['gid'] && self::$is_admin; // group directory and user is an eGW admin - } - - /** - * Concat a relative path to an url, taking into account, that the url might already end with a slash or the path starts with one or is empty - * - * Also normalizing the path, as the relative path can contain ../ - * - * @param string $_url base url or path, might end in a / - * @param string $relative relative path to add to $url - * @return string - */ - static function concat($_url,$relative) - { - list($url,$query) = explode('?',$_url,2); - if (substr($url,-1) == '/') $url = substr($url,0,-1); - $ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative); - - // now normalize the path (remove "/something/..") - while (strpos($ret,'/../') !== false) - { - list($a_str,$b_str) = explode('/../',$ret,2); - $a = explode('/',$a_str); - array_pop($a); - $b = explode('/',$b_str); - $ret = implode('/',array_merge($a,$b)); - } - return $ret.($query ? (strpos($url,'?')===false ? '?' : '&').$query : ''); - } - - /** - * Build an url from it's components (reverse of parse_url) - * - * @param array $url_parts values for keys 'scheme', 'host', 'user', 'pass', 'query', 'fragment' (all but 'path' are optional) - * @return string - */ - static function build_url(array $url_parts) - { - $url = (!isset($url_parts['scheme'])?'':$url_parts['scheme'].'://'. - (!isset($url_parts['user'])?'':$url_parts['user'].(!isset($url_parts['pass'])?'':':'.$url_parts['pass']).'@'). - $url_parts['host']).$url_parts['path']. - (!isset($url_parts['query'])?'':'?'.$url_parts['query']). - (!isset($url_parts['fragment'])?'':'?'.$url_parts['fragment']); - //error_log(__METHOD__.'('.array2string($url_parts).") = '".$url."'"); - return $url; - } - - /** - * URL to download a file - * - * We use our webdav handler as download url instead of an own download method. - * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. - * - * Please note: If you dont use eTemplate or the html class, you have to run this url throught egw::link() to get a full url - * - * @param string $path - * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! - * @todo get $force_download working through webdav - * @return string - */ - static function download_url($path,$force_download=false) - { - if (($url = self::_call_on_backend('download_url',array($path,$force_download),true))) - { - return $url; - } - if ($path[0] != '/') - { - $path = self::parse_url($path,PHP_URL_PATH); - } - // we do NOT need to encode % itself, as our path are already url encoded, with the exception of ' ' and '+' - // we urlencode double quotes '"', as that fixes many problems in html markup - return '/webdav.php'.strtr($path,array('+' => '%2B',' ' => '%20','"' => '%22')).($force_download ? '?download' : ''); - } - - /** - * Download the given file list as a ZIP - * - * @param array $_files List of files to include in the zip - * @param string $name optional Zip file name. If not provided, it will be determined automatically from the files - * - * @return undefined - */ - public static function download_zip(Array $_files, $name = false) - { - error_log(__METHOD__ . ': '.implode(',',$_files)); - - // Create zip file - $zip_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip'); - - $zip = new ZipArchive(); - if (!$zip->open($zip_file, ZipArchive::OVERWRITE)) - { - throw new egw_exception("Cannot open zip file for writing."); - } - - // Find lowest common directory, to use relative paths - // eg: User selected /home/nathan/picture.jpg, /home/Pictures/logo.jpg - // We want /home - $dirs = array(); - foreach($_files as $file) - { - $dirs[] = self::dirname($file); - } - $paths = array_unique($dirs); - if(count($paths) > 0) - { - // Shortest to longest - usort($paths, function($a, $b) { - return strlen($a) - strlen($b); - }); - - // Start with shortest, pop off sub-directories that don't match - $parts = explode('/',$paths[0]); - foreach($paths as $path) - { - $dirs = explode('/',$path); - foreach($dirs as $dir_index => $dir) - { - if($parts[$dir_index] && $parts[$dir_index] != $dir) - { - unset($parts[$dir_index]); - } - } - } - $base_dir = implode('/', $parts); - } - else - { - $base_dir = $paths[0]; - } - - // Remove 'unsafe' filename characters - // (en.wikipedia.org/wiki/Filename#Reserved_characters_and_words) - $replace = array( - // Linux - '/', - // Windows - '\\','?','%','*',':','|',/*'.',*/ '"','<','>' - ); - - // A nice name for the user, - $filename = $GLOBALS['egw_info']['server']['site_title'] . '_' . - str_replace($replace,'_',( - $name ? $name : ( - count($_files) == 1 ? - // Just one file (hopefully a directory?) selected - self::basename($_files[0]) : - // Use the lowest common directory (eg: Infolog, Open, nathan) - self::basename($base_dir)) - )) . '.zip'; - - // Make sure basename is a dir - if(substr($base_dir, -1) != '/') - { - $base_dir .='/'; - } - - // Go into directories, find them all - $files = self::find($_files); - $links = array(); - - // We need to remove them _after_ we're done - $tempfiles = array(); - - // Give 1 second per file - set_time_limit(count($files)); - - // Add files to archive - foreach($files as &$addfile) - { - // Use relative paths inside zip - $relative = substr($addfile, strlen($base_dir)); - - // Use safe names - replace unsafe chars, convert to ASCII (ZIP spec says CP437, but we'll try) - $path = explode('/',$relative); - $_name = translation::convert(translation::to_ascii(implode('/', str_replace($replace,'_',$path))),false,'ASCII'); - - // Don't go infinite with app entries - if(self::is_link($addfile)) - { - if(in_array($addfile, $links)) continue; - $links[] = $addfile; - } - // Add directory - if empty, client app might not show it though - if(self::is_dir($addfile)) - { - // Zip directories - $zip->addEmptyDir($addfile); - } - else if(self::is_readable($addfile)) - { - // Copy to temp file, as ZipArchive fails to read VFS - $temp = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip_'); - $from = egw_vfs::fopen($addfile,'r'); - $to = fopen($temp,'w'); - if(!stream_copy_to_stream($from,$to) || !$zip->addFile($temp, $_name)) - { - unlink($temp); - trigger_error("Could not add $addfile to ZIP file", E_USER_ERROR); - continue; - } - // Keep temp file until _after_ zipping is done - $tempfiles[] = $temp; - - // Add comment in - $props = self::propfind($addfile); - if($props) - { - $comment = self::find_prop($props,'comment'); - if($comment) - { - $zip->setCommentName($_name, $comment); - } - } - unset($props); - } - } - - // Set a comment to help tell them apart - $zip->setArchiveComment(lang('Created by %1', $GLOBALS['egw_info']['user']['account_lid']) . ' ' .egw_time::to()); - - // Record total for debug, not available after close() - $total_files = $zip->numFiles; - - $result = $zip->close(); - if(!$result || !filesize($zip_file)) - { - error_log('close() result: '.array2string($result)); - return 'Error creating zip file'; - } - - error_log("Total files: " . $total_files . " Peak memory to zip: " . self::hsize(memory_get_peak_usage(true))); - - // Stop any buffering - while(ob_get_level() > 0) - { - ob_end_clean(); - } - - // Stream the file to the client - header("Content-Type: application/zip"); - header("Content-Length: " . filesize($zip_file)); - header("Content-Disposition: attachment; filename=\"$filename\""); - readfile($zip_file); - - unlink($zip_file); - foreach($tempfiles as $temp_file) - { - unlink($temp_file); - } - - // Make sure to exit after, if you don't want to add to the ZIP - } - - /** - * We cache locks within a request, as HTTP_WebDAV_Server generates so many, that it can be a bottleneck - * - * @var array - */ - static protected $lock_cache; - - /** - * Log (to error log) all calls to lock(), unlock() or checkLock() - * - */ - const LOCK_DEBUG = false; - - /** - * lock a ressource/path - * - * @param string $path path or url - * @param string &$token - * @param int &$timeout - * @param string &$owner - * @param string &$scope - * @param string &$type - * @param boolean $update =false - * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes - * @return boolean true on success - */ - static function lock($path,&$token,&$timeout,&$owner,&$scope,&$type,$update=false,$check_writable=true) - { - // we require write rights to lock/unlock a resource - if (!$path || $update && !$token || $check_writable && - !(egw_vfs::is_writable($path) || !egw_vfs::file_exists($path) && egw_vfs::is_writable(egw_vfs::dirname($path)))) - { - return false; - } - // remove the lock info evtl. set in the cache - unset(self::$lock_cache[$path]); - - if ($timeout < 1000000) // < 1000000 is a relative timestamp, so we add the current time - { - $timeout += time(); - } - - if ($update) // Lock Update - { - if (($ret = (boolean)($row = self::$db->select(self::LOCK_TABLE,array('lock_owner','lock_exclusive','lock_write'),array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__)->fetch()))) - { - $owner = $row['lock_owner']; - $scope = egw_db::from_bool($row['lock_exclusive']) ? 'exclusive' : 'shared'; - $type = egw_db::from_bool($row['lock_write']) ? 'write' : 'read'; - - self::$db->update(self::LOCK_TABLE,array( - 'lock_expires' => $timeout, - 'lock_modified' => time(), - ),array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__); - } - } - // HTTP_WebDAV_Server does this check before calling LOCK, but we want to be complete and usable outside WebDAV - elseif(($lock = self::checkLock($path)) && ($lock['scope'] == 'exclusive' || $scope == 'exclusive')) - { - $ret = false; // there's alread a lock - } - else - { - // HTTP_WebDAV_Server sets owner and token, but we want to be complete and usable outside WebDAV - if (!$owner || $owner == 'unknown') - { - $owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; - } - if (!$token) - { - if (strpos(ini_get('include_path'), EGW_API_INC) === false) - { - ini_set('include_path', EGW_API_INC.PATH_SEPARATOR.ini_get('include_path')); - } - require_once('HTTP/WebDAV/Server.php'); - $token = HTTP_WebDAV_Server::_new_locktoken(); - } - try { - self::$db->insert(self::LOCK_TABLE,array( - 'lock_token' => $token, - 'lock_path' => $path, - 'lock_created' => time(), - 'lock_modified' => time(), - 'lock_owner' => $owner, - 'lock_expires' => $timeout, - 'lock_exclusive' => $scope == 'exclusive', - 'lock_write' => $type == 'write', - ),false,__LINE__,__FILE__); - $ret = true; - } - catch(egw_exception_db $e) { - unset($e); - $ret = false; // there's already a lock - } - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$timeout,$owner,$scope,$type,update=$update,check_writable=$check_writable) returns ".($ret ? 'true' : 'false')); - return $ret; - } - - /** - * unlock a ressource/path - * - * @param string $path path to unlock - * @param string $token locktoken - * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes - * @return boolean true on success - */ - static function unlock($path,$token,$check_writable=true) - { - // we require write rights to lock/unlock a resource - if ($check_writable && !egw_vfs::is_writable($path)) - { - return false; - } - if (($ret = self::$db->delete(self::LOCK_TABLE,array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__) && self::$db->affected_rows())) - { - // remove the lock from the cache too - unset(self::$lock_cache[$path]); - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$check_writable) returns ".($ret ? 'true' : 'false')); - return $ret; - } - - /** - * checkLock() helper - * - * @param string resource path to check for locks - * @return array|boolean false if there's no lock, else array with lock info - */ - static function checkLock($path) - { - if (isset(self::$lock_cache[$path])) - { - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$path],true))); - return self::$lock_cache[$path]; - } - $where = 'lock_path='.self::$db->quote($path); - // ToDo: additional check parent dirs for locks and children of the requested directory - //$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%'); - // ToDo: shared locks can return multiple rows - if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch())) - { - $result = egw_db::strip_array_keys($result,'lock_'); - $result['type'] = egw_db::from_bool($result['write']) ? 'write' : 'read'; - $result['scope'] = egw_db::from_bool($result['exclusive']) ? 'exclusive' : 'shared'; - $result['depth'] = egw_db::from_bool($result['recursive']) ? 'infinite' : 0; - } - if ($result && $result['expires'] < time()) // lock is expired --> remove it - { - self::$db->delete(self::LOCK_TABLE,array( - 'lock_path' => $result['path'], - 'lock_token' => $result['token'], - ),__LINE__,__FILE__); - - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed"); - $result = false; - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns ".($result?array2string($result):'false')); - return self::$lock_cache[$path] = $result; - } - - /** - * Get backend specific information (data and etemplate), to integrate as tab in filemanagers settings dialog - * - * @param string $path - * @param array $content =null - * @return array|boolean array with values for keys 'data','etemplate','name','label','help' or false if not supported by backend - */ - static function getExtraInfo($path,array $content=null) - { - $extra = array(); - if (($extra_info = self::_call_on_backend('extra_info',array($path,$content),true))) // true = fail silent if backend does NOT support it - { - $extra[] = $extra_info; - } - - if (($vfs_extra = $GLOBALS['egw']->hooks->process(array( - 'location' => 'vfs_extra', - 'path' => $path, - 'content' => $content, - )))) - { - foreach($vfs_extra as $data) - { - $extra = $extra ? array_merge($extra, $data) : $data; - } - } - return $extra; - } - - /** - * Mapps entries of applications to a path for the locking - * - * @param string $app - * @param int|string $id - * @return string - */ - static function app_entry_lock_path($app,$id) - { - return "/apps/$app/entry/$id"; - } - - /** - * Encoding of various special characters, which can NOT be unencoded in file-names, as they have special meanings in URL's - * - * @var array - */ - static public $encode = array( - //'%' => '%25', // % should be encoded, but easily leads to double encoding, therefore better NOT encodig it - '#' => '%23', - '?' => '%3F', - '/' => '', // better remove it completly - ); - - /** - * Encode a path component: replacing certain chars with their urlencoded counterparts - * - * Not all chars get encoded, slashes '/' are silently removed! - * - * 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 - */ - static public function encodePathComponent($component) - { - return str_replace(array_keys(self::$encode),array_values(self::$encode),$component); - } - - /** - * Encode a path: replacing certain chars with their urlencoded counterparts - * - * To reverse the encoding, eg. to display a filename to the user, you have to use egw_vfs::decodePath() - * - * @param string $path - * @return string - */ - static public function encodePath($path) - { - 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 - */ - static function init_static() - { - // if special user/vfs_user given (eg. from sharing) use it instead default user/account_id - self::$user = (int)(isset($GLOBALS['egw_info']['user']['vfs_user']) ? - $GLOBALS['egw_info']['user']['vfs_user'] : $GLOBALS['egw_info']['user']['account_id']); - self::$is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']); - self::$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; - self::$lock_cache = array(); - } - - /** - * Returns the URL to the thumbnail of the given file. The thumbnail may simply - * be the mime-type icon, or - if activated - the preview with the given thsize. - * - * @param string $file name of the file - * @param int $thsize the size of the preview - false if the default should be used. - * @param string $mime if you already know the mime type of the file, you can supply - * it here. Otherwise supply "false". - */ - public static function thumbnail_url($file, $thsize = false, $mime = false) - { - // Retrive the mime-type of the file - if (!$mime) - { - $mime = egw_vfs::mime_content_type($file); - } - - $image = ""; - - // Seperate the mime type into the primary and the secondary part - list($mime_main, $mime_sub) = explode('/', $mime); - - if ($mime_main == 'egw') - { - $image = common::image($mime_sub, 'navbar'); - } - else if ($file && $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' && - ($stat = egw_vfs::stat($file)) && $stat['size'] < 1500000) - { - if (substr($file, 0, 6) == '/apps/') - { - $file = self::parse_url(egw_vfs::resolve_url_symlinks($file), PHP_URL_PATH); - } - - //Assemble the thumbnail parameters - $thparams = array(); - $thparams['path'] = $file; - if ($thsize) - { - $thparams['thsize'] = $thsize; - } - $image = $GLOBALS['egw']->link('/etemplate/thumbnail.php', $thparams); - } - else - { - list($app, $name) = explode("/", egw_vfs::mime_icon($mime), 2); - $image = common::image($app, $name); - } - - return $image; - } - - /** - * Get the configured start directory for the current user - * - * @return string - */ - static public function get_home_dir() - { - $start = '/home/'.$GLOBALS['egw_info']['user']['account_lid']; - - // check if user specified a valid startpath in his prefs --> use it - if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) && - $path[0] == '/' && egw_vfs::is_dir($path) && egw_vfs::check_access($path, egw_vfs::READABLE)) - { - $start = $path; - } - return $start; - } - - /** - * Copies the files given in $src to $dst. - * - * @param array $src contains the source file - * @param string $dst is the destination directory - */ - static public function copy_files(array $src, $dst, &$errs, array &$copied) - { - if (self::is_dir($dst)) - { - foreach ($src as $file) - { - // Check whether the file has already been copied - prevents from - // recursion - if (!in_array($file, $copied)) - { - // Calculate the target filename - $target = egw_vfs::concat($dst, egw_vfs::basename($file)); - - if (self::is_dir($file)) - { - if ($file !== $target) - { - // Create the target directory - egw_vfs::mkdir($target,null,STREAM_MKDIR_RECURSIVE); - - $copied[] = $file; - $copied[] = $target; // < newly created folder must not be copied again! - if (egw_vfs::copy_files(egw_vfs::find($file), $target, - $errs, $copied)) - { - continue; - } - } - - $errs++; - } - else - { - // Copy a single file - check whether the file should be - // copied onto itself. - // TODO: Check whether target file already exists and give - // return those files so that a dialog might be displayed - // on the client side which lets the user decide. - if ($target !== $file && egw_vfs::copy($file, $target)) - { - $copied[] = $file; - } - else - { - $errs++; - } - } - } - } - } - - return $errs == 0; - } - - /** - * Moves the files given in src to dst - */ - static public function move_files(array $src, $dst, &$errs, array &$moved) - { - if (egw_vfs::is_dir($dst)) - { - foreach($src as $file) - { - $target = egw_vfs::concat($dst, egw_vfs::basename($file)); - - if ($file != $target && egw_vfs::rename($file, $target)) - { - $moved[] = $file; - } - else - { - ++$errs; - } - } - - return $errs == 0; - } - - return false; - } - - /** - * Copy an uploaded file into the vfs, optionally set some properties (eg. comment or other cf's) - * - * Treat copying incl. properties as atomar operation in respect of notifications (one notification about an added file). - * - * @param array|string $src path to uploaded file or etemplate file array (value for key 'tmp_name') - * @param string $target path or directory to copy uploaded file - * @param array|string $props =null array with properties (name => value pairs, eg. 'comment' => 'FooBar','#cfname' => 'something'), - * array as for proppatch (array of array with values for keys 'name', 'val' and optional 'ns') or string with comment - * @param boolean $check_is_uploaded_file =true should method perform an is_uploaded_file check, default yes - * @return boolean|array stat array on success, false on error - */ - static public function copy_uploaded($src,$target,$props=null,$check_is_uploaded_file=true) - { - $tmp_name = is_array($src) ? $src['tmp_name'] : $src; - - if (self::stat($target) && self::is_dir($target)) - { - $target = self::concat($target, self::encodePathComponent(is_array($src) ? $src['name'] : basename($tmp_name))); - } - if ($check_is_uploaded_file && !is_uploaded_file($tmp_name)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !is_uploaded_file()"); - return false; - } - if (!(self::is_writable($target) || self::is_writable(self::dirname($target)))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !writable"); - return false; - } - if ($props) - { - if (!is_array($props)) $props = array(array('name' => 'comment','val' => $props)); - - // if $props is name => value pairs, convert it to internal array or array with values for keys 'name', 'val' and optional 'ns' - if (!isset($props[0])) - { - foreach($props as $name => $val) - { - if (($name == 'comment' || $name[0] == '#') && $val) // only copy 'comment' and cfs - { - $vfs_props[] = array( - 'name' => $name, - 'val' => $val, - ); - } - } - $props = $vfs_props; - } - } - if ($props) - { - // set props before copying the file, so notifications already contain them - if (!self::stat($target)) - { - self::touch($target); // create empty file, to be able to attach properties - self::$treat_as_new = true; // notify as new - } - self::proppatch($target, $props); - } - $ret = copy($tmp_name,self::PREFIX.$target) ? self::stat($target) : false; - if (self::LOG_LEVEL > 1 || !$ret && self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).") returning ".array2string($ret)); - return $ret; - } - - /** - * Compare two files from vfs or local file-system for identical content - * - * VFS files must use URL, to be able to distinguish them eg. from temp. files! - * - * @param string $file1 vfs-url or local path, eg. /tmp/some-file.txt or vfs://default/home/user/some-file.txt - * @param string $file2 -- " -- - * @return boolean true: if files are identical, false: if not or file not found - */ - public static function compare($file1, $file2) - { - if (filesize($file1) != filesize($file2) || - !($fp1 = fopen($file1, 'r')) || !($fp2 = fopen($file2, 'r'))) - { - //error_log(__METHOD__."($file1, $file2) returning FALSE (different size)"); - return false; - } - while (($read1 = fread($fp1, 8192)) !== false && - ($read2 = fread($fp2, 8192)) !== false && - $read1 === $read2 && !feof($fp1) && !feof($fp2)) - { - // just loop until we find a difference - } - - fclose($fp1); - fclose($fp2); - //error_log(__METHOD__."($file1, $file2) returning ".array2string($read1 === $read2)." (content differs)"); - return $read1 === $read2; - } -} - -egw_vfs::init_static(); +class egw_vfs extends Vfs {} diff --git a/phpgwapi/inc/class.iface_stream_wrapper.inc.php b/phpgwapi/inc/class.iface_stream_wrapper.inc.php index 48da3c762a..ed8c778bec 100644 --- a/phpgwapi/inc/class.iface_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.iface_stream_wrapper.inc.php @@ -1,6 +1,6 @@ =') && version_compare(PHP_VERSION,'5.1','<')) - * { - * $eof = !$eof; - * } - * - * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise - */ - function stream_eof ( ); - - /** - * This method is called in response to ftell() calls on the stream. - * - * @return integer current read/write position of the stream - */ - function stream_tell ( ); - - /** - * This method is called in response to fseek() calls on the stream. - * - * You should update the read/write position of the stream according to offset and whence. - * See fseek() for more information about these parameters. - * - * @param integer $offset - * @param integer $whence SEEK_SET - Set position equal to offset bytes - * SEEK_CUR - Set position to current location plus offset. - * SEEK_END - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) - * @return boolean TRUE if the position was updated, FALSE otherwise. - */ - function stream_seek ( $offset, $whence ); - - /** - * This method is called in response to fflush() calls on the stream. - * - * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. - * - * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. - */ - function stream_flush ( ); - - /** - * This method is called in response to fstat() calls on the stream. - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @return array containing the same values as appropriate for the stream. - */ - function stream_stat ( ); - - /** - * This method is called in response to unlink() calls on URL paths associated with the wrapper. - * - * It should attempt to delete the item specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! - * - * @param string $path - * @return boolean TRUE on success or FALSE on failure - */ - static function unlink ( $path ); - - /** - * This method is called in response to rename() calls on URL paths associated with the wrapper. - * - * It should attempt to rename the item specified by path_from to the specification given by path_to. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. - * - * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! - * - * @param string $path_from - * @param string $path_to - * @return boolean TRUE on success or FALSE on failure - */ - static function rename ( $path_from, $path_to ); - - /** - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $path - * @param int $mode - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir ( $path, $mode, $options ); - - /** - * This method is called in response to rmdir() calls on URL paths associated with the wrapper. - * - * It should attempt to remove the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. - * - * @param string $path - * @param int $options Possible values include STREAM_REPORT_ERRORS. - * @return boolean TRUE on success or FALSE on failure. - */ - static function rmdir ( $path, $options ); - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * @param string $path URL that was passed to opendir() and that this object is expected to explore. - * @return booelan - */ - function dir_opendir ( $path, $options ); - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * It should return as many elements in common with the system function as possible. - * Unknown or unavailable values should be set to a rational value (usually 0). - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @param string $path - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @return array - */ - static function url_stat ( $path, $flags ); - - /** - * This method is called in response to readdir(). - * - * It should return a string representing the next filename in the location opened by dir_opendir(). - * - * @return string - */ - function dir_readdir ( ); - - /** - * This method is called in response to rewinddir(). - * - * It should reset the output generated by dir_readdir(). i.e.: - * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). - * - * @return boolean - */ - function dir_rewinddir ( ); - - /** - * This method is called in response to closedir(). - * - * You should release any resources which were locked or allocated during the opening and use of the directory stream. - * - * @return boolean - */ - function dir_closedir ( ); -} +interface iface_stream_wrapper extends Vfs\StreamWrapperIface {} diff --git a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php index eb6a5c0906..da45133c11 100644 --- a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php @@ -7,1911 +7,13 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +use EGroupware\Api\Vfs\Sqlfs; + /** - * EGroupware API: VFS - new DB based VFS stream wrapper - * - * The sqlfs stream wrapper has 2 operation modi: - * - content of files is stored in the filesystem (eGW's files_dir) (default) - * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db) - * please note the current (php5.2.6) problems: - * a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,PDO::PARAM_LOB) does NOT work, string returned) - * (there's a workaround implemented, but it requires to allocate memory for the whole file!) - * b) uploading/writing files > 1M fail on PDOStatement::execute() (setting PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help) - * (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation) - * - * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). - * - * The stream wrapper interface is according to the docu on php.net - * - * @link http://www.php.net/manual/en/function.stream-wrapper-register.php + * @depredated use EGroupware\Api\Vfs\Sqlfs\StreamWrapper */ -class sqlfs_stream_wrapper implements iface_stream_wrapper -{ - /** - * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' - */ - const DIR_MIME_TYPE = 'httpd/unix-directory'; - /** - * Mime type for symlinks - */ - const SYMLINK_MIME_TYPE = 'application/x-symlink'; - /** - * Scheme / protocoll used for this stream-wrapper - */ - const SCHEME = 'sqlfs'; - /** - * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) - */ - const STAT_RETURN_MIME_TYPE = 'mime'; - /** - * Our tablename - */ - const TABLE = 'egw_sqlfs'; - /** - * Name of our property table - */ - const PROPS_TABLE = 'egw_sqlfs_props'; - /** - * mode-bits, which have to be set for files - */ - const MODE_FILE = 0100000; - /** - * mode-bits, which have to be set for directories - */ - const MODE_DIR = 040000; - /** - * mode-bits, which have to be set for links - */ - const MODE_LINK = 0120000; - /** - * How much should be logged to the apache error-log - * - * 0 = Nothing - * 1 = only errors - * 2 = all function calls and errors (contains passwords too!) - * 3 = log line numbers in sql statements - */ - const LOG_LEVEL = 1; - - /** - * We store the content in the DB (no versioning) - */ - const STORE2DB = 1; - /** - * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) - */ - const STORE2FS = 2; - /** - * default for operation, change that if you want to test with STORE2DB atm - */ - const DEFAULT_OPERATION = self::STORE2FS; - - /** - * operation mode of the opened file - * - * @var int - */ - protected $operation = self::DEFAULT_OPERATION; - - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; - - /** - * Path off the file opened by stream_open - * - * @var string - */ - protected $opened_path; - /** - * Mode of the file opened by stream_open - * - * @var int - */ - protected $opened_mode; - /** - * Stream of the opened file, either from the DB via PDO or the filesystem - * - * @var resource - */ - protected $opened_stream; - /** - * fs_id of opened file - * - * @var int - */ - protected $opened_fs_id; - /** - * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls - * - * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat! - * - * @var array $path => info-array pairs - */ - static protected $stat_cache = array(); - /** - * Reference to the PDO object we use - * - * @var PDO - */ - static protected $pdo; - /** - * Array with filenames of dir opened with dir_opendir - * - * @var array - */ - protected $opened_dir; - - /** - * Extra columns added since the intitial introduction of sqlfs - * - * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates - * - * @var string; - */ - static public $extra_columns = ',fs_link'; - - /** - * Clears our stat-cache - * - * Normaly not necessary, as it is automatically cleared/updated, UNLESS egw_vfs::$user changes! - * - * @param string $path ='/' - */ - public static function clearstatcache($path='/') - { - //error_log(__METHOD__."('$path')"); - unset($path); // not used - - self::$stat_cache = array(); - - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null); - } - - /** - * This method is called immediately after your stream object is created. - * - * @param string $url URL that was passed to fopen() and that this object is expected to retrieve - * @param string $mode mode used to open the file, as detailed for fopen() - * @param int $options additional flags set by the streams API (or'ed together): - * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. - * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. - * If this flag is not set, you should not raise any errors. - * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set - * @param array $overwrite_new =null if set create new file with values overwriten by the given ones - * @return boolean true if the ressource was opened successful, otherwise false - */ - function stream_open ( $url, $mode, $options, &$opened_path, array $overwrite_new=null ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - $this->operation = self::url2operation($url); - $dir = egw_vfs::dirname($url); - - $this->opened_path = $opened_path = $path; - $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system - $this->opened_stream = null; - - if (!is_null($overwrite_new) || !($stat = static::url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x') // file not found or file should NOT exist - { - if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) - $mode[0] == 'x' && $stat || // or file should not exist, but does - !($dir_stat=static::url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - // new file --> create it in the DB - $new_file = true; - $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)'; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - $stmt = self::$pdo->prepare($query); - $values = array( - 'fs_name' => egw_vfs::basename($path), - 'fs_dir' => $dir_stat['ino'], - // we use the mode of the dir, so files in group dirs stay accessible by all members - 'fs_mode' => $dir_stat['mode'] & 0666, - // for the uid we use the uid of the dir if not 0=root or the current user otherwise - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : egw_vfs::$user, - // we allways use the group of the dir - 'fs_gid' => $dir_stat['gid'], - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - 'fs_mime' => 'application/octet-stream', // required NOT NULL! - 'fs_size' => 0, - 'fs_active' => self::_pdo_boolean(true), - ); - if ($overwrite_new) $values = array_merge($values,$overwrite_new); - if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'))) - { - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo()); - return false; - } - if ($this->operation == self::STORE2DB) - { - // we buffer all write operations in a temporary file, which get's written on close - $this->opened_stream = tmpfile(); - } - // create the hash-dirs, if they not yet exist - elseif(!file_exists($fs_dir=egw_vfs::dirname(self::_fs_path($this->opened_fs_id)))) - { - $umaskbefore = umask(); - if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); - self::mkdir_recursive($fs_dir,0700,true); - } - } - // check if opend file is a directory - elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - else - { - if ($mode == 'r' && !egw_vfs::check_access($url,egw_vfs::READABLE ,$stat) ||// we are not allowed to read - $mode != 'r' && !egw_vfs::check_access($url,egw_vfs::WRITABLE,$stat)) // or edit it - { - self::_remove_password($url); - $op = $mode == 'r' ? 'read' : 'edited'; - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - $this->opened_fs_id = $stat['ino']; - - if ($this->operation == self::STORE2DB) - { - $stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?'); - $stmt->execute(array($stat['ino'])); - $stmt->bindColumn(1,$this->opened_stream,PDO::PARAM_LOB); - $stmt->fetch(PDO::FETCH_BOUND); - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($this->opened_stream)) - { - $name = md5($url); - $GLOBALS[$name] =& $this->opened_stream; unset($this->opened_stream); - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $this->opened_stream = fopen('global://'.$name,'r'); - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - //echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream); - } - } - // do we operate directly on the filesystem --> open file from there - if ($this->operation == self::STORE2FS) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); - if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) - { - // delete db entry again, if we are not able to open a new(!) file - unset($stmt); - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - $stmt->execute(array('fs_id' => $this->opened_fs_id)); - } - } - if ($mode[0] == 'a') // append modes: a, a+ - { - $this->stream_seek(0,SEEK_END); - } - if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!"); - - return is_resource($this->opened_stream); - } - - /** - * This method is called when the stream is closed, using fclose(). - * - * You must release any resources that were locked or allocated by the stream. - */ - function stream_close ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id) - { - return false; - } - - if ($this->opened_mode != 'r') - { - $this->stream_seek(0,SEEK_END); - - // we need to update the mime-type, size and content (if STORE2DB) - $values = array( - 'fs_size' => $this->stream_tell(), - // todo: analyse the file for the mime-type - 'fs_mime' => mime_magic::filename2mime($this->opened_path), - 'fs_id' => $this->opened_fs_id, - 'fs_modifier' => egw_vfs::$user, - 'fs_modified' => self::_pdo_timestamp(time()), - ); - - if ($this->operation == self::STORE2FS) - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id'); - if (!($ret = $stmt->execute($values))) - { - error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); - } - } - else - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id'); - $this->stream_seek(0,SEEK_SET); // rewind to the start - foreach($values as $name => &$value) - { - $stmt->bindParam($name,$value); - } - $stmt->bindParam('fs_content', $this->opened_stream, PDO::PARAM_LOB); - if (!($ret = $stmt->execute())) - { - error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); - } - } - } - else - { - $ret = true; - } - $ret = fclose($this->opened_stream) && $ret; - - unset(self::$stat_cache[$this->opened_path]); - $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opend_fs_id = null; - $this->operation = self::DEFAULT_OPERATION; - - return $ret; - } - - /** - * This method is called in response to fread() and fgets() calls on the stream. - * - * You must return up-to count bytes of data from the current read/write position as a string. - * If there are less than count bytes available, return as many as are available. - * If no more data is available, return either FALSE or an empty string. - * You must also update the read/write position of the stream by the number of bytes that were successfully read. - * - * @param int $count - * @return string/false up to count bytes read or false on EOF - */ - function stream_read ( $count ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); - - if (is_resource($this->opened_stream)) - { - return fread($this->opened_stream,$count); - } - return false; - } - - /** - * This method is called in response to fwrite() calls on the stream. - * - * You should store data into the underlying storage used by your stream. - * If there is not enough room, try to store as many bytes as possible. - * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. - * You must also update the read/write position of the stream by the number of bytes that were successfully written. - * - * @param string $data - * @return integer - */ - function stream_write ( $data ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); - - if (is_resource($this->opened_stream)) - { - return fwrite($this->opened_stream,$data); - } - return false; - } - - /** - * This method is called in response to feof() calls on the stream. - * - * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! - * - * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) - * { - * $eof = !$eof; - * } - * - * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise - */ - function stream_eof ( ) - { - if (is_resource($this->opened_stream)) - { - return feof($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to ftell() calls on the stream. - * - * @return integer current read/write position of the stream - */ - function stream_tell ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_resource($this->opened_stream)) - { - return ftell($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to fseek() calls on the stream. - * - * You should update the read/write position of the stream according to offset and whence. - * See fseek() for more information about these parameters. - * - * @param integer $offset - * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes - * SEEK_CUR - 1 - Set position to current location plus offset. - * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) - * @return boolean TRUE if the position was updated, FALSE otherwise. - */ - function stream_seek ( $offset, $whence ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); - - if (is_resource($this->opened_stream)) - { - return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure - } - return false; - } - - /** - * This method is called in response to fflush() calls on the stream. - * - * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. - * - * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. - */ - function stream_flush ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_resource($this->opened_stream)) - { - return fflush($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to fstat() calls on the stream. - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @return array containing the same values as appropriate for the stream. - */ - function stream_stat ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); - - return $this->url_stat($this->opened_path,0); - } - - /** - * This method is called in response to unlink() calls on URL paths associated with the wrapper. - * - * It should attempt to delete the item specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! - * - * @param string $url - * @return boolean TRUE on success or FALSE on failure - */ - static function unlink ( $url, $parent_stat=null ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,STREAM_URL_STAT_LINK)) || !egw_vfs::check_access(egw_vfs::dirname($path),egw_vfs::WRITABLE, $parent_stat)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; // no permission or file does not exist - } - if ($stat['mime'] == self::DIR_MIME_TYPE) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!"); - return false; // no permission or file does not exist - } - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - unset(self::$stat_cache[$path]); - - if (($ret = $stmt->execute(array('fs_id' => $stat['ino'])))) - { - if (self::url2operation($url) == self::STORE2FS && - ($stat['mode'] & self::MODE_LINK) != self::MODE_LINK) - { - unlink(self::_fs_path($stat['ino'])); - } - // delete props - unset($stmt); - $stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); - $stmt->execute(array($stat['ino'])); - } - return $ret; - } - - /** - * This method is called in response to rename() calls on URL paths associated with the wrapper. - * - * It should attempt to rename the item specified by path_from to the specification given by path_to. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. - * - * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! - * - * @param string $url_from - * @param string $url_to - * @return boolean TRUE on success or FALSE on failure - */ - static function rename ( $url_from, $url_to) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); - - $path_from = egw_vfs::parse_url($url_from,PHP_URL_PATH); - $from_dir = egw_vfs::dirname($path_from); - $path_to = egw_vfs::parse_url($url_to,PHP_URL_PATH); - $to_dir = egw_vfs::dirname($path_to); - $operation = self::url2operation($url_from); - - // we have to use array($class,'url_stat'), as $class.'::url_stat' requires PHP 5.2.3 and we currently only require 5.2+ - if (!($from_stat = static::url_stat($path_from, 0)) || - !egw_vfs::check_access($from_dir, egw_vfs::WRITABLE, $from_dir_stat = static::url_stat($from_dir, 0))) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); - return false; // no permission or file does not exist - } - if (!egw_vfs::check_access($to_dir, egw_vfs::WRITABLE, $to_dir_stat = static::url_stat($to_dir, 0))) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); - return false; // no permission or parent-dir does not exist - } - // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems - // for our vfs too, we abort here with an error, like the filesystem one does - if (($to_stat = static::url_stat($path_to, 0)) && - ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); - return false; // no permission or file does not exist - } - // if destination file already exists, delete it - if ($to_stat && !static::unlink($url_to,$operation)) - { - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); - return false; - } - unset(self::$stat_cache[$path_from]); - unset(self::$stat_cache[$path_to]); - - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name WHERE fs_dir=:old_dir AND fs_name=:old_name'); - $ok = $stmt->execute(array( - 'fs_dir' => $to_dir_stat['ino'], - 'fs_name' => egw_vfs::basename($path_to), - 'old_dir' => $from_dir_stat['ino'], - 'old_name' => $from_stat['name'], - )); - unset($stmt); - - // check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!) - // fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc - if ($ok && ($new_mime = egw_vfs::mime_content_type($url_to,true)) != egw_vfs::mime_content_type($url_to)) - { - //echo "

egw_vfs::nime_content_type($url_to,true) = $new_mime

\n"; - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id'); - $stmt->execute(array( - 'fs_mime' => $new_mime, - 'fs_id' => $from_stat['ino'], - )); - unset(self::$stat_cache[$path_to]); - } - return $ok; - } - - /** - * due to problems with recursive directory creation, we have our own here - */ - private static function mkdir_recursive($pathname, $mode, $depth=0) - { - $maxdepth=10; - $depth2propagate = (int)$depth + 1; - if ($depth2propagate > $maxdepth) return is_dir($pathname); - is_dir(egw_vfs::dirname($pathname)) || self::mkdir_recursive(egw_vfs::dirname($pathname), $mode, $depth2propagate); - return is_dir($pathname) || @mkdir($pathname, $mode); - } - - /** - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $url - * @param int $mode - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir ( $url, $mode, $options ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); - if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (self::url_stat($path,STREAM_URL_STAT_QUIET)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - //throw new Exception(__METHOD__."('$url',$mode,$options) already exist!"); - trigger_error(__METHOD__."('$url',$mode,$options) already exist!",E_USER_WARNING); - } - return false; - } - $parent_path = egw_vfs::dirname($path); - if (($query = egw_vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'.$query; - $parent = self::url_stat($parent_path,STREAM_URL_STAT_QUIET); - - // check if we should also create all non-existing path components and our parent does not exist, - // if yes call ourself recursive with the parent directory - if (($options & STREAM_MKDIR_RECURSIVE) && $parent_path != '/' && !$parent) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__." creating parents: $parent_path, $mode"); - if (!self::mkdir($parent_path,$mode,$options)) - { - return false; - } - $parent = self::url_stat($parent_path,0); - } - if (!$parent || !egw_vfs::check_access($parent_path,egw_vfs::WRITABLE,$parent)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING); - } - return false; // no permission or file does not exist - } - unset(self::$stat_cache[$path]); - $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - if (($ok = $stmt->execute(array( - 'fs_name' => egw_vfs::basename($path), - 'fs_dir' => $parent['ino'], - 'fs_mode' => $parent['mode'], - 'fs_uid' => $parent['uid'], - 'fs_gid' => $parent['gid'], - 'fs_size' => 0, - 'fs_mime' => self::DIR_MIME_TYPE, - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - )))) - { - // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) - $new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'); - - unset($stmt); // free statement object, on some installs a new prepare fails otherwise! - - $stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE. - ' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name'); - if ($stmt->execute(array( - 'fs_dir' => $parent['ino'], - 'fs_active' => self::_pdo_boolean(true), - 'fs_name' => egw_vfs::basename($path), - )) && $stmt->fetchColumn() > 1) // if there's more then one --> remove our new dir - { - self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id); - } - } - return $ok; - } - - /** - * This method is called in response to rmdir() calls on URL paths associated with the wrapper. - * - * It should attempt to remove the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. - * - * @param string $url - * @param int $options Possible values include STREAM_REPORT_ERRORS. - * @return boolean TRUE on success or FALSE on failure. - */ - static function rmdir ( $url, $options ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - $parent = egw_vfs::dirname($path); - - if (!($stat = self::url_stat($path,0)) || $stat['mime'] != self::DIR_MIME_TYPE || - !egw_vfs::check_access($parent,egw_vfs::WRITABLE)) - { - self::_remove_password($url); - $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : - ($stat['mime'] != self::DIR_MIME_TYPE ? 'not a directory!' : 'permission denied!')); - if (self::LOG_LEVEL) error_log($err_msg); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error($err_msg,E_USER_WARNING); - } - return false; // no permission or file does not exist - } - $stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?'); - $stmt->execute(array($stat['ino'])); - if ($stmt->fetchColumn()) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING); - } - return false; - } - unset(self::$stat_cache[$path]); - unset($stmt); // free statement object, on some installs a new prepare fails otherwise! - - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?'); - if (($ret = $del_stmt->execute(array($stat['ino'])))) - { - self::eacl($path,null,false,$stat['ino']); // remove all (=false) evtl. existing extended acl for that dir - // delete props - unset($del_stmt); - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); - $del_stmt->execute(array($stat['ino'])); - } - return $ret; - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * @param string $url - * @param int $time =null modification time (unix timestamp), default null = current time - * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs! - */ - static function touch($url,$time=null,$atime=null) - { - unset($atime); // not used - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $time)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,STREAM_URL_STAT_QUIET))) - { - // file does not exist --> create an empty one - if (!($f = fopen(self::SCHEME.'://default'.$path,'w')) || !fclose($f)) - { - return false; - } - if (is_null($time)) - { - return true; // new (empty) file created with current mod time - } - $stat = self::url_stat($path,0); - } - unset(self::$stat_cache[$path]); - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified,fs_modifier=:fs_modifier WHERE fs_id=:fs_id'); - - return $stmt->execute(array( - 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), - 'fs_modifier' => egw_vfs::$user, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chown command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $owner - * @return boolean - */ - static function chown($url,$owner) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::$is_root) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only root can do that!"); - trigger_error("Only root can do that!",E_USER_WARNING); - return false; - } - if ($owner < 0 || $owner && !$GLOBALS['egw']->accounts->id2name($owner)) // not a user (0 == root) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) user id!"); - trigger_error(__METHOD__."($url,$owner) Unknown (numeric) user id!",E_USER_WARNING); - //throw new Exception(__METHOD__."($url,$owner) Unknown (numeric) user id!"); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_id=:fs_id'); - - // update stat-cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_uid'] = $owner; - - return $stmt->execute(array( - 'fs_uid' => (int) $owner, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chgrp command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $owner - * @return boolean - */ - static function chgrp($url,$owner) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::has_owner_rights($path,$stat)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only owner or root can do that!"); - trigger_error("Only owner or root can do that!",E_USER_WARNING); - return false; - } - if ($owner < 0) $owner = -$owner; // sqlfs uses a positiv group id's! - - if ($owner && !$GLOBALS['egw']->accounts->id2name(-$owner)) // not a group - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) group id!"); - trigger_error("Unknown (numeric) group id!",E_USER_WARNING); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_gid=:fs_gid WHERE fs_id=:fs_id'); - - // update stat-cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_gid'] = $owner; - - return $stmt->execute(array( - 'fs_gid' => $owner, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chmod command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $mode - * @return boolean - */ - static function chmod($url,$mode) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $mode)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::has_owner_rights($path,$stat)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) only owner or root can do that!"); - trigger_error("Only owner or root can do that!",E_USER_WARNING); - return false; - } - if (!is_numeric($mode)) // not a mode - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no (numeric) mode!"); - trigger_error("No (numeric) mode!",E_USER_WARNING); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); - - // update stat cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_mode'] = ((int) $mode) & 0777; - - return $stmt->execute(array( - 'fs_mode' => ((int) $mode) & 0777, // we dont store the file and dir bits, give int overflow! - 'fs_id' => $stat['ino'], - )); - } - - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * @param string $url URL that was passed to opendir() and that this object is expected to explore. - * @param int $options - * @return booelan - */ - function dir_opendir ( $url, $options ) - { - $this->opened_dir = null; - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($url,0)) || // dir not found - $stat['mime'] != self::DIR_MIME_TYPE || // no dir - !egw_vfs::check_access($url,egw_vfs::EXECUTABLE|egw_vfs::READABLE,$stat)) // no access - { - self::_remove_password($url); - $msg = $stat['mime'] != self::DIR_MIME_TYPE ? "$url is no directory" : 'permission denied'; - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!"); - $this->opened_dir = null; - return false; - } - $this->opened_dir = array(); - $query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. - ' FROM '.self::TABLE.' WHERE fs_dir=? AND fs_active='.self::_pdo_boolean(true). - " ORDER BY fs_mime='httpd/unix-directory' DESC, fs_name ASC"; - //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$options)".' */ '.$query; - - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - if ($stmt->execute(array($stat['ino']))) - { - foreach($stmt as $file) - { - $this->opened_dir[] = $file['fs_name']; - self::$stat_cache[egw_vfs::concat($path,$file['fs_name'])] = $file; - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options): ".implode(', ',$this->opened_dir)); - reset($this->opened_dir); - - return true; - } - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * It should return as many elements in common with the system function as possible. - * Unknown or unavailable values should be set to a rational value (usually 0). - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @param string $url - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @param boolean $eacl_access =null allows extending classes to pass the value of their check_extended_acl() method (no lsb!) - * @return array - */ - static function url_stat ( $url, $flags, $eacl_access=null ) - { - static $max_subquery_depth=null; - if (is_null($max_subquery_depth)) - { - $max_subquery_depth = $GLOBALS['egw_info']['server']['max_subquery_depth']; - if (!$max_subquery_depth) $max_subquery_depth = 7; // setting current default of 7, if nothing set - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags,$eacl_access)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise - if ($path != '/' && substr($path,-1) == '/') - { - $path = substr($path,0,-1); - } - if (empty($path)) - { - return false; // is invalid and gives sql error - } - // check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db - if (self::$stat_cache && isset(self::$stat_cache[$path]) && (is_null($eacl_access) || self::$stat_cache[$path] !== false)) - { - return self::$stat_cache[$path] ? self::_vfsinfo2stat(self::$stat_cache[$path]) : false; - } - - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. - ' FROM '.self::TABLE.' WHERE fs_active='.self::_pdo_boolean(true). - ' AND fs_name'.self::$case_sensitive_equal.'? AND fs_dir='; - $parts = explode('/',$path); - - // if we have extendes acl access to the url, we dont need and can NOT include the sql for the readable check - if (is_null($eacl_access)) - { - $eacl_access = self::check_extended_acl($path,egw_vfs::READABLE); // should be static::check_extended_acl, but no lsb! - } - - try { - foreach($parts as $n => $name) - { - if ($n == 0) - { - $query = (int) ($path != '/'); // / always has fs_id == 1, no need to query it ($path=='/' needs fs_dir=0!) - } - elseif ($n < count($parts)-1) - { - // MySQL 5.0 has a nesting limit for subqueries - // --> we replace the so far cumulated subqueries with their result - // no idea about the other DBMS, but this does NOT hurt ... - // --> depth limit of subqueries is now dynamicly decremented in catch - if ($n > 1 && !(($n-1) % $max_subquery_depth) && !($query = self::$pdo->query($query)->fetchColumn())) - { - if (self::LOG_LEVEL > 1) - { - self::_remove_password($url); - error_log(__METHOD__."('$url',$flags) file or directory not found!"); - } - // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) - return self::$stat_cache[$path] = false; - } - $query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_active='. - self::_pdo_boolean(true).' AND fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name); - - // if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights) - if (!egw_vfs::$is_root && !$eacl_access) - { - if (!egw_vfs::$user) - { - self::_remove_password($url); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!"); - return false; - } - $query .= ' AND '.self::_sql_readable(); - } - } - else - { - $query = str_replace('fs_name'.self::$case_sensitive_equal.'?','fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name),$base_query).'('.$query.')'; - } - } - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$flags,$eacl_access)".' */ '.$query; - //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(PDO::FETCH_ASSOC))) - { - if (self::LOG_LEVEL > 1) - { - self::_remove_password($url); - error_log(__METHOD__."('$url',$flags) file or directory not found!"); - } - // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) - return self::$stat_cache[$path] = false; - } - } - catch (PDOException $e) { - // decrement subquery limit by 1 and try again, if not already smaller then 3 - if ($max_subquery_depth < 3) - { - throw new egw_exception_db($e->getMessage()); - } - $GLOBALS['egw_info']['server']['max_subquery_depth'] = --$max_subquery_depth; - error_log(__METHOD__."() decremented max_subquery_depth to $max_subquery_depth"); - config::save_value('max_subquery_depth', $max_subquery_depth, 'phpgwapi'); - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache(); - return self::url_stat($url, $flags, $eacl_access); - } - self::$stat_cache[$path] = $info; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info)); - return self::_vfsinfo2stat($info); - } - - /** - * Return readable check as sql (to be AND'ed into the query), only use if !egw_vfs::$is_root - * - * @return string - */ - protected function _sql_readable() - { - static $sql_read_acl=null; - - if (is_null($sql_read_acl)) - { - foreach($GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true) as $gid) - { - $memberships[] = abs($gid); // sqlfs stores the gid's positiv - } - // using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400) - // 256 = 0400, 32 = 040 - $sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)egw_vfs::$user. - ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; - //error_log(__METHOD__."() egw_vfs::\$user=".array2string(egw_vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); - } - return $sql_read_acl; - } - - /** - * This method is called in response to readdir(). - * - * It should return a string representing the next filename in the location opened by dir_opendir(). - * - * @return string - */ - function dir_readdir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - $file = current($this->opened_dir); next($this->opened_dir); - - return $file; - } - - /** - * This method is called in response to rewinddir(). - * - * It should reset the output generated by dir_readdir(). i.e.: - * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). - * - * @return boolean - */ - function dir_rewinddir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - reset($this->opened_dir); - - return true; - } - - /** - * This method is called in response to closedir(). - * - * You should release any resources which were locked or allocated during the opening and use of the directory stream. - * - * @return boolean - */ - function dir_closedir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - $this->opened_dir = null; - - return true; - } - - /** - * This method is called in response to readlink(). - * - * The readlink value is read by url_stat or dir_opendir and therefore cached in the stat-cache. - * - * @param string $path - * @return string|boolean content of the symlink or false if $url is no symlink (or not found) - */ - static function readlink($path) - { - $link = !($lstat = self::url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); - - return $link; - } - - /** - * Method called for symlink() - * - * @param string $target - * @param string $link - * @return boolean true on success false on error - */ - static function symlink($target,$link) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); - - if (self::url_stat($link,0) || !($dir = egw_vfs::dirname($link)) || - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!stat('$link') || !is_writable('$dir'))"); - return false; // $link already exists or parent dir does not - } - $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_link'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_link)'; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - $stmt = self::$pdo->prepare($query); - unset(self::$stat_cache[egw_vfs::parse_url($link,PHP_URL_PATH)]); - - return !!$stmt->execute(array( - 'fs_name' => egw_vfs::basename($link), - 'fs_dir' => $dir_stat['ino'], - 'fs_mode' => ($dir_stat['mode'] & 0666), - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : egw_vfs::$user, - 'fs_gid' => $dir_stat['gid'], - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - 'fs_mime' => self::SYMLINK_MIME_TYPE, - 'fs_size' => bytes($target), - 'fs_link' => $target, - )); - } - - private static $extended_acl; - - /** - * Check if extendes ACL (stored in eGW's ACL table) grants access - * - * The extended ACL is inherited, so it's valid for all subdirs and the included files! - * The used algorithm break on the first match. It could be used, to disallow further access. - * - * @param string $url url to check - * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable - * @return boolean - */ - static function check_extended_acl($url,$check) - { - $url_path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (is_null(self::$extended_acl)) - { - self::_read_extended_acl(); - } - $access = false; - foreach(self::$extended_acl as $path => $rights) - { - if ($path == $url_path || substr($url_path,0,strlen($path)+1) == $path.'/') - { - $access = ($rights & $check) == $check; - break; - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$check) ".($access?"access granted by $path=$rights":'no access!!!')); - return $access; - } - - /** - * Read the extended acl via acl::get_grants('sqlfs') - * - */ - static protected function _read_extended_acl() - { - if ((self::$extended_acl = egw_cache::getSession(self::EACL_APPNAME, 'extended_acl'))) - { - return; // ext. ACL read from session. - } - self::$extended_acl = array(); - if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(egw_vfs::$user,self::EACL_APPNAME))) - { - $pathes = self::id2path(array_keys($rights)); - } - foreach($rights as $fs_id => $right) - { - $path = $pathes[$fs_id]; - if (isset($path)) - { - self::$extended_acl[$path] = (int)$right; - } - } - // sort by length descending, to allow more specific pathes to have precedence - uksort(self::$extended_acl, function($a,$b) { - return strlen($b)-strlen($a); - }); - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() '.array2string(self::$extended_acl)); - } - - /** - * Appname used with the acl class to store the extended acl - */ - const EACL_APPNAME = 'sqlfs'; - - /** - * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) - * - * Only root, the owner of the path or an eGW admin (only if there's no owner but a group) are allowd to set eACL's! - * - * @param string $path string with path - * @param int $rights =null rights to set, or null to delete the entry - * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted) - * @return boolean true if acl is set/deleted, false on error - */ - static function eacl($path,$rights=null,$owner=null,$fs_id=null) - { - if ($path[0] != '/') - { - $path = egw_vfs::parse_url($path,PHP_URL_PATH); - } - if (is_null($fs_id)) - { - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) no such file or directory!"); - return false; // $path not found - } - if (!egw_vfs::has_owner_rights($path,$stat)) // not group dir and user is eGW admin - { - if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) permission denied!"); - return false; // permission denied - } - $fs_id = $stat['ino']; - } - if (is_null($owner)) - { - $owner = egw_vfs::$user; - } - if (is_null($rights) || $owner === false) - { - // delete eacl - if (is_null($owner) || $owner == egw_vfs::$user || - $owner < 0 && egw_vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true))) - { - self::$extended_acl = null; // force new read of eACL, as there could be multiple eACL for that path - } - $ret = $GLOBALS['egw']->acl->delete_repository(self::EACL_APPNAME,$fs_id,(int)$owner); - } - else - { - if (isset(self::$extended_acl) && ($owner == egw_vfs::$user || - $owner < 0 && egw_vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true)))) - { - // set rights for this class, if applicable - self::$extended_acl[$path] |= $rights; - } - $ret = $GLOBALS['egw']->acl->add_repository(self::EACL_APPNAME,$fs_id,$owner,$rights); - } - if ($ret) - { - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$rights,$owner,$fs_id)=".(int)$ret); - return $ret; - } - - /** - * Get all ext. ACL set for a path - * - * Calls itself recursive, to get the parent directories - * - * @param string $path - * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found - */ - function get_eacl($path) - { - if (!($stat = static::url_stat($path, STREAM_URL_STAT_QUIET))) - { - error_log(__METHOD__.__LINE__.' '.array2string($path).' not found!'); - return false; // not found - } - $eacls = array(); - foreach($GLOBALS['egw']->acl->get_all_rights($stat['ino'],self::EACL_APPNAME) as $owner => $rights) - { - $eacls[] = array( - 'path' => $path, - 'owner' => $owner, - 'rights' => $rights, - 'ino' => $stat['ino'], - ); - } - if (($path = egw_vfs::dirname($path))) - { - $eacls = array_merge((array)self::get_eacl($path),$eacls); - } - // sort by length descending, to show precedence - usort($eacls, function($a, $b) { - return strlen($b['path']) - strlen($a['path']); - }); - //error_log(__METHOD__."('$_path') returning ".array2string($eacls)); - return $eacls; - } - - /** - * Return the path of given fs_id(s) - * - * Searches the stat_cache first and then the db. - * Calls itself recursive to to determine the path of the parent/directory - * - * @param int|array $fs_ids integer fs_id or array of them - * @return string|array path or array or pathes indexed by fs_id - */ - static function id2path($fs_ids) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')'); - $ids = (array)$fs_ids; - $pathes = array(); - // first check our stat-cache for the ids - foreach(self::$stat_cache as $path => $stat) - { - if (($key = array_search($stat['fs_id'],$ids)) !== false) - { - $pathes[$stat['fs_id']] = $path; - unset($ids[$key]); - if (!$ids) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes).' *from stat_cache*'); - return is_array($fs_ids) ? $pathes : array_shift($pathes); - } - } - } - // now search via the database - if (count($ids) > 1) array_map(function(&$v) { $v = (int)$v; },$ids); - $query = 'SELECT fs_id,fs_dir,fs_name FROM '.self::TABLE.' WHERE fs_id'. - (count($ids) == 1 ? '='.(int)$ids[0] : ' IN ('.implode(',',$ids).')'); - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - if (!$stmt->execute()) - { - return false; // not found - } - $parents = array(); - foreach($stmt as $row) - { - if ($row['fs_dir'] > 1 && !in_array($row['fs_dir'],$parents)) - { - $parents[] = $row['fs_dir']; - } - $rows[$row['fs_id']] = $row; - } - unset($stmt); - - if ($parents && !($parents = self::id2path($parents))) - { - return false; // parent not found, should never happen ... - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__." trying foreach with:".print_r($rows,true)."#"); - foreach((array)$rows as $fs_id => $row) - { - $parent = $row['fs_dir'] > 1 ? $parents[$row['fs_dir']] : ''; - - $pathes[$fs_id] = $parent . '/' . $row['fs_name']; - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes)); - return is_array($fs_ids) ? $pathes : array_shift($pathes); - } - - /** - * Convert a sqlfs-file-info into a stat array - * - * @param array $info - * @return array - */ - static protected function _vfsinfo2stat($info) - { - $stat = array( - 'ino' => $info['fs_id'], - 'name' => $info['fs_name'], - 'mode' => $info['fs_mode'] | - ($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR : - ($info['fs_mime'] == self::SYMLINK_MIME_TYPE ? self::MODE_LINK : self::MODE_FILE)), // required by the stream wrapper - 'size' => $info['fs_size'], - 'uid' => $info['fs_uid'], - 'gid' => $info['fs_gid'], - 'mtime' => strtotime($info['fs_modified']), - 'ctime' => strtotime($info['fs_created']), - 'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1, - // eGW addition to return some extra values - 'mime' => $info['fs_mime'], - 'readlink' => $info['fs_link'], - ); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat)); - return $stat; - } - - public static $pdo_type; - /** - * Case sensitive comparison operator, for mysql we use ' COLLATE utf8_bin =' - * - * @var string - */ - public static $case_sensitive_equal = '='; - - /** - * Reconnect to database - */ - static public function reconnect() - { - self::$pdo = self::_pdo(); - } - - /** - * Create pdo object / connection, as long as pdo is not generally used in eGW - * - * @return PDO - */ - static protected function _pdo() - { - $egw_db = isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; - - switch($egw_db->Type) - { - case 'mysqli': - case 'mysqlt': - case 'mysql': - self::$case_sensitive_equal = '= BINARY '; - self::$pdo_type = 'mysql'; - break; - default: - self::$pdo_type = $egw_db->Type; - break; - } - $dsn = self::$pdo_type.':dbname='.$egw_db->Database.($egw_db->Host ? ';host='.$egw_db->Host.($egw_db->Port ? ';port='.$egw_db->Port : '') : ''); - // check once if pdo extension and DB specific driver is loaded or can be loaded - static $pdo_available=null; - if (is_null($pdo_available)) - { - foreach(array('pdo','pdo_'.self::$pdo_type) as $ext) - { - check_load_extension($ext,true); // true = throw Exception - } - $pdo_available = true; - } - try { - self::$pdo = new PDO($dsn,$egw_db->User,$egw_db->Password,array( - PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, - )); - } - catch(Exception $e) - { - unset($e); - // Exception reveals password, so we ignore the exception and connect again without pw, to get the right exception without pw - self::$pdo = new PDO($dsn,$egw_db->User,'$egw_db->Password'); - } - // set client charset of the connection - $charset = translation::charset(); - switch(self::$pdo_type) - { - case 'mysql': - if (isset($egw_db->Link_ID->charset2mysql[$charset])) $charset = $egw_db->Link_ID->charset2mysql[$charset]; - // fall throught - case 'pgsql': - $query = "SET NAMES '$charset'"; - break; - } - if ($query) - { - self::$pdo->exec($query); - } - return self::$pdo; - } - - /** - * Just a little abstration 'til I know how to organise stuff like that with PDO - * - * @param mixed $time - * @return string Y-m-d H:i:s - */ - static protected function _pdo_timestamp($time) - { - if (is_numeric($time)) - { - $time = date('Y-m-d H:i:s',$time); - } - return $time; - } - - /** - * Just a little abstration 'til I know how to organise stuff like that with PDO - * - * @param boolean $val - * @return string '1' or '0' for mysql, 'true' or 'false' for everyone else - */ - static protected function _pdo_boolean($val) - { - if (self::$pdo_type == 'mysql') - { - return $val ? '1' : '0'; - } - return $val ? 'true' : 'false'; - } - - /** - * Maximum value for a single hash element (should be 10^N): 10, 100 (default), 1000, ... - * - * DONT change this value, once you have files stored, they will no longer be found! - */ - const HASH_MAX = 100; - - /** - * Return the path of the stored content of a file if $this->operation == self::STORE2FS - * - * To limit the number of files stored in one directory, we create a hash from the fs_id: - * 1 --> /00/1 - * 34 --> /00/34 - * 123 --> /01/123 - * 4567 --> /45/4567 - * 99999 --> /09/99/99999 - * --> so one directory contains maximum 2 * HASH_MAY entries (HASH_MAX dirs + HASH_MAX files) - * @param int $id id of the file - * @return string - */ - static function _fs_path($id) - { - if (!is_numeric($id)) - { - throw new egw_exception_wrong_parameter(__METHOD__."(id=$id) id has to be an integer!"); - } - if (!isset($GLOBALS['egw_info']['server']['files_dir'])) - { - if (is_object($GLOBALS['egw_setup']->db)) // if we run under setup, query the db for the files dir - { - $GLOBALS['egw_info']['server']['files_dir'] = $GLOBALS['egw_setup']->db->select('egw_config','config_value',array( - 'config_name' => 'files_dir', - 'config_app' => 'phpgwapi', - ),__LINE__,__FILE__)->fetchColumn(); - } - } - if (!$GLOBALS['egw_info']['server']['files_dir']) - { - throw new egw_exception_assertion_failed("\$GLOBALS['egw_info']['server']['files_dir'] not set!"); - } - $hash = array(); - $n = $id; - while(($n = (int) ($n / self::HASH_MAX))) - { - $hash[] = sprintf('%02d',$n % self::HASH_MAX); - } - if (!$hash) $hash[] = '00'; // we need at least one directory, to not conflict with the dir-names - array_unshift($hash,$id); - - $path = '/sqlfs/'.implode('/',array_reverse($hash)); - //error_log(__METHOD__."($id) = '$path'"); - return $GLOBALS['egw_info']['server']['files_dir'].$path; - } - - /** - * Replace the password of an url with '...' for error messages - * - * @param string &$url - */ - static protected function _remove_password(&$url) - { - $parts = egw_vfs::parse_url($url); - - if ($parts['pass'] || $parts['scheme']) - { - $url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : ''). - $parts['host'].$parts['path']; - } - } - - /** - * Get storage mode from url (get parameter 'storage', eg. ?storage=db) - * - * @param string|array $url complete url or array of url-parts from parse_url - * @return int self::STORE2FS or self::STORE2DB - */ - static function url2operation($url) - { - $operation = self::DEFAULT_OPERATION; - - if (strpos(is_array($url) ? $url['query'] : $url,'storage=') !== false) - { - $query = null; - parse_str(is_array($url) ? $url['query'] : egw_vfs::parse_url($url,PHP_URL_QUERY), $query); - switch ($query['storage']) - { - case 'db': - $operation = self::STORE2DB; - break; - case 'fs': - default: - $operation = self::STORE2FS; - break; - } - } - //error_log(__METHOD__."('$url') = $operation (1=DB, 2=FS)"); - return $operation; - } - - /** - * Store properties for a single ressource (file or dir) - * - * @param string|int $path string with path or integer fs_id - * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) - * @return boolean true if props are updated, false otherwise (eg. ressource not found) - */ - static function proppatch($path,array $props) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); - if (!is_numeric($path)) - { - if (!($stat = self::url_stat($path,0))) - { - return false; - } - $id = $stat['ino']; - } - elseif(!($path = self::id2path($id=$path))) - { - return false; - } - if (!egw_vfs::check_access($path,EGW_ACL_EDIT,$stat)) - { - return false; // permission denied - } - $ins_stmt = $del_stmt = null; - foreach($props as &$prop) - { - if (!isset($prop['ns'])) $prop['ns'] = egw_vfs::DEFAULT_PROP_NAMESPACE; - - if (!isset($prop['val']) || self::$pdo_type != 'mysql') // for non mysql, we have to delete the prop anyway, as there's no REPLACE! - { - if (!isset($del_stmt)) - { - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id AND prop_namespace=:prop_namespace AND prop_name=:prop_name'); - } - $del_stmt->execute(array( - 'fs_id' => $id, - 'prop_namespace' => $prop['ns'], - 'prop_name' => $prop['name'], - )); - } - if (isset($prop['val'])) - { - if (!isset($ins_stmt)) - { - $ins_stmt = self::$pdo->prepare((self::$pdo_type == 'mysql' ? 'REPLACE' : 'INSERT'). - ' INTO '.self::PROPS_TABLE.' (fs_id,prop_namespace,prop_name,prop_value) VALUES (:fs_id,:prop_namespace,:prop_name,:prop_value)'); - } - if (!$ins_stmt->execute(array( - 'fs_id' => $id, - 'prop_namespace' => $prop['ns'], - 'prop_name' => $prop['name'], - 'prop_value' => $prop['val'], - ))) - { - return false; - } - } - } - return true; - } - - /** - * Read properties for a ressource (file, dir or all files of a dir) - * - * @param array|string|int $path_ids (array of) string with path or integer fs_id - * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, use null for all - * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or - * fs_id/path => array of props for $depth==1 or is_array($path_ids) - */ - static function propfind($path_ids,$ns=egw_vfs::DEFAULT_PROP_NAMESPACE) - { - $ids = is_array($path_ids) ? $path_ids : array($path_ids); - foreach($ids as &$id) - { - if (!is_numeric($id)) - { - if (!($stat = self::url_stat($id,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); - return false; - } - $id = $stat['ino']; - } - } - if (count($ids) >= 1) array_map(function(&$v) { $v = (int)$v; },$ids); - $query = 'SELECT * FROM '.self::PROPS_TABLE.' WHERE (fs_id'. - (count($ids) == 1 ? '='.(int)implode('',$ids) : ' IN ('.implode(',',$ids).')').')'. - (!is_null($ns) ? ' AND prop_namespace=?' : ''); - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - $stmt->execute(!is_null($ns) ? array($ns) : array()); - - $props = array(); - foreach($stmt as $row) - { - $props[$row['fs_id']][] = array( - 'val' => $row['prop_value'], - 'name' => $row['prop_name'], - 'ns' => $row['prop_namespace'], - ); - } - if (!is_array($path_ids)) - { - $props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array(); // return empty array for no props - } - elseif ($props && isset($stat)) // need to map fs_id's to pathes - { - foreach(self::id2path(array_keys($props)) as $id => $path) - { - $props[$path] =& $props[$id]; - unset($props[$id]); - } - } - if (self::LOG_LEVEL > 1) - { - foreach((array)$props as $k => $v) - { - error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v)); - } - } - return $props; - } -} - -stream_register_wrapper(sqlfs_stream_wrapper::SCHEME ,'sqlfs_stream_wrapper'); +class sqlfs_stream_wrapper extends Sqlfs\StreamWrapper {} diff --git a/phpgwapi/inc/class.sqlfs_utils.inc.php b/phpgwapi/inc/class.sqlfs_utils.inc.php index 16911912a6..5533aac921 100644 --- a/phpgwapi/inc/class.sqlfs_utils.inc.php +++ b/phpgwapi/inc/class.sqlfs_utils.inc.php @@ -7,474 +7,13 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ -require_once 'class.iface_stream_wrapper.inc.php'; -require_once 'class.sqlfs_stream_wrapper.inc.php'; +use EGroupware\Api\Vfs\Sqlfs; /** - * sqlfs stream wrapper utilities: migration db-fs, fsck + * @depredated use EGroupware\Api\Vfs\Sqlfs\Utils */ -class sqlfs_utils extends sqlfs_stream_wrapper -{ - /** - * Migrate SQLFS content from DB to filesystem - * - * @param boolean $debug true to echo a message for each copied file - */ - static function migrate_db2fs($debug=false) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. - ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; - - $fs_id = $fs_name = $fs_size = $fs_content = null; - $stmt = self::$pdo->prepare($query); - $stmt->bindColumn(1,$fs_id); - $stmt->bindColumn(2,$fs_name); - $stmt->bindColumn(3,$fs_size); - $stmt->bindColumn(4,$fs_content,PDO::PARAM_LOB); - - if ($stmt->execute()) - { - $n = 0; - foreach($stmt as $row) - { - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($fs_content)) - { - $name = md5($fs_name.$fs_id); - $GLOBALS[$name] =& $fs_content; - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $content = fopen('global://'.$name,'r'); - if (!$content) echo "fopen('global://$name','w' failed, strlen(\$GLOBALS['$name'])=".strlen($GLOBALS[$name]).", \$GLOBALS['$name']=".substr($GLOBALS['$name'],0,100)."...\n"; - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - else - { - $content = $fs_content; - } - if (!is_resource($content)) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); - } - $filename = self::_fs_path($fs_id); - if (!file_exists($fs_dir=egw_vfs::dirname($filename))) - { - self::mkdir_recursive($fs_dir,0700,true); - } - if (!($dest = fopen($filename,'w'))) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); - } - if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); - } - if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; - fclose($dest); - fclose($content); unset($content); - - ++$n; - unset($row); // not used, as we access bound variables - } - unset($stmt); - - if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) - { - $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; - $stmt = self::$pdo->prepare($query); - $stmt->execute(); - } - } - return $n; - } - - /** - * Check and optionaly fix corruption in sqlfs - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - public static function fsck($check_only=true) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $msgs = array(); - foreach(array( - self::fsck_fix_required_nodes($check_only), - self::fsck_fix_multiple_active($check_only), - self::fsck_fix_unconnected($check_only), - self::fsck_fix_no_content($check_only), - ) as $check_msgs) - { - if ($check_msgs) $msgs = array_merge($msgs, $check_msgs); - } - - foreach ($GLOBALS['egw']->hooks->process(array( - 'location' => 'fsck', - 'check_only' => $check_only) - ) as $app_msgs) - { - if ($app_msgs) $msgs = array_merge($msgs, $app_msgs); - } - return $msgs; - } - - /** - * Check and optionally create required nodes: /, /home, /apps - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_required_nodes($check_only=true) - { - static $dirs = array( - '/' => 1, - '/home' => 2, - '/apps' => 3, - ); - $stmt = $delete_stmt = null; - $msgs = array(); - foreach($dirs as $path => $id) - { - if (!($stat = self::url_stat($path, STREAM_URL_STAT_LINK))) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" not found!', $path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_id,fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. - ') VALUES (:fs_id,:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - } - try { - $ok = $stmt->execute($data = array( - 'fs_id' => $id, - 'fs_name' => substr($path,1), - 'fs_dir' => $path == '/' ? 0 : $dirs['/'], - 'fs_mode' => 05, - 'fs_uid' => 0, - 'fs_gid' => 0, - 'fs_size' => 0, - 'fs_mime' => 'httpd/unix-directory', - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => 0, - )); - } - catch (PDOException $e) - { - $ok = false; - unset($e); // ignore exception - } - if (!$ok) // can not insert it, try deleting it first - { - if (!isset($delete_stmt)) - { - $delete_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - } - try { - $ok = $delete_stmt->execute(array('fs_id' => $id)) && $stmt->execute($data); - } - catch (PDOException $e) - { - unset($e); // ignore exception - } - } - $msgs[] = $ok ? lang('Required directory "%1" created.', $path) : - lang('Failed to create required directory "%1"!', $path); - } - } - // check if directory is at least world readable and executable (r-x), we allow more but not less - elseif (($stat['mode'] & 05) != 05) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" has wrong mode %2 instead of %3!', - $path, egw_vfs::int2mode($stat['mode']), egw_vfs::int2mode(05|0x4000)); - } - else - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); - if (($ok = $stmt->execute(array( - 'fs_id' => $id, - 'fs_mode' => 05, - )))) - { - $msgs[] = lang('Mode of required directory "%1" changed to %2.', $path, egw_vfs::int2mode(05|0x4000)); - } - else - { - $msgs[] = lang('Failed to change mode of required directory "%1" to %2!', $path, egw_vfs::int2mode(05|0x4000)); - } - } - } - } - if (!$check_only && $msgs) - { - global $oProc; - if (!isset($oProc)) $oProc = new schema_proc(); - // PostgreSQL seems to require to update the sequenz, after manually inserting id's - $oProc->UpdateSequence('egw_sqlfs', 'fs_id'); - } - return $msgs; - } - - /** - * Check and optionally remove files without content part in physical filesystem - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_no_content($check_only=true) - { - $stmt = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. - " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) - { - if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) - { - $path = self::id2path($row['fs_id']); - if ($check_only) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - $stmt_props = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array('fs_id' => $row['fs_id'])) && - $stmt_props->execute(array('fs_id' => $row['fs_id']))) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); - } - else - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - } - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Files without content in physical filesystem will be removed.'); - } - return $msgs; - } - - /** - * Name of lost+found directory for unconnected nodes - */ - const LOST_N_FOUND = '/lost+found'; - const LOST_N_FOUND_MOD = 070; - const LOST_N_FOUND_GRP = 'Admins'; - - /** - * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: - * - * SELECT fs.* - * FROM egw_sqlfs fs - * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir - * WHERE fs.fs_id > 1 && dir.fs_id IS NULL - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_unconnected($check_only=true) - { - $lostnfound = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. - ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. - ' WHERE fs.fs_id > 1 AND dir.fs_id IS NULL') as $row) - { - if ($check_only) - { - $msgs[] = lang('Found unconnected %1 %2!', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); - continue; - } - if (!isset($lostnfound)) - { - // check if we already have /lost+found, create it if not - if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - egw_vfs::$is_root = true; - if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || - !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || - self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || - !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); - } - egw_vfs::$is_root = false; - if (!$lostnfound) break; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array( - 'fs_dir' => $lostnfound['ino'], - 'fs_id' => $row['fs_id'], - ))) - { - $msgs[] = lang('Moved unconnected %1 %2 to %3.', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', - self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Failed to move unconnected %1 %2 to %3!', - mime_magic::mime2label($row['fs_mime']), egw_vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); - } - return $msgs; - } - - /** - * Check and optionally fix multiple active files and directories with identical path - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_multiple_active($check_only=true) - { - $stmt = $inactivate_msg_added = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. - ' WHERE fs_active='.self::_pdo_boolean(true). - ' GROUP BY fs_dir,'.(self::$pdo_type == 'mysql' ? 'BINARY ' : '').'fs_name'. // fs_name is casesensitive! - ' HAVING COUNT(*) > 1') as $row) - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. - ' FROM '.self::TABLE.' fs'. - ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. - " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); - $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. - ' SET fs_active='.self::_pdo_boolean(false). - ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). - ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); - } - //$msgs[] = array2string($row); - $cnt = 0; - $stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - )); - foreach($stmt as $n => $entry) - { - if ($entry['fs_mime'] == 'httpd/unix-directory') - { - if (!$n) - { - $dir = $entry; // directory to keep - $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); - if ($check_only) break; - } - else - { - if ($entry['children']) - { - $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', - $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. - ' WHERE fs_dir='.(int)$entry['fs_id']), - $entry['fs_id'], $dir['fs_id']); - - $dir['children'] += $children; - } - self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); - $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); - } - } - elseif (isset($dir)) // file and directory with same name exist! - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $dir['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = ucfirst(lang('none of %1', $row[2]-1)); - } - $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); - break; - } - else // newest file --> set for all other fs_active=false - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $entry['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = lang('none of %1', $row[2]-1); - } - $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', - self::id2path($entry['fs_id']), $cnt); - break; - } - } - unset($dir); - if ($cnt && !isset($inactivate_msg_added)) - { - $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); - $inactivate_msg_added = true; - } - } - return $msgs; - } -} - -// fsck testcode, if this file is called via it's URL (you need to uncomment it!) -/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) -{ - $GLOBALS['egw_info'] = array( - 'flags' => array( - 'currentapp' => 'admin', - 'nonavbar' => true, - ), - ); - include_once '../../header.inc.php'; - - $msgs = sqlfs_utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); - echo '

'.implode("

\n

", (array)$msgs)."

\n"; -}*/ \ No newline at end of file +class sqlfs_utils extends Sqlfs\Utils {} diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php index 92dea212c1..f8d855cfd5 100755 --- a/phpgwapi/inc/common_functions.inc.php +++ b/phpgwapi/inc/common_functions.inc.php @@ -1616,16 +1616,44 @@ function json_php_unserialize($str, $allow_not_serialized=false) } /** - * php5 autoload function for eGroupWare understanding the following naming schema: + * New PSR-4 autoloader for EGroupware + * + * class_exists('\\EGroupware\\Api\\Vfs'); // /api/src/Vfs.php + * class_exists('\\EGroupware\\Api\\Vfs\\Dav\\Directory'); // /api/src/Vfs/Dav/Directory.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\StreamWrapper'); // /api/src/Vfs/Sqlfs/StreamWrapper.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\Utils'); // /api/src/Vfs/Sqlfs/Utils.php + * class_exists('\\EGroupware\\Stylite\\Versioning\\StreamWrapper'); // /stylite/src/Versioning/StreamWrapper.php + * class_exists('\\EGroupware\\Calendar\\Ui'); // /calendar/src/Ui.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Lists'); // /calendar/src/Ui/Lists.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Views'); // /calendar/src/Ui/Views.php + */ +spl_autoload_register(function($class) +{ + $parts = explode('\\', $class); + if (array_shift($parts) != 'EGroupware') return; // not our prefix + + $app = lcfirst(array_shift($parts)); + $base = EGW_INCLUDE_ROOT.'/'.$app.'/src/'; + $path = $base.implode('/', $parts).'.php'; + + if (file_exists($path)) + { + require_once $path; + //error_log("PSR4_autoload('$class') --> require_once($path) --> class_exists('$class')=".array2string(class_exists($class,false))); + } +}); + +/** + * Old autoloader for EGroupware understanding the following naming schema: + * * 1. new (prefered) nameing schema: app_class_something loading app/inc/class.class_something.inc.php * 2. API classes: classname loading phpgwapi/inc/class.classname.inc.php * 2a.API classes containing multiple classes per file eg. egw_exception* in class.egw_exception.inc.php * 3. eTemplate classes: classname loading etemplate/inc/class.classname.inc.php - * 4. classes of the current app: classname loading $GLOBALS['egw_info']['flags']['currentapp']/inc/class.classname.inc.php * * @param string $class name of class to load */ -function __autoload($class) +spl_autoload_register(function($class) { // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings if (strlen($class) > 64 || strpos($class, '.') !== false) return; @@ -1660,9 +1688,7 @@ function __autoload($class) { call_user_func($GLOBALS['egw_info']['flags']['autoload'],$class); } -} -// register our autoloader with sql, so other PHP code can use spl_autoload_register, without stalling our autoloader -spl_autoload_register('__autoload'); +}); // if we have a Composer vendor directory, also load it's autoloader, to allow manage our requirements with Composer if (file_exists(EGW_SERVER_ROOT.'/vendor')) From be1d83968611ae95abf162269b0e3600f042e63f Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 26 Jan 2015 11:39:35 +0000 Subject: [PATCH 03/42] Remove specific handling on link's caption to open expose view, although still we keep the handler on icon --- etemplate/js/et2_widget_link.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/etemplate/js/et2_widget_link.js b/etemplate/js/et2_widget_link.js index 87dee296cb..ffe7faadb8 100644 --- a/etemplate/js/et2_widget_link.js +++ b/etemplate/js/et2_widget_link.js @@ -1675,18 +1675,7 @@ var et2_link_list = et2_link_string.extend( $j(document.createElement("td")) .appendTo(row) .addClass(columns[i]) - .click( function(){ - // Check if the link entry is mime with media type, in order to open it in expose view - if (typeof _link_data.type != 'undefined' && _link_data.type.match(/^(video|audio|image|media)\//,'ig')) - { - var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon'); - if ($vfs_img_node.length > 0) $vfs_img_node.click(); - } - else - { - self.egw().open(_link_data, "", "view",null,_link_data.target ? _link_data.target : _link_data.app,_link_data.app); - } - }) + .click( function(){self.egw().open(_link_data, "", "view",null,_link_data.target ? _link_data.target : _link_data.app,_link_data.app);}) .text(_link_data[columns[i]] ? _link_data[columns[i]]+"" : ""); } From e6d9cdd121fffc9581abed32752f29412afc29a5 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 26 Jan 2015 11:42:28 +0000 Subject: [PATCH 04/42] Style popup loading view for mobile theme --- pixelegg/css/mobile.css | 5 ++--- pixelegg/css/mobile.less | 11 +++++------ pixelegg/images/loading.gif | Bin 0 -> 3897 bytes 3 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 pixelegg/images/loading.gif diff --git a/pixelegg/css/mobile.css b/pixelegg/css/mobile.css index ab3d83527e..eb7cf28899 100644 --- a/pixelegg/css/mobile.css +++ b/pixelegg/css/mobile.css @@ -6957,9 +6957,8 @@ a.textSidebox { visibility: hidden; } .egw_fw_mobile_popup_loader { - background-image: url('../images/ajax-loader.gif'); - background-repeat: no-repeat; + background: url(../images/loading.gif) center no-repeat; + background-size: 120px 120px; background-position: center; - background: rgba(0, 0, 0, 0.44) !important; } } diff --git a/pixelegg/css/mobile.less b/pixelegg/css/mobile.less index b1cc63f1e3..2ff43b507f 100644 --- a/pixelegg/css/mobile.less +++ b/pixelegg/css/mobile.less @@ -49,7 +49,7 @@ font-size:medium; margin-top:15px; } - + //################### //# # //# Grid & NM # @@ -61,8 +61,8 @@ th{ font-size: small !important; } - } - } + } + } } } #egw_fw_basecontainer{ @@ -724,9 +724,8 @@ } } .egw_fw_mobile_popup_loader { - background-image: url('../images/ajax-loader.gif'); - background-repeat: no-repeat; + background: url(../images/loading.gif) center no-repeat; + background-size: 120px 120px; background-position: center; - background: rgba(0, 0, 0, 0.44) !important; } } \ No newline at end of file diff --git a/pixelegg/images/loading.gif b/pixelegg/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..90f28cbdbb390b095e0d619cbe8d91208798e58f GIT binary patch literal 3897 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 D^>?LD literal 0 HcmV?d00001 From 8c1f3a5386fd2643fb1f4f4a141871e1c38e948e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 26 Jan 2015 12:13:10 +0000 Subject: [PATCH 05/42] changes suggested on lists for a correct Brasilian translation --- calendar/lang/egw_pt-br.lang | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/calendar/lang/egw_pt-br.lang b/calendar/lang/egw_pt-br.lang index 1cf9a9bfd0..92f0c3764a 100644 --- a/calendar/lang/egw_pt-br.lang +++ b/calendar/lang/egw_pt-br.lang @@ -1,8 +1,8 @@ %1 %2 in %3 calendar pt-br %1 %2 em %3 %1 days calendar pt-br %1 dias %1 event(s) %2 calendar pt-br %1 evento (s) 2% -%1 hours calendar pt-br %1 uma hora -%1 minutes calendar pt-br %1 minuto +%1 hours calendar pt-br %1 horas +%1 minutes calendar pt-br %1 minutos %1 records imported calendar pt-br %1 registro(s) importado(s) %1 records read (not yet imported, you may go back and uncheck test import) calendar pt-br %1 registro(s) lido(s) (não importado(s) ainda. Você deve voltar e desmarcar "Testar Importação") %1 weeks calendar pt-br %1 semanas From c3a8f0d7c08da4f02093ae7fffdfb8bffbc6fd4b Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 26 Jan 2015 14:32:39 +0000 Subject: [PATCH 06/42] Fix etemplate2 clear instance broken by commit 51394 --- etemplate/js/etemplate2.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index c4ebecab89..9f1c49ce09 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -196,6 +196,19 @@ etemplate2.prototype.clear = function() this.widgetContainer = null; } $j(this.DOMContainer).empty(); + + // Remove self from the index + for(name in this.templates) + { + if(typeof etemplate2._byTemplate[name] == "undefined") continue; + for(var i = 0; i < etemplate2._byTemplate[name].length; i++) + { + if(etemplate2._byTemplate[name][i] == this) + { + etemplate2._byTemplate[name].splice(i,1); + } + } + } }; /** From 21d52f5ceed3de8e9a9fe86a7828b5a3a0d242b8 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 26 Jan 2015 15:04:15 +0000 Subject: [PATCH 07/42] fixed warning of calling createRowID static with incompatible $this, it work before because mail_ui as well as mail_compose define $this->mail_bo --- mail/inc/class.mail_compose.inc.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index b9a90240e0..31fe317548 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -101,7 +101,7 @@ class mail_compose $this->mailPreferences =& $this->mail_bo->mailPreferences; } } - + /** * Provide toolbar actions used for compose toolbar * @param array $content content of compose temp @@ -110,6 +110,7 @@ class mail_compose */ function getToolbarActions($content) { + $group = 0; $actions = array( 'send' => array( 'caption' => 'Send', @@ -183,7 +184,7 @@ class mail_compose 'children' => array(), 'toolbarDefault' => true, 'hint' => 'Select the message priority tag', - + ), 'save2vfs' => array ( 'caption' => 'Save to VFS', @@ -218,7 +219,7 @@ class mail_compose { $actions['prty']['children'][$content['priority']]['default'] = true; } - + return $actions; } @@ -1240,7 +1241,7 @@ class mail_compose if (!isset($content['priority']) || empty($content['priority'])) $content['priority']=3; //$GLOBALS['egw_info']['flags']['currentapp'] = 'mail';//should not be needed $etpl = new etemplate_new('mail.compose'); - + $etpl->setElementAttribute('composeToolbar', 'actions', $this->getToolbarActions($_content)); if ($content['mimeType']=='html') { @@ -1526,7 +1527,7 @@ class mail_compose } } // if the message is located within the draft folder, add it as last drafted version (for possible cleanup on abort)) - if ($mail_bo->isDraftFolder($_folder)) $this->sessionData['lastDrafted'] = mail_ui::createRowID($_folder, $_uid);//array('uid'=>$_uid,'folder'=>$_folder); + if ($mail_bo->isDraftFolder($_folder)) $this->sessionData['lastDrafted'] = mail_ui::generateRowID($this->mail_bo->profileID, $_folder, $_uid);//array('uid'=>$_uid,'folder'=>$_folder); $this->sessionData['uid'] = $_uid; $this->sessionData['messageFolder'] = $_folder; $this->sessionData['isDraft'] = true; @@ -2455,7 +2456,7 @@ class mail_compose $messageUid = ($messageUid===true ? $status['uidnext'] : $messageUid); if (is_array($this->mail_bo->getMessageHeader($messageUid, '',false, false, $folder))) { - $draft_id = mail_ui::createRowID($folder, $messageUid); + $draft_id = mail_ui::generateRowID($this->mail_bo->profileID, $folder, $messageUid); if ($content['lastDrafted'] != $draft_id && isset($content['lastDrafted'])) { $dhA = mail_ui::splitRowID($content['lastDrafted']); @@ -3090,7 +3091,7 @@ class mail_compose * @param int $_searchStringLength * @param boolean $_returnList * @param int $_mailaccountToSearch - * @param boolean $_noPrefixID = false, if set to true folders name does not get prefixed by account id + * @param boolean $_noPrefixId = false, if set to true folders name does not get prefixed by account id * @return type */ function ajax_searchFolder($_searchStringLength=2, $_returnList=false, $_mailaccountToSearch=null, $_noPrefixId=false) { From ebab506aa9c1435096b239eb8cc7988afc40fd69 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 26 Jan 2015 16:13:08 +0000 Subject: [PATCH 08/42] * All Applications: Get browser autocomplete form working --- etemplate/inc/class.etemplate_new.inc.php | 3 ++- etemplate/js/etemplate2.js | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 74e08e3ddd..36cb9a1133 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -225,7 +225,8 @@ class etemplate_new extends etemplate_widget_template { $load_array['response'] = egw_json_response::get()->returnResult(); } - echo '
'; + //
'; if ($output_mode == 2) { diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 9f1c49ce09..c6040c338a 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -657,6 +657,11 @@ etemplate2.prototype.submit = function(button, async, no_validation) var api = this.widgetContainer.egw(); var request = api.json(this.menuaction, [this.etemplate_exec_id, values, no_validation], null, this, async); request.sendRequest(); + + // Submit the template to an empty iframe (egw_iframe_autocomplete_helper) + // in order to get default browser autocomplete working + // maybe later we find better solution then we can remove this part + jQuery("form#egw_form_autocomplete_helper").submit(); } else { From 6b01a189606f0835888b7896a52a24cc8491647c Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 26 Jan 2015 17:30:10 +0000 Subject: [PATCH 09/42] Clean up debug --- .../inc/class.etemplate_widget_vfs.inc.php | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/etemplate/inc/class.etemplate_widget_vfs.inc.php b/etemplate/inc/class.etemplate_widget_vfs.inc.php index 0e04f9bd91..a88b267f2c 100644 --- a/etemplate/inc/class.etemplate_widget_vfs.inc.php +++ b/etemplate/inc/class.etemplate_widget_vfs.inc.php @@ -33,17 +33,12 @@ class etemplate_widget_vfs extends etemplate_widget_file { if($this->type == 'vfs-upload') { -echo "EXPAND"; -_debug_array($expand); $form_name = self::form_name($cname, $this->id, $expand ? $expand : array('cont'=>self::$request->content)); -echo "this-ID: {$this->id}
"; -echo "Form name: $form_name
"; // ID maps to path - check there for any existing files list($app,$id,$relpath) = explode(':',$this->id,3); if($app && $id) { -echo "ID: $id
"; if(!is_numeric($id)) { $_id = self::expand_name($id,0,0,0,0,self::$request->content); @@ -51,11 +46,9 @@ echo "ID: $id
"; { $id = $_id; $form_name = "$app:$id:$relpath"; -echo "Form name: $form_name
"; } } $value =& self::get_array(self::$request->content, $form_name, true); -echo "ID: $id
"; $path = egw_link::vfs_path($app,$id,'',true); if (!empty($relpath)) $path .= '/'.$relpath; @@ -67,23 +60,17 @@ echo "ID: $id
"; else if (substr($path, -1) == '/' && egw_vfs::is_dir($path)) { $value = egw_vfs::scandir($path); -echo 'HERE!'; foreach($value as &$file) { -echo $file.'
'; $file = egw_vfs::stat("$path$file"); -_debug_array($file); } } } -echo $this; -_debug_array($value); } } public static function ajax_upload() { parent::ajax_upload(); - error_log(array2string($_FILES)); foreach($_FILES as $field => $file) { self::store_file($field, $file); @@ -91,15 +78,14 @@ _debug_array($value); } /** - * Ajax callback to receive an incoming file - * - * The incoming file is automatically placed into the appropriate VFS location. - * If the entry is not yet created, the file information is stored into the widget's value. - * When the form is submitted, the information for all files uploaded is available in the returned - * $content array and the application should deal with the file. - */ + * Ajax callback to receive an incoming file + * + * The incoming file is automatically placed into the appropriate VFS location. + * If the entry is not yet created, the file information is stored into the widget's value. + * When the form is submitted, the information for all files uploaded is available in the returned + * $content array and the application should deal with the file. + */ public static function store_file($path, $file) { -error_log(array2string($file)); $name = $path; // Find real path @@ -124,13 +110,11 @@ error_log(array2string($file)); if (!egw_vfs::file_exists($dir = egw_vfs::dirname($path)) && !egw_vfs::mkdir($dir,null,STREAM_MKDIR_RECURSIVE)) { self::set_validation_error($name,lang('Error create parent directory %1!',egw_vfs::decodePath($dir))); -error_log(lang('Error create parent directory %1!',egw_vfs::decodePath($dir))); return false; } if (!copy($file['tmp_name'],egw_vfs::PREFIX.$path)) { self::set_validation_error($name,lang('Error copying uploaded file to vfs!')); -error_log(lang('Error copying uploaded file to vfs!')); return false; } From a644255c7835a9ab19bc082be6b1a50259309017 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 26 Jan 2015 17:34:47 +0000 Subject: [PATCH 10/42] Clean up debug --- etemplate/js/etemplate2.js | 1 - 1 file changed, 1 deletion(-) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index c6040c338a..4e64ad0c4f 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -936,7 +936,6 @@ etemplate2.getById = function(id) { for( var name in etemplate2._byTemplate) { - console.log(name, etemplate2._byTemplate[name]); for(var i = 0; i < etemplate2._byTemplate[name].length; i++) { var et = etemplate2._byTemplate[name][i]; From fdff696a0cf9fe971a680bc31f8fed5d15a6d9b4 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 26 Jan 2015 17:48:06 +0000 Subject: [PATCH 11/42] Enhance error log with name of problem favorite. --- phpgwapi/inc/class.egw_favorites.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpgwapi/inc/class.egw_favorites.inc.php b/phpgwapi/inc/class.egw_favorites.inc.php index ced6315f08..0a456b0cda 100644 --- a/phpgwapi/inc/class.egw_favorites.inc.php +++ b/phpgwapi/inc/class.egw_favorites.inc.php @@ -75,7 +75,7 @@ class egw_favorites //filter must not be empty if there's one, ignore it at the moment but it need to be checked how it got there in database if (!$filter) { - error_log(__METHOD__.'Favorite filter is not suppose to be empty, it should be an array. filter = '. array2string($filters[$name])); + error_log(__METHOD__.'Favorite filter "'.$name.'" is not supposed to be empty, it should be an array. Skipping, more investigation needed. filter = '. array2string($filters[$name])); continue; } From 61103e950d6526a39634c5f47f7e01d8a3d6ed59 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 26 Jan 2015 19:23:13 +0000 Subject: [PATCH 12/42] Avoid displaying portlets from apps the user has no access to. --- home/inc/class.home_ui.inc.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/home/inc/class.home_ui.inc.php b/home/inc/class.home_ui.inc.php index d55e6223c6..f0fce6de41 100644 --- a/home/inc/class.home_ui.inc.php +++ b/home/inc/class.home_ui.inc.php @@ -164,6 +164,14 @@ class home_ui ) continue; $classname = $context['class']; + + // Avoid portlets for apps user can't use (eg. from defaults/forced) + list($app,$other) = explode('_',$classname); + if(!$GLOBALS['egw_info']['apps'][$app]) $app .='_'.$other; + if(!$GLOBALS['egw_info']['user']['apps'][$app]) { + continue; + } + $portlet = new $classname($context); $desc = $portlet->get_description(); $portlet_content = array( From 69366f13d17e789a6f87cda58b51e80bdf522850 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 07:55:11 +0000 Subject: [PATCH 13/42] * Mail: fix not working BCC addresses --- phpgwapi/inc/class.egw_mailer.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpgwapi/inc/class.egw_mailer.inc.php b/phpgwapi/inc/class.egw_mailer.inc.php index fb899cbe65..c617f331bf 100644 --- a/phpgwapi/inc/class.egw_mailer.inc.php +++ b/phpgwapi/inc/class.egw_mailer.inc.php @@ -705,6 +705,8 @@ class egw_mailer extends Horde_Mime_Mail { switch($name) { + case '_bcc': + $this->_bcc = $value; // this is NOT PHPMailer compatibility, but need for working BCC, if $this->_bcc is NOT set case 'Sender': $this->addHeader('Return-Path', '<'.$value.'>', true); break; From 3f0d279704c890c2abc5875555d4c02428e7299d Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 08:55:11 +0000 Subject: [PATCH 14/42] move autoloader on top of file, as it is no longer a function (__autoload) and therefore it need to be executed before all other code to be available, eg. for html::purify() in _check_script_tag() --- phpgwapi/inc/common_functions.inc.php | 162 +++++++++++++------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php index f8d855cfd5..3ba1a42383 100755 --- a/phpgwapi/inc/common_functions.inc.php +++ b/phpgwapi/inc/common_functions.inc.php @@ -14,6 +14,87 @@ * @version $Id$ */ +/** + * New PSR-4 autoloader for EGroupware + * + * class_exists('\\EGroupware\\Api\\Vfs'); // /api/src/Vfs.php + * class_exists('\\EGroupware\\Api\\Vfs\\Dav\\Directory'); // /api/src/Vfs/Dav/Directory.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\StreamWrapper'); // /api/src/Vfs/Sqlfs/StreamWrapper.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\Utils'); // /api/src/Vfs/Sqlfs/Utils.php + * class_exists('\\EGroupware\\Stylite\\Versioning\\StreamWrapper'); // /stylite/src/Versioning/StreamWrapper.php + * class_exists('\\EGroupware\\Calendar\\Ui'); // /calendar/src/Ui.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Lists'); // /calendar/src/Ui/Lists.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Views'); // /calendar/src/Ui/Views.php + */ +spl_autoload_register(function($class) +{ + $parts = explode('\\', $class); + if (array_shift($parts) != 'EGroupware') return; // not our prefix + + $app = lcfirst(array_shift($parts)); + $base = EGW_INCLUDE_ROOT.'/'.$app.'/src/'; + $path = $base.implode('/', $parts).'.php'; + + if (file_exists($path)) + { + require_once $path; + //error_log("PSR4_autoload('$class') --> require_once($path) --> class_exists('$class')=".array2string(class_exists($class,false))); + } +}); + +/** + * Old autoloader for EGroupware understanding the following naming schema: + * + * 1. new (prefered) nameing schema: app_class_something loading app/inc/class.class_something.inc.php + * 2. API classes: classname loading phpgwapi/inc/class.classname.inc.php + * 2a.API classes containing multiple classes per file eg. egw_exception* in class.egw_exception.inc.php + * 3. eTemplate classes: classname loading etemplate/inc/class.classname.inc.php + * + * @param string $class name of class to load + */ +spl_autoload_register(function($class) +{ + // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings + if (strlen($class) > 64 || strpos($class, '.') !== false) return; + + $components = explode('_',$class); + $app = array_shift($components); + // classes using the new naming schema app_class_name, eg. admin_cmd + if (file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$class.'.inc.php') || + // classes using the new naming schema app_class_name, eg. admin_cmd + isset($components[0]) && file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$app.'_'.$components[0].'.inc.php') || + // classes with an underscore in their name + !isset($GLOBALS['egw_info']['apps'][$app]) && isset($GLOBALS['egw_info']['apps'][$app . '_' . $components[0]]) && + file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'_'.$components[0].'/inc/class.'.$class.'.inc.php') || + // eGW api classes using the old naming schema, eg. html + file_exists($file = EGW_API_INC.'/class.'.$class.'.inc.php') || + // eGW api classes containing multiple classes in on file, eg. egw_exception + isset($components[0]) && file_exists($file = EGW_API_INC.'/class.'.$app.'_'.$components[0].'.inc.php') || + // eGW eTemplate classes using the old naming schema, eg. etemplate + file_exists($file = EGW_INCLUDE_ROOT.'/etemplate/inc/class.'.$class.'.inc.php') || + // include PEAR and PSR0 classes from include_path + // need to use include (not include_once) as eg. a previous included EGW_API_INC/horde/Horde/String.php causes + // include_once('Horde/String.php') to return true, even if the former was included with an absolute path + // only use include_path, if no Composer vendor directory exists + !isset($GLOBALS['egw_info']['apps'][$app]) && !file_exists(EGW_SERVER_ROOT.'/vendor') && + @include($file = strtr($class, array('_'=>'/','\\'=>'/')).'.php')) + { + include_once($file); + //if (!class_exists($class, false) && !interface_exists($class, false)) error_log("autoloading class $class by include_once($file) failed!"); + } + // allow apps to define an own autoload method + elseif (isset($GLOBALS['egw_info']['flags']['autoload']) && is_callable($GLOBALS['egw_info']['flags']['autoload'])) + { + call_user_func($GLOBALS['egw_info']['flags']['autoload'],$class); + } +}); + +// if we have a Composer vendor directory, also load it's autoloader, to allow manage our requirements with Composer +if (file_exists(EGW_SERVER_ROOT.'/vendor')) +{ + require_once EGW_SERVER_ROOT.'/vendor/autoload.php'; +} + /** * applies stripslashes recursivly on each element of an array * @@ -1615,87 +1696,6 @@ function json_php_unserialize($str, $allow_not_serialized=false) return $str; } -/** - * New PSR-4 autoloader for EGroupware - * - * class_exists('\\EGroupware\\Api\\Vfs'); // /api/src/Vfs.php - * class_exists('\\EGroupware\\Api\\Vfs\\Dav\\Directory'); // /api/src/Vfs/Dav/Directory.php - * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\StreamWrapper'); // /api/src/Vfs/Sqlfs/StreamWrapper.php - * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\Utils'); // /api/src/Vfs/Sqlfs/Utils.php - * class_exists('\\EGroupware\\Stylite\\Versioning\\StreamWrapper'); // /stylite/src/Versioning/StreamWrapper.php - * class_exists('\\EGroupware\\Calendar\\Ui'); // /calendar/src/Ui.php - * class_exists('\\EGroupware\\Calendar\\Ui\\Lists'); // /calendar/src/Ui/Lists.php - * class_exists('\\EGroupware\\Calendar\\Ui\\Views'); // /calendar/src/Ui/Views.php - */ -spl_autoload_register(function($class) -{ - $parts = explode('\\', $class); - if (array_shift($parts) != 'EGroupware') return; // not our prefix - - $app = lcfirst(array_shift($parts)); - $base = EGW_INCLUDE_ROOT.'/'.$app.'/src/'; - $path = $base.implode('/', $parts).'.php'; - - if (file_exists($path)) - { - require_once $path; - //error_log("PSR4_autoload('$class') --> require_once($path) --> class_exists('$class')=".array2string(class_exists($class,false))); - } -}); - -/** - * Old autoloader for EGroupware understanding the following naming schema: - * - * 1. new (prefered) nameing schema: app_class_something loading app/inc/class.class_something.inc.php - * 2. API classes: classname loading phpgwapi/inc/class.classname.inc.php - * 2a.API classes containing multiple classes per file eg. egw_exception* in class.egw_exception.inc.php - * 3. eTemplate classes: classname loading etemplate/inc/class.classname.inc.php - * - * @param string $class name of class to load - */ -spl_autoload_register(function($class) -{ - // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings - if (strlen($class) > 64 || strpos($class, '.') !== false) return; - - $components = explode('_',$class); - $app = array_shift($components); - // classes using the new naming schema app_class_name, eg. admin_cmd - if (file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$class.'.inc.php') || - // classes using the new naming schema app_class_name, eg. admin_cmd - isset($components[0]) && file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$app.'_'.$components[0].'.inc.php') || - // classes with an underscore in their name - !isset($GLOBALS['egw_info']['apps'][$app]) && isset($GLOBALS['egw_info']['apps'][$app . '_' . $components[0]]) && - file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'_'.$components[0].'/inc/class.'.$class.'.inc.php') || - // eGW api classes using the old naming schema, eg. html - file_exists($file = EGW_API_INC.'/class.'.$class.'.inc.php') || - // eGW api classes containing multiple classes in on file, eg. egw_exception - isset($components[0]) && file_exists($file = EGW_API_INC.'/class.'.$app.'_'.$components[0].'.inc.php') || - // eGW eTemplate classes using the old naming schema, eg. etemplate - file_exists($file = EGW_INCLUDE_ROOT.'/etemplate/inc/class.'.$class.'.inc.php') || - // include PEAR and PSR0 classes from include_path - // need to use include (not include_once) as eg. a previous included EGW_API_INC/horde/Horde/String.php causes - // include_once('Horde/String.php') to return true, even if the former was included with an absolute path - // only use include_path, if no Composer vendor directory exists - !isset($GLOBALS['egw_info']['apps'][$app]) && !file_exists(EGW_SERVER_ROOT.'/vendor') && - @include($file = strtr($class, array('_'=>'/','\\'=>'/')).'.php')) - { - include_once($file); - //if (!class_exists($class, false) && !interface_exists($class, false)) error_log("autoloading class $class by include_once($file) failed!"); - } - // allow apps to define an own autoload method - elseif (isset($GLOBALS['egw_info']['flags']['autoload']) && is_callable($GLOBALS['egw_info']['flags']['autoload'])) - { - call_user_func($GLOBALS['egw_info']['flags']['autoload'],$class); - } -}); - -// if we have a Composer vendor directory, also load it's autoloader, to allow manage our requirements with Composer -if (file_exists(EGW_SERVER_ROOT.'/vendor')) -{ - require_once EGW_SERVER_ROOT.'/vendor/autoload.php'; -} - /** * Clasify exception for a headline and log it to error_log, if not running as cli * From 7a0662b529d6698eb07d19d58ec12a272f0c3056 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 08:57:34 +0000 Subject: [PATCH 15/42] need to call egw_json_request::isJSONRequest(true), before throwing an exception (or calling egw_json_request::parseRequest()), otherwise exception is not shown on client as alert --- json.php | 1 + 1 file changed, 1 insertion(+) diff --git a/json.php b/json.php index 28736362dd..3ae33cc8a2 100644 --- a/json.php +++ b/json.php @@ -100,6 +100,7 @@ if (isset($_GET['menuaction'])) //Check whether the request data is set if (isset($GLOBALS['egw_unset_vars']['_POST[json_data]'])) { + $json->isJSONRequest(true); // otherwise exception is not send back to client, as we have not yet called parseRequest() throw new egw_exception_assertion_failed("JSON Data contains script tags. Aborting..."); } $json->parseRequest($_GET['menuaction'], $_REQUEST['json_data']); From 59e922221c0595ce48508cb84b63ad7fb1fa90fd Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 27 Jan 2015 10:52:42 +0000 Subject: [PATCH 16/42] Enhance autocomplete fixer and fixes some bugs --- etemplate/inc/class.etemplate_new.inc.php | 2 +- etemplate/js/etemplate2.js | 49 ++++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 36cb9a1133..c0afadbbb7 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -226,7 +226,7 @@ class etemplate_new extends etemplate_widget_template $load_array['response'] = egw_json_response::get()->returnResult(); } //
'; + echo '
'; if ($output_mode == 2) { diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 4e64ad0c4f..3593ecae1a 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -571,6 +571,47 @@ etemplate2.prototype.isDirty = function() return dirty; }; +/** + * Fixes browser autocomplete issue because of no form submission + * it wraps the given DOMNode with form tag and add an iframe with + * about:blank source to fake the submission + * + * @param {type} _node DOMNode which needs to be wrapped by autocomplete fixer form tag + * @param {type} _id uniqu id to remove autocomplete extra tags after the operation + */ +etemplate2.prototype.autocomplete_fixer = function (_node,_id) +{ + var node = _node||jQuery('.et2_contianer'); + var id = _id||this.uniqueId; + + // Submit the template to an empty iframe (egw_iframe_autocomplete_helper) + // in order to get default browser autocomplete working + // maybe later we find better solution then we can remove this part + var $et2_container = jQuery(node); + if ($et2_container.length > 0) + { + //Creates form wrapper + var $form = jQuery(document.createElement('form')) + .attr({id:id + '_form_autocomplete_fixer',action:'about:blank', target:'egw_iframe_autocomplete helper'}); + //Creates iframe for fake submission + $et2_container.before(jQuery(document.createElement('iframe')) + .attr({id:id + '_iframe_autocomplete_fixer',src:'about:blank',name:'egw_iframe_autocomplete helper'}).hide()); + //Wraps the wrapper + $et2_container.wrap($form); + } + //Activate autocomplete + setTimeout(function(){ + var $a = jQuery(node).parent(); + if ($a.length>0 && typeof $a.parent() != 'undefined') $a.submit(); //Submit to iframe + setTimeout(function(){ + //Clean up the mess from DOM + if(jQuery(node).parent().is('form')) + jQuery(node).unwrap(); + jQuery('iframe#'+id + '_iframe_autocomplete_fixer').remove(); + },1); + },1); +}; + /** * Submit form via ajax * @@ -651,17 +692,15 @@ etemplate2.prototype.submit = function(button, async, no_validation) // Create the request object if (this.menuaction) { + // Call autocomplete fixer + this.autocomplete_fixer(this.DOMContainer,this.uniqueId); + // unbind our session-destroy handler, as we are submitting this.unbind_unload(); var api = this.widgetContainer.egw(); var request = api.json(this.menuaction, [this.etemplate_exec_id, values, no_validation], null, this, async); request.sendRequest(); - - // Submit the template to an empty iframe (egw_iframe_autocomplete_helper) - // in order to get default browser autocomplete working - // maybe later we find better solution then we can remove this part - jQuery("form#egw_form_autocomplete_helper").submit(); } else { From 0e4d04cb6ce20e6da22c5bd6cf6d51d6f961c0c1 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 27 Jan 2015 11:17:11 +0000 Subject: [PATCH 17/42] Fix autocomplete fixer wrapper messes up nextmatch height size --- etemplate/js/etemplate2.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 3593ecae1a..d3bc0e5116 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -592,7 +592,8 @@ etemplate2.prototype.autocomplete_fixer = function (_node,_id) { //Creates form wrapper var $form = jQuery(document.createElement('form')) - .attr({id:id + '_form_autocomplete_fixer',action:'about:blank', target:'egw_iframe_autocomplete helper'}); + .attr({id:id + '_form_autocomplete_fixer',action:'about:blank', target:'egw_iframe_autocomplete helper'}) + .css({height:"100%"}); //Creates iframe for fake submission $et2_container.before(jQuery(document.createElement('iframe')) .attr({id:id + '_iframe_autocomplete_fixer',src:'about:blank',name:'egw_iframe_autocomplete helper'}).hide()); From 668c48eaa8ed42d97a4c2c32b2df62d3e978c0a4 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 27 Jan 2015 13:07:59 +0000 Subject: [PATCH 18/42] fix typo preventing ics parsing --- calendar/inc/class.calendar_ical.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index bcf599c427..292c1d5243 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -2661,7 +2661,7 @@ class calendar_ical extends calendar_boupdate break; case 'ORGANIZER': $event['organizer'] = $attributes['value']; // no egw field, but needed in AS - if (strtlower(substr($event['organizer'],0,7)) == 'mailto:') + if (strtolower(substr($event['organizer'],0,7)) == 'mailto:') { $event['organizer'] = substr($event['organizer'],7); } From c5cbbf240ac1525bba27c14df75b9b2199fca317 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 15:14:56 +0000 Subject: [PATCH 19/42] fix autocomplete for Firefox by doing a real submit to an https url, as faked submit to "about:blank" causes a security warning in FF --- etemplate/empty.html | 0 etemplate/js/etemplate2.js | 16 +++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 etemplate/empty.html diff --git a/etemplate/empty.html b/etemplate/empty.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index d3bc0e5116..7eb8e8e24b 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -594,6 +594,10 @@ etemplate2.prototype.autocomplete_fixer = function (_node,_id) var $form = jQuery(document.createElement('form')) .attr({id:id + '_form_autocomplete_fixer',action:'about:blank', target:'egw_iframe_autocomplete helper'}) .css({height:"100%"}); + // Firefox give a security warning when transmitting to "about:blank" from a https site + // we work around that by giving existing etemplate/empty.html url + if (navigator.userAgent.match(/firefox/i)) + $form.attr({action: egw.webserverUrl+'/etemplate/empty.html',method:'post'}); //Creates iframe for fake submission $et2_container.before(jQuery(document.createElement('iframe')) .attr({id:id + '_iframe_autocomplete_fixer',src:'about:blank',name:'egw_iframe_autocomplete helper'}).hide()); @@ -604,13 +608,11 @@ etemplate2.prototype.autocomplete_fixer = function (_node,_id) setTimeout(function(){ var $a = jQuery(node).parent(); if ($a.length>0 && typeof $a.parent() != 'undefined') $a.submit(); //Submit to iframe - setTimeout(function(){ - //Clean up the mess from DOM - if(jQuery(node).parent().is('form')) - jQuery(node).unwrap(); - jQuery('iframe#'+id + '_iframe_autocomplete_fixer').remove(); - },1); - },1); + //Clean up the mess from DOM + if(jQuery(node).parent().is('form')) + jQuery(node).unwrap(); + jQuery('iframe#'+id + '_iframe_autocomplete_fixer').remove(); + },0); }; /** From 789d02a7e0dc1417335d67395ab6862b875fde9b Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 15:57:07 +0000 Subject: [PATCH 20/42] Don't send settings on reload either. Avoids security errors with Ralf's iframe notes. --- etemplate/js/et2_widget_portlet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etemplate/js/et2_widget_portlet.js b/etemplate/js/et2_widget_portlet.js index ff6ab34dc4..98cb30df0f 100644 --- a/etemplate/js/et2_widget_portlet.js +++ b/etemplate/js/et2_widget_portlet.js @@ -270,7 +270,7 @@ var et2_portlet = et2_valueWidget.extend( this.div.addClass("loading"); // Pass updated settings, unless we're removing - var settings = value == '~remove~' ? {} : this.options.settings || {} + var settings = (typeof value == 'string') ? {} : this.options.settings || {} this.egw().jsonq("home.home_ui.ajax_set_properties",[this.id, settings, value,this.settings?this.settings.group:false], function(data) { // This section not for us From cee521d4cd1986470b0e1699357050d3fa500eec Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 16:26:59 +0000 Subject: [PATCH 21/42] Work in progress of printing nextmatches, still needs some prettying up & edge case testing --- etemplate/js/et2_core_interfaces.js | 17 +++ etemplate/js/et2_extension_nextmatch.js | 130 ++++++++++++++++++++- etemplate/js/etemplate2.js | 59 ++++++++++ etemplate/templates/default/etemplate2.css | 11 +- phpgwapi/js/framework/fw_base.js | 12 +- 5 files changed, 226 insertions(+), 3 deletions(-) diff --git a/etemplate/js/et2_core_interfaces.js b/etemplate/js/et2_core_interfaces.js index 15a5a98d22..70d2508de9 100644 --- a/etemplate/js/et2_core_interfaces.js +++ b/etemplate/js/et2_core_interfaces.js @@ -144,3 +144,20 @@ var et2_IDetachedDOM = new Interface({ }); +/** + * Interface for widgets that need to do something special before printing + */ +var et2_IPrint = new Interface({ + /** + * Set up for printing + * + * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up + * (waiting for data) + */ + beforePrint: function() {}, + + /** + * Reset after printing + */ + afterPrint: function() {} +}); \ No newline at end of file diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index 5f4888390e..6c65d42e33 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -68,7 +68,7 @@ var et2_INextmatchSortable = new Interface({ * * @augments et2_DOMWidget */ -var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], +var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrint], { attributes: { // These normally set in settings, but broken out into attributes to allow run-time changes @@ -1836,6 +1836,134 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], set_value: function(_value) { this.value = _value; + }, + + // Printing + /** + * Prepare for printing + * + * We check for un-loaded rows, and ask the user what they want to do about them. + * If they want to print them all, we ask the server and print when they're loaded. + */ + beforePrint: function() { + // Check for rows that aren't loaded yet, or lots of rows + var range = this.controller._grid.getIndexRange(); + this.old_height = this.controller._grid._scrollHeight; + var loaded_count = range.bottom - range.top; + var total = this.controller._grid.getTotalCount(); + if(loaded_count != total || + this.controller._grid.getTotalCount() > 100) + { + // Defer the printing + var defer = jQuery.Deferred(); + + // Something not in the grid, lets ask + et2_dialog.show_prompt(jQuery.proxy(function(button, value) { + if(button == 'dialog[cancel]') { + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function() {defer.reject();}, 0); + return; + } + value = parseInt(value); + if(value > total) + { + value = total; + } + + // If they want the whole thing, treat it as all + if(button == 'dialog[ok]' && value == this.controller._grid.getTotalCount()) + { + button = 'dialog[all]'; + // Add the class, gives more reliable sizing + this.div.addClass('print'); + } + // We need more rows + if(button == 'dialog[all]' || value > loaded_count) + { + var count = 0; + var fetchedCount = 0; + var cancel = false; + var nm = this; + var dialog = et2_dialog.show_dialog( + // Abort the long task if they canceled the data load + function() {count = total; cancel=true;window.setTimeout(function() {defer.reject();},0)}, + egw.lang('Loading'), egw.lang('please wait...'),{},[ + {"button_id": et2_dialog.CANCEL_BUTTON,"text": 'cancel',id: 'dialog[cancel]',image: 'cancel'} + ] + ); + + // dataFetch() is asyncronous, so all these requests just get fired off... + // 200 rows chosen arbitrarily to reduce requests. + do { + var ctx = { + "self": this.controller, + "start": count, + "count": Math.min(value,200), + "lastModification": this.controller._lastModification + }; + if(nm.controller.dataStorePrefix) + { + ctx.prefix = nm.controller.dataStorePrefix; + } + nm.controller.dataFetch({start:count, num_rows: Math.min(value,200)}, function(data) { + // Keep track + if(data && data.order) + { + fetchedCount += data.order.length; + } + nm.controller._fetchCallback.apply(this, arguments); + + if(fetchedCount >= value) + { + if(cancel) + { + dialog.destroy(); + defer.reject(); + return; + } + nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (value+1)); + // Grid needs to redraw before it can be printed, so wait + window.setTimeout(jQuery.proxy(function() { + dialog.destroy(); + // Should be OK to print now + defer.resolve(); + },nm),ET2_GRID_INVALIDATE_TIMEOUT); + + } + + },ctx); + count += 200; + } while (count < value) + nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (value+1)); + } + else + { + // Don't need more rows, limit to requested and finish + this.controller._grid.setScrollHeight(this.controller._grid.getAverageHeight() * (value+1)); + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function() {defer.resolve();}, 0); + } + //this.controller._gridCallback(0, button == et2_dialog.OK_BUTTON ? value : this.controller._grid.getTotalCount()); + },this), + egw.lang('How many rows to print'), egw.lang('Print'), + Math.min(100, total), + [ + {"button_id": 1,"text": egw.lang('Ok'), id: 'dialog[ok]', image: 'check', "default":true}, + // Nice for small lists, kills server for large lists + //{"button_id": 2,"text": egw.lang('All'), id: 'dialog[all]', image: ''}, + {"button_id": 0,"text": egw.lang('Cancel'), id: 'dialog[cancel]', image: 'cancel'}, + ] + ); + return defer; + } + // Don't return anything, just work normally + // Add the class, if needed + this.div.addClass('print'); + }, + afterPrint: function() { + this.div.removeClass('print'); + this.controller._grid.setScrollHeight(this.old_height); + delete this.old_height; } }); et2_register_widget(et2_nextmatch, ["nextmatch"]); diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 7eb8e8e24b..70e5249a29 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -919,6 +919,65 @@ etemplate2.app_refresh = function(_msg, _app, _id, _type) return refresh_done; }; +/** + * "Intelligently" print a given app + * + * Mostly, we let the nextmatch change how many rows it's showing, so you don't + * get just one printed page. + */ +etemplate2.print = function(_app) +{ + // Allow any widget to change for printing + var et2 = etemplate2.getByApplication(_app); + + // Sometimes changes take time + var deferred = []; + for(var i = 0; i < et2.length; i++) + { + // Skip hidden templates + if(!jQuery(et2[i].DOMContainer).filter(':visible')) continue; + + et2[i].widgetContainer.iterateOver(function(_widget) { + var result = _widget.beforePrint(); + if (typeof result == "object" && result.done) + { + deferred.push(result); + } + },et2,et2_IPrint); + } + + // Try to clean up after - not guaranteed + var afterPrint = function() { + for(var i = 0; i < et2.length; i++) + { + // Skip hidden templates + if(!jQuery(et2[i].DOMContainer).filter(':visible')) continue; + et2[i].widgetContainer.iterateOver(function(_widget) { + _widget.afterPrint(); + },et2,et2_IPrint); + } + var mediaQueryList = window.matchMedia('print'); + mediaQueryList + }; + if(egw.window.matchMedia) { + var mediaQueryList = window.matchMedia('print'); + var listener = function(mql) { + if (!mql.matches) { + afterPrint(); + mediaQueryList.removeListener(listener); + } + }; + mediaQueryList.addListener(listener); + } + + egw.window.onafterprint = afterPrint; + + // Wait for everything to be loaded, then send it off + jQuery.when.apply(jQuery, deferred).done(function() { + egw.window.print(); + }); +} + // Some static things to make getting into widget context a little easier // /** diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index e0b7de067f..48611bb183 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -462,6 +462,12 @@ which caused click on free space infront of a tag stops nm row selection*/ .et2_nextmatch .egwGridView_grid tr td div.et2_vbox a { display: table-row; } +.et2_nextmatch.print .egwGridView_scrollarea { + height: auto !important; +} +.et2_nextmatch.print > div { + height: auto !important; +} /** * Diff widget */ @@ -1115,13 +1121,16 @@ div.message.floating { .et2_nextmatch .egwGridView_grid > tbody > tr { display: block; } + .et2_nextmatch .egwGridView_spacer { + display:none; + } .egwGridView_grid > tbody > tr { page-break-inside: avoid; -webkit-region-break-inside: avoid; } .et2_nextmatch > div { width: 100% !important; - height: auto; + height: auto !important; } #cke_1_top.cke_top { display: none; diff --git a/phpgwapi/js/framework/fw_base.js b/phpgwapi/js/framework/fw_base.js index 55d5268fe5..dd88d0d5d8 100644 --- a/phpgwapi/js/framework/fw_base.js +++ b/phpgwapi/js/framework/fw_base.js @@ -958,7 +958,17 @@ var fw_base = Class.extend({ if (appWindow) { appWindow.focus(); - appWindow.print(); + + // et2 available, let its widgets prepare + if(typeof etemplate2 == "function" && etemplate2.print) + { + etemplate2.print(this.activeApp.appName); + } + else + { + // Print + appWindow.print(); + } } } } From f834ba893b93c6f80a7b0ffbc33e93d417e49e05 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 16:53:11 +0000 Subject: [PATCH 22/42] fix 2nd loading of etemplates in IE, which was broken since r51385, because IE can not use in main window cached object, if it was loaded from a now closed popup/iframe We use now jQuery.ajax() instead of native XMLHTTPRequest object from jQuery object of main-window --- etemplate/js/et2_core_xml.js | 75 +++++++++++++++--------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/etemplate/js/et2_core_xml.js b/etemplate/js/et2_core_xml.js index 5995a34857..425c011190 100644 --- a/etemplate/js/et2_core_xml.js +++ b/etemplate/js/et2_core_xml.js @@ -13,9 +13,14 @@ "use strict"; /** - * Loads the given URL asynchronously from the server. When the file is loaded, - * the given callback function is called, where "this" is set to the given - * context. + * Loads the given URL asynchronously from the server + * + * We make the Ajax call through main-windows jQuery object, to ensure cached copy + * in main-windows etemplate2 prototype works in IE too! + * + * @param {string} _url + * @param {function} _callback function(_xml) + * @param {object} _context for _callback */ function et2_loadXMLFromURL(_url, _callback, _context) { @@ -24,53 +29,33 @@ function et2_loadXMLFromURL(_url, _callback, _context) _context = null; } - if (window.XMLHttpRequest) - { - // Otherwise make an XMLHttpRequest. Tested with Firefox 3.6, Chrome, Opera - var xmlhttp = new XMLHttpRequest(); - - // Set the callback function - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState == 4) - { - if(xmlhttp.responseXML) - { - var xmldoc = xmlhttp.responseXML.documentElement; - _callback.call(_context, xmldoc); - } - // Sometimes it's not recogized as XML - reason unknown - else if (xmlhttp.response) - { - egw().debug("log","File was not recogized as XML, trying to parse text..."); - var response = xmlhttp.response.replace(/^\s+|\s+$/g,''); - // Manually parse from text - var parser = new DOMParser(); - try { - var xmldoc = parser.parseFromString(response, "text/xml"); - egw().debug("log","Parsed OK"); - _callback.call(_context, xmldoc.documentElement); - } catch (e) { - egw().debug("log", "Well, that didn't work"); - } - } - } - } - - // Force the browser to interpret the result as XML. overrideMimeType is - // non-standard, so we check for its existance. - if (xmlhttp.overrideMimeType) + // use window object from main window with same algorithm as for the template cache + var win; + try { + if (opener && opener.etemplate2) { - xmlhttp.overrideMimeType("application/xml"); + win = opener; } - - // Retrieve the script asynchronously - xmlhttp.open("GET", _url, true); - xmlhttp.send(null); } - else + catch (e) { + // catch security exception if opener is from a different domain + } + if (typeof win == "undefined") { - throw("XML Request object could not be created!"); + win = top; } + win.jQuery.ajax({ + url: _url, + context: _context, + type: 'GET', + dataType: 'xml', + success: function(_data, _status, _xmlhttp){ + _callback.call(_context, _data.documentElement); + }, + error: function(_xmlhttp, _err) { + alert('Loading eTemplate from '+_url+' failed! '+_err); + } + }); } function et2_directChildrenByTagName(_node, _tagName) From 481803ac3bc87cfef61bfb715934df6983d2ffa5 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 17:09:00 +0000 Subject: [PATCH 23/42] fix PHP Fatal error: Class "EGroupware\Api\ZipArchive" not found --- api/src/Vfs.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/Vfs.php b/api/src/Vfs.php index bfe0ab9523..ae2de39a62 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -1411,13 +1411,13 @@ class Vfs extends Vfs\StreamWrapper */ public static function download_zip(Array $_files, $name = false) { - error_log(__METHOD__ . ': '.implode(',',$_files)); + //error_log(__METHOD__ . ': '.implode(',',$_files)); // Create zip file $zip_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip'); - $zip = new ZipArchive(); - if (!$zip->open($zip_file, ZipArchive::OVERWRITE)) + $zip = new \ZipArchive(); + if (!$zip->open($zip_file, \ZipArchive::OVERWRITE)) { throw new egw_exception("Cannot open zip file for writing."); } From c853502a0103d5c78a7c9950b653a5a91f5e21ae Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 17:17:50 +0000 Subject: [PATCH 24/42] fix an other not aliased global class (egw_time) --- api/src/Vfs.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/Vfs.php b/api/src/Vfs.php index ae2de39a62..42191b3190 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -24,6 +24,7 @@ use HTTP_WebDAV_Server; use egw_exception_assertion_failed; use egw_exception_db; use egw_exception_wrong_parameter; +use egw_time; /** * Class containing static methods to use the new eGW virtual file system From 24284eb1fdcccbe88cd6d44a2d37405305dea4fd Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 27 Jan 2015 18:12:10 +0000 Subject: [PATCH 25/42] * Calendar/CalDAV: fixed not synced recurrences, because invitation was to a group only or first recurrence was an exception --- calendar/inc/class.calendar_groupdav.inc.php | 30 +++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php index fdf3616de0..5c02dad457 100644 --- a/calendar/inc/class.calendar_groupdav.inc.php +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -647,24 +647,28 @@ class calendar_groupdav extends groupdav_handler $events =& $bo->search($params); + // find master, which is not always first event, eg. when first event is an exception $master = null; foreach($events as $k => &$recurrence) { - if (!isset($master)) // first event is master + if ($recurrence['recur_type']) { $master = $recurrence; $exceptions =& $master['recur_exception']; unset($events[$k]); - continue; + break; } + } + foreach($events as $k => &$recurrence) + { //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($uid)[$k]:" . array2string($recurrence)); if ($recurrence['id'] != $master['id']) // real exception { // user is NOT participating in this exception - if ($user && !isset($recurrence['participants'][$user])) + if ($user && !self::isParticipant($recurrence, $user)) { // if he is NOT in master, delete this exception - if (!isset($master['participants'][$user])) + if (!self::isParticipant($master, $user)) { unset($events[$k]); continue; @@ -698,15 +702,27 @@ class calendar_groupdav extends groupdav_handler // not for included exceptions (Lightning): $master['recur_exception'][] = $recurrence['start']; } // only add master if we are not expanding and current user participates in master (and not just some exceptions) - if (!$expand && (!$user || isset($master['participants'][$user]) || - // for group-invitations we need to check memberships of $user too - array_intersect(array_keys($master['participants']), $GLOBALS['egw']->accounts->memberships($user, true)))) + if (!$expand && (!$user || self::isParticipant($master, $user))) { $events = array_merge(array($master), $events); } return $events; } + /** + * Check if $user is a participant of given $event incl. group-invitations + * + * @param array $event + * @param int|string $user + * @return boolean + */ + public static function isParticipant(array $event, $user) + { + return isset($event['participants'][$user]) || + // for group-invitations we need to check memberships of $user too + array_intersect(array_keys($event['participants']), $GLOBALS['egw']->accounts->memberships($user, true)); + } + /** * Handle put request for an event * From 16a64c879dc0d4c4deffcd87edd7186c33fc2458 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 18:53:50 +0000 Subject: [PATCH 26/42] Pass edit mode (resize, move, progress) so we can update better --- etemplate/js/et2_widget_gantt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etemplate/js/et2_widget_gantt.js b/etemplate/js/et2_widget_gantt.js index 23a4700a19..5f415fa11e 100644 --- a/etemplate/js/et2_widget_gantt.js +++ b/etemplate/js/et2_widget_gantt.js @@ -759,7 +759,7 @@ var et2_gantt = et2_valueWidget.extend([et2_IResizeable,et2_IInput], if(gantt_widget.options.ajax_update) { var request = gantt_widget.egw().json(gantt_widget.options.ajax_update, - [task,value] + [task, mode, value] ).sendRequest(true); } }); From 2be34b4b53c5b96d0a1716c65e8cdf6a9e52ee5b Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 20:48:06 +0000 Subject: [PATCH 27/42] Bug fixes on nextmatch printing - fix loaded rows check - fix hidden etemplate check --- etemplate/js/et2_extension_nextmatch.js | 2 +- etemplate/js/etemplate2.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index 6c65d42e33..78d65959ed 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -1849,7 +1849,7 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin // Check for rows that aren't loaded yet, or lots of rows var range = this.controller._grid.getIndexRange(); this.old_height = this.controller._grid._scrollHeight; - var loaded_count = range.bottom - range.top; + var loaded_count = range.bottom - range.top +1; var total = this.controller._grid.getTotalCount(); if(loaded_count != total || this.controller._grid.getTotalCount() > 100) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 70e5249a29..9b64e3069c 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -935,9 +935,11 @@ etemplate2.print = function(_app) for(var i = 0; i < et2.length; i++) { // Skip hidden templates - if(!jQuery(et2[i].DOMContainer).filter(':visible')) continue; + if(!jQuery(et2[i].DOMContainer).filter(':visible').length) continue; et2[i].widgetContainer.iterateOver(function(_widget) { + // Skip widgets from a different etemplate (home) + if(_widget.getInstanceManager() != et2[i]) return; var result = _widget.beforePrint(); if (typeof result == "object" && result.done) { From 1f46ac05165de99a4d557ced25900ccebcec0e77 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 21:20:02 +0000 Subject: [PATCH 28/42] Import warning improvements: - Only warn once about missing contact type - Do not try to check header if definition says there is no header --- .../class.addressbook_import_contacts_csv.inc.php | 13 +++++++++++-- .../inc/class.importexport_basic_import_csv.inc.php | 1 - .../inc/class.importexport_import_ui.inc.php | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/addressbook/inc/class.addressbook_import_contacts_csv.inc.php b/addressbook/inc/class.addressbook_import_contacts_csv.inc.php index 107a4f6e38..695db5181f 100644 --- a/addressbook/inc/class.addressbook_import_contacts_csv.inc.php +++ b/addressbook/inc/class.addressbook_import_contacts_csv.inc.php @@ -33,6 +33,11 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { */ protected $tracking; + /** + * @var boolean If import file has no type, it can generate a lot of warnings. + * Users don't like this, so we only warn once. + */ + private $type_warned = false; /** * imports entries according to given definition object. @@ -56,7 +61,6 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { $this->lookups['tid'][$tid] = $data['name']; } - // set contact owner $contact_owner = isset( $_definition->plugin_options['contact_owner'] ) ? $_definition->plugin_options['contact_owner'] : $this->user; @@ -122,7 +126,12 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { // Do not import into non-existing type, warn and change if(!$record->tid || !$this->lookups['tid'][$record->tid]) { - $this->warnings[$import_csv->get_current_position()] = lang('Unknown type %1, imported as %2',$record->tid,lang($this->lookups['tid']['n'])); + // Avoid lots of warnings about type (2 types are contact and deleted) + if(!$this->type_warned || count($this->lookups['tid']) == 2 ) + { + $this->warnings[$import_csv->get_current_position()] = lang('Unknown type %1, imported as %2',$record->tid,lang($this->lookups['tid']['n'])); + $this->type_warned = true; + } $record->tid = 'n'; } diff --git a/importexport/inc/class.importexport_basic_import_csv.inc.php b/importexport/inc/class.importexport_basic_import_csv.inc.php index 5fae9d9aa5..8144da9fb0 100644 --- a/importexport/inc/class.importexport_basic_import_csv.inc.php +++ b/importexport/inc/class.importexport_basic_import_csv.inc.php @@ -434,7 +434,6 @@ abstract class importexport_basic_import_csv implements importexport_iface_impor // Set up HTML $rows['h1'] = $labels; - error_log("Wow, ".count($this->preview_records) . ' preveiw records'); foreach($this->preview_records as $i => $row_data) { // Convert to human-friendly diff --git a/importexport/inc/class.importexport_import_ui.inc.php b/importexport/inc/class.importexport_import_ui.inc.php index 581e8f15e0..1b95daf174 100644 --- a/importexport/inc/class.importexport_import_ui.inc.php +++ b/importexport/inc/class.importexport_import_ui.inc.php @@ -359,6 +359,9 @@ // Only CSV files if(!$options['csv_fields']) return true; + // Can't check if definition has no header + if($options['num_header_lines'] == 0) return true; + $preference = $GLOBALS['egw_info']['user']['preferences']['common']['csv_charset']; $charset = $options['charset'] == 'user' || !$options['charset'] ? $preference : $options['charset']; From f1d7cd3b9e7aa67f70473023ca7bb931496e3016 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 21:50:56 +0000 Subject: [PATCH 29/42] If recur end date could not be parsed, handle it as missing. --- calendar/inc/class.calendar_ical.inc.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index 292c1d5243..a5352f75c6 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -2455,6 +2455,11 @@ class calendar_ical extends calendar_boupdate if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches)) { $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($matches[1]); + // If it couldn't be parsed, treat it as not set + if(is_string($vcardData['recur_enddate'])) + { + unset($vcardData['recur_enddate']); + } } elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches)) { From fd0b513bbbefc96b1ccd134550e6fd0b53d44f68 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 22:41:13 +0000 Subject: [PATCH 30/42] Keep appname through actions, fixes blank after action --- admin/inc/class.admin_categories.inc.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/admin/inc/class.admin_categories.inc.php b/admin/inc/class.admin_categories.inc.php index 483f44e130..3f3af1c684 100644 --- a/admin/inc/class.admin_categories.inc.php +++ b/admin/inc/class.admin_categories.inc.php @@ -471,6 +471,7 @@ class admin_categories } elseif($content['nm']['action']) { + $appname = $content['nm']['appname']; // Old buttons foreach(array('delete') as $button) { @@ -515,6 +516,8 @@ class admin_categories { $msg .= lang('%1 category(s) %2, %3 failed because of insufficent rights !!!',$success,$action_msg,$failed); } + egw_framework::refresh_opener($msg, 'admin'); + $msg = ''; } } $content['msg'] = $msg; From 5d36da80623fac58d302674f64c5b8bf2a7d049d Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 27 Jan 2015 22:43:17 +0000 Subject: [PATCH 31/42] Check to make sure found resource is an array instead of a column flag. Fixes fatal error when deleting a category. --- resources/inc/class.resources_hooks.inc.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/inc/class.resources_hooks.inc.php b/resources/inc/class.resources_hooks.inc.php index ebce431b03..3035d6240c 100644 --- a/resources/inc/class.resources_hooks.inc.php +++ b/resources/inc/class.resources_hooks.inc.php @@ -152,10 +152,14 @@ class resources_hooks $query = array('filter' => $args['cat_id']); $bo = new resources_bo(); $bo->get_rows($query, $resources, $readonly); + foreach($resources as $resource) { - $resource['cat_id'] = $new_cat_id; - $bo->save($resource); + if(is_array($resource)) + { + $resource['cat_id'] = $new_cat_id; + $bo->save($resource); + } } } From 69ae4efb47f8483b8a6de74b38c6a00c5e3a70e0 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 28 Jan 2015 10:14:52 +0000 Subject: [PATCH 32/42] need to split off domain first, as it could contain app-name part of template eg. stylite.report.xet and https://my.stylite.de/egw/... --- etemplate/js/etemplate2.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 9b64e3069c..37406eb871 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -324,7 +324,17 @@ etemplate2.prototype.load = function(_name, _url, _data, _callback) { this.name = _name; // store top-level template name to have it available in widgets // store template base url, in case initial template is loaded via webdav, to use that for further loads too - this.template_base_url = _url.split(_name.split('.').shift())[0]; + // need to split off domain first, as it could contain app-name part of template eg. stylite.report.xet and https://my.stylite.de/egw/... + if (_url[0] != '/') + { + this.template_base_url = _url.match(/https?:\/\/[^/]+/).shift(); + _url = _url.split(this.template_base_url)[1]; + } + else + { + this.template_base_url = ''; + } + this.template_base_url += _url.split(_name.split('.').shift())[0]; egw().debug("info", "Loaded data", _data); var currentapp = this.app = _data.currentapp || window.egw_appName; From 9c9ef1b3d40b7561b0616acbe6b7847b1a380778 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 28 Jan 2015 10:57:33 +0000 Subject: [PATCH 33/42] Implement fullScreen toggle mode for blueimp gallery plugin --- .../js/jquery/blueimp/css/blueimp-gallery.css | 20 ++++++++++++--- .../blueimp/css/blueimp-gallery.min.css | 2 +- phpgwapi/js/jquery/blueimp/img/fullscreen.png | Bin 0 -> 407 bytes .../js/jquery/blueimp/js/blueimp-gallery.js | 24 +++++++++++++++++- .../jquery/blueimp/js/blueimp-gallery.min.js | 4 +-- 5 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 phpgwapi/js/jquery/blueimp/img/fullscreen.png diff --git a/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.css b/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.css index b860d78646..dc5ef04d26 100644 --- a/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.css +++ b/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.css @@ -163,11 +163,23 @@ .blueimp-gallery-playing > .play-pause { background-position: -15px 0; } +.blueimp-gallery > .fullscreen { + position: absolute; + right: 15px; + bottom: 30px; + width: 15px; + height: 15px; + background: url(../img/fullscreen.png) 0 0 no-repeat; + cursor: pointer; + opacity: 0.5; + display:none; +} .blueimp-gallery > .prev:hover, .blueimp-gallery > .next:hover, .blueimp-gallery > .close:hover, .blueimp-gallery > .title:hover, -.blueimp-gallery > .play-pause:hover { +.blueimp-gallery > .play-pause:hover, +.blueimp-gallery > .fullscreen:hover{ color: #fff; opacity: 1; } @@ -175,7 +187,8 @@ .blueimp-gallery-controls > .next, .blueimp-gallery-controls > .close, .blueimp-gallery-controls > .title, -.blueimp-gallery-controls > .play-pause { +.blueimp-gallery-controls > .play-pause, +.blueimp-gallery-controls > .fullscreen{ display: block; /* Fix z-index issues (controls behind slide element) on Android: */ -webkit-transform: translateZ(0); @@ -195,7 +208,8 @@ .blueimp-gallery > .prev, .blueimp-gallery > .next, .blueimp-gallery > .close, -.blueimp-gallery > .play-pause { +.blueimp-gallery > .play-pause, +.blueimp-gallery > .fullscreen{ -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; diff --git a/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.min.css b/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.min.css index c65eaa0515..79b7883e91 100644 --- a/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.min.css +++ b/phpgwapi/js/jquery/blueimp/css/blueimp-gallery.min.css @@ -1 +1 @@ -@charset "UTF-8";.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{position:absolute;top:0;right:0;bottom:0;left:0;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.slide-content{margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;opacity:1}.blueimp-gallery{position:fixed;z-index:999999;overflow:hidden;background:#000;background:rgba(0,0,0,.9);opacity:0;display:none;direction:ltr;-ms-touch-action:none;touch-action:none}.blueimp-gallery-carousel{position:relative;z-index:auto;margin:1em auto;padding-bottom:56.25%;box-shadow:0 0 10px #000;-ms-touch-action:pan-y;touch-action:pan-y}.blueimp-gallery-display{display:block;opacity:1}.blueimp-gallery>.slides{position:relative;height:100%;overflow:hidden}.blueimp-gallery-carousel>.slides{position:absolute}.blueimp-gallery>.slides>.slide{position:relative;float:left;height:100%;text-align:center;-webkit-transition-timing-function:cubic-bezier(0.645,.045,.355,1);-moz-transition-timing-function:cubic-bezier(0.645,.045,.355,1);-ms-transition-timing-function:cubic-bezier(0.645,.045,.355,1);-o-transition-timing-function:cubic-bezier(0.645,.045,.355,1);transition-timing-function:cubic-bezier(0.645,.045,.355,1)}.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{-webkit-transition:opacity .5s linear;-moz-transition:opacity .5s linear;-ms-transition:opacity .5s linear;-o-transition:opacity .5s linear;transition:opacity .5s linear}.blueimp-gallery>.slides>.slide-loading{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}.blueimp-gallery>.slides>.slide-loading>.slide-content{opacity:0}.blueimp-gallery>.slides>.slide-error{background:url(../img/error.png) center no-repeat}.blueimp-gallery>.slides>.slide-error>.slide-content{display:none}.blueimp-gallery>.prev,.blueimp-gallery>.next{position:absolute;top:50%;left:15px;width:40px;height:40px;margin-top:-23px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-decoration:none;text-shadow:0 0 2px #000;text-align:center;background:#222;background:rgba(0,0,0,.5);-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;cursor:pointer;display:none}.blueimp-gallery>.next{left:auto;right:15px}.blueimp-gallery>.close,.blueimp-gallery>.title{position:absolute;top:15px;left:15px;margin:0 40px 0 0;font-size:20px;line-height:30px;color:#fff;text-shadow:0 0 2px #000;opacity:.8;display:none}.blueimp-gallery>.close{padding:15px;right:15px;left:auto;margin:-15px;font-size:30px;text-decoration:none;cursor:pointer}.blueimp-gallery>.play-pause{position:absolute;right:15px;bottom:15px;width:15px;height:15px;background:url(../img/play-pause.png) 0 0 no-repeat;cursor:pointer;opacity:.5;display:none}.blueimp-gallery-playing>.play-pause{background-position:-15px 0}.blueimp-gallery>.prev:hover,.blueimp-gallery>.next:hover,.blueimp-gallery>.close:hover,.blueimp-gallery>.title:hover,.blueimp-gallery>.play-pause:hover{color:#fff;opacity:1}.blueimp-gallery-controls>.prev,.blueimp-gallery-controls>.next,.blueimp-gallery-controls>.close,.blueimp-gallery-controls>.title,.blueimp-gallery-controls>.play-pause{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-single>.prev,.blueimp-gallery-left>.prev,.blueimp-gallery-single>.next,.blueimp-gallery-right>.next,.blueimp-gallery-single>.play-pause{display:none}.blueimp-gallery>.slides>.slide>.slide-content,.blueimp-gallery>.prev,.blueimp-gallery>.next,.blueimp-gallery>.close,.blueimp-gallery>.play-pause{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body:last-child .blueimp-gallery>.slides>.slide-error{background-image:url(../img/error.svg)}body:last-child .blueimp-gallery>.play-pause{width:20px;height:20px;background-size:40px 20px;background-image:url(../img/play-pause.svg)}body:last-child .blueimp-gallery-playing>.play-pause{background-position:-20px 0}*+html .blueimp-gallery>.slides>.slide{min-height:300px}*+html .blueimp-gallery>.slides>.slide>.slide-content{position:relative}@charset "UTF-8";.blueimp-gallery>.indicator{position:absolute;top:auto;right:15px;bottom:15px;left:15px;margin:0 40px;padding:0;list-style:none;text-align:center;line-height:10px;display:none}.blueimp-gallery>.indicator>li{display:inline-block;width:9px;height:9px;margin:6px 3px 0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:1px solid transparent;background:#ccc;background:rgba(255,255,255,.25)center no-repeat;border-radius:5px;box-shadow:0 0 2px #000;opacity:.5;cursor:pointer}.blueimp-gallery>.indicator>li:hover,.blueimp-gallery>.indicator>.active{background-color:#fff;border-color:#fff;opacity:1}.blueimp-gallery-controls>.indicator{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-single>.indicator{display:none}.blueimp-gallery>.indicator{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}*+html .blueimp-gallery>.indicator>li{display:inline}@charset "UTF-8";.blueimp-gallery>.slides>.slide>.video-content>img{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.video-content>video{position:absolute;top:0;left:0;width:100%;height:100%}.blueimp-gallery>.slides>.slide>.video-content>iframe{position:absolute;top:100%;left:0;width:100%;height:100%;border:none}.blueimp-gallery>.slides>.slide>.video-playing>iframe{top:0}.blueimp-gallery>.slides>.slide>.video-content>a{position:absolute;top:50%;right:0;left:0;margin:-64px auto 0;width:128px;height:128px;background:url(../img/video-play.png) center no-repeat;opacity:.8;cursor:pointer}.blueimp-gallery>.slides>.slide>.video-content>a:hover{opacity:1}.blueimp-gallery>.slides>.slide>.video-playing>a,.blueimp-gallery>.slides>.slide>.video-playing>img{display:none}.blueimp-gallery>.slides>.slide>.video-content>video{display:none}.blueimp-gallery>.slides>.slide>.video-playing>video{display:block}.blueimp-gallery>.slides>.slide>.video-loading>a{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}body:last-child .blueimp-gallery>.slides>.slide>.video-content:not(.video-loading)>a{background-image:url(../img/video-play.svg)}*+html .blueimp-gallery>.slides>.slide>.video-content{height:100%}*+html .blueimp-gallery>.slides>.slide>.video-content>a{left:50%;margin-left:-64px} \ No newline at end of file +@charset "UTF-8";.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{position:absolute;top:0;right:0;bottom:0;left:0;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.slide-content{margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;opacity:1}.blueimp-gallery{position:fixed;z-index:999999;overflow:hidden;background:#000;background:rgba(0,0,0,0.9);opacity:0;display:none;direction:ltr;-ms-touch-action:none;touch-action:none}.blueimp-gallery-carousel{position:relative;z-index:auto;margin:1em auto;padding-bottom:56.25%;box-shadow:0 0 10px #000;-ms-touch-action:pan-y;touch-action:pan-y}.blueimp-gallery-display{display:block;opacity:1}.blueimp-gallery>.slides{position:relative;height:100%;overflow:hidden}.blueimp-gallery-carousel>.slides{position:absolute}.blueimp-gallery>.slides>.slide{position:relative;float:left;height:100%;text-align:center;-webkit-transition-timing-function:cubic-bezier(0.645,0.045,0.355,1.000);-moz-transition-timing-function:cubic-bezier(0.645,0.045,0.355,1.000);-ms-transition-timing-function:cubic-bezier(0.645,0.045,0.355,1.000);-o-transition-timing-function:cubic-bezier(0.645,0.045,0.355,1.000);transition-timing-function:cubic-bezier(0.645,0.045,0.355,1.000)}.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{-webkit-transition:opacity .5s linear;-moz-transition:opacity .5s linear;-ms-transition:opacity .5s linear;-o-transition:opacity .5s linear;transition:opacity .5s linear}.blueimp-gallery>.slides>.slide-loading{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}.blueimp-gallery>.slides>.slide-loading>.slide-content{opacity:0}.blueimp-gallery>.slides>.slide-error{background:url(../img/error.png) center no-repeat}.blueimp-gallery>.slides>.slide-error>.slide-content{display:none}.blueimp-gallery>.prev,.blueimp-gallery>.next{position:absolute;top:50%;left:15px;width:40px;height:40px;margin-top:-23px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-decoration:none;text-shadow:0 0 2px #000;text-align:center;background:#222;background:rgba(0,0,0,0.5);-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;cursor:pointer;display:none}.blueimp-gallery>.next{left:auto;right:15px}.blueimp-gallery>.close,.blueimp-gallery>.title{position:absolute;top:15px;left:15px;margin:0 40px 0 0;font-size:20px;line-height:30px;color:#fff;text-shadow:0 0 2px #000;opacity:.8;display:none}.blueimp-gallery>.close{padding:15px;right:15px;left:auto;margin:-15px;font-size:30px;text-decoration:none;cursor:pointer}.blueimp-gallery>.play-pause{position:absolute;right:15px;bottom:15px;width:15px;height:15px;background:url(../img/play-pause.png) 0 0 no-repeat;cursor:pointer;opacity:.5;display:none}.blueimp-gallery-playing>.play-pause{background-position:-15px 0}.blueimp-gallery>.fullscreen{position:absolute;right:15px;bottom:30px;width:15px;height:15px;background:url(../img/fullscreen.png) 0 0 no-repeat;cursor:pointer;opacity:.5;display:none}.blueimp-gallery>.prev:hover,.blueimp-gallery>.next:hover,.blueimp-gallery>.close:hover,.blueimp-gallery>.title:hover,.blueimp-gallery>.play-pause:hover,.blueimp-gallery>.fullscreen:hover{color:#fff;opacity:1}.blueimp-gallery-controls>.prev,.blueimp-gallery-controls>.next,.blueimp-gallery-controls>.close,.blueimp-gallery-controls>.title,.blueimp-gallery-controls>.play-pause,.blueimp-gallery-controls>.fullscreen{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-single>.prev,.blueimp-gallery-left>.prev,.blueimp-gallery-single>.next,.blueimp-gallery-right>.next,.blueimp-gallery-single>.play-pause{display:none}.blueimp-gallery>.slides>.slide>.slide-content,.blueimp-gallery>.prev,.blueimp-gallery>.next,.blueimp-gallery>.close,.blueimp-gallery>.play-pause,.blueimp-gallery>.fullscreen{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body:last-child .blueimp-gallery>.slides>.slide-error{background-image:url(../img/error.svg)}body:last-child .blueimp-gallery>.play-pause{width:20px;height:20px;background-size:40px 20px;background-image:url(../img/play-pause.svg)}body:last-child .blueimp-gallery-playing>.play-pause{background-position:-20px 0}*+html .blueimp-gallery>.slides>.slide{min-height:300px}*+html .blueimp-gallery>.slides>.slide>.slide-content{position:relative}@charset "UTF-8";.blueimp-gallery>.indicator{position:absolute;top:auto;right:15px;bottom:15px;left:15px;margin:0 40px;padding:0;list-style:none;text-align:center;line-height:10px;display:none}.blueimp-gallery>.indicator>li{display:inline-block;width:9px;height:9px;margin:6px 3px 0 3px;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:1px solid transparent;background:#ccc;background:rgba(255,255,255,0.25) center no-repeat;border-radius:5px;box-shadow:0 0 2px #000;opacity:.5;cursor:pointer}.blueimp-gallery>.indicator>li:hover,.blueimp-gallery>.indicator>.active{background-color:#fff;border-color:#fff;opacity:1}.blueimp-gallery-controls>.indicator{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-single>.indicator{display:none}.blueimp-gallery>.indicator{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}*+html .blueimp-gallery>.indicator>li{display:inline}@charset "UTF-8";.blueimp-gallery>.slides>.slide>.video-content>img{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.video-content>video{position:absolute;top:0;left:0;width:100%;height:100%}.blueimp-gallery>.slides>.slide>.video-content>iframe{position:absolute;top:100%;left:0;width:100%;height:100%;border:0}.blueimp-gallery>.slides>.slide>.video-playing>iframe{top:0}.blueimp-gallery>.slides>.slide>.video-content>a{position:absolute;top:50%;right:0;left:0;margin:-64px auto 0;width:128px;height:128px;background:url(../img/video-play.png) center no-repeat;opacity:.8;cursor:pointer}.blueimp-gallery>.slides>.slide>.video-content>a:hover{opacity:1}.blueimp-gallery>.slides>.slide>.video-playing>a,.blueimp-gallery>.slides>.slide>.video-playing>img{display:none}.blueimp-gallery>.slides>.slide>.video-content>video{display:none}.blueimp-gallery>.slides>.slide>.video-playing>video{display:block}.blueimp-gallery>.slides>.slide>.video-loading>a{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}body:last-child .blueimp-gallery>.slides>.slide>.video-content:not(.video-loading)>a{background-image:url(../img/video-play.svg)}*+html .blueimp-gallery>.slides>.slide>.video-content{height:100%}*+html .blueimp-gallery>.slides>.slide>.video-content>a{left:50%;margin-left:-64px} \ No newline at end of file diff --git a/phpgwapi/js/jquery/blueimp/img/fullscreen.png b/phpgwapi/js/jquery/blueimp/img/fullscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..f6992b0c7bc89918768c81238ea182e2486fd61c GIT binary patch literal 407 zcmV;I0cie-P)XABBw<;LFni^_Xy-DXnZ7<^17hpTUAut5qfDhnU zeTj%^UTK+sp!TaB<#|O!l)-xGCz}H^wE++jmb?^GBwz#RW$oVrm%w+1_Y$8%Z2<$I zmL6)TMMOMD#9c&~L|jS9HDI8&>OemUbgrHOpTHGxU8%MW?5bAodJ*OzM1&rQmn2z%GBIc+iH>FBek0^MkgcftG!a~3f?KlRf?a:f)-e-1),this.slideWidth*c,0);a=this.circle(a),this.move(f,this.slideWidth*c,b),this.move(a,0,b),this.options.continuous&&this.move(this.circle(a-c),-(this.slideWidth*c),0)}else a=this.circle(a),this.animate(f*-this.slideWidth,a*-this.slideWidth,b);this.onslide(a)}},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){(this.options.continuous||this.index)&&this.slide(this.index-1)},next:function(){(this.options.continuous||this.index1&&(this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(a,c){b.animationFrameId=b.requestAnimationFrame.call(window,function(){b.slide(a,c)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)),this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout),this.interval=null,this.container.removeClass(this.options.playingClass)},add:function(a){var b;for(a.concat||(a=Array.prototype.slice.call(a)),this.list.concat||(this.list=Array.prototype.slice.call(this.list)),this.list=this.list.concat(a),this.num=this.list.length,this.num>2&&null===this.options.continuous&&(this.options.continuous=!0,this.container.removeClass(this.options.leftEdgeClass)),this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass),b=this.num-a.length;bc?(d.slidesContainer[0].style.left=b+"px",d.ontransitionend(),void window.clearInterval(f)):void(d.slidesContainer[0].style.left=(b-a)*(Math.floor(g/c*100)/100)+a+"px")},4)},preventDefault:function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},onresize:function(){this.initSlides(!0)},onmousedown:function(a){a.which&&1===a.which&&"VIDEO"!==a.target.nodeName&&(a.preventDefault(),(a.originalEvent||a).touches=[{pageX:a.pageX,pageY:a.pageY}],this.ontouchstart(a))},onmousemove:function(a){this.touchStart&&((a.originalEvent||a).touches=[{pageX:a.pageX,pageY:a.pageY}],this.ontouchmove(a))},onmouseup:function(a){this.touchStart&&(this.ontouchend(a),delete this.touchStart)},onmouseout:function(b){if(this.touchStart){var c=b.target,d=b.relatedTarget;(!d||d!==c&&!a.contains(c,d))&&this.onmouseup(b)}},ontouchstart:function(a){this.options.stopTouchEventsPropagation&&this.stopPropagation(a);var b=(a.originalEvent||a).touches[0];this.touchStart={x:b.pageX,y:b.pageY,time:Date.now()},this.isScrolling=void 0,this.touchDelta={}},ontouchmove:function(a){this.options.stopTouchEventsPropagation&&this.stopPropagation(a);var b,c,d=(a.originalEvent||a).touches[0],e=(a.originalEvent||a).scale,f=this.index;if(!(d.length>1||e&&1!==e))if(this.options.disableScroll&&a.preventDefault(),this.touchDelta={x:d.pageX-this.touchStart.x,y:d.pageY-this.touchStart.y},b=this.touchDelta.x,void 0===this.isScrolling&&(this.isScrolling=this.isScrolling||Math.abs(b)0||f===this.num-1&&0>b?Math.abs(b)/this.slideWidth+1:1,c=[f],f&&c.push(f-1),f20||Math.abs(this.touchDelta.x)>i/2,l=!g&&this.touchDelta.x>0||g===this.num-1&&this.touchDelta.x<0,m=!k&&this.options.closeOnSwipeUpOrDown&&(j&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2);this.options.continuous&&(l=!1),b=this.touchDelta.x<0?-1:1,this.isScrolling?m?this.close():this.translateY(g,0,h):k&&!l?(c=g+b,d=g-b,e=i*b,f=-i*b,this.options.continuous?(this.move(this.circle(c),e,0),this.move(this.circle(g-2*b),f,0)):c>=0&&cthis.container[0].clientHeight&&(d.style.maxHeight=this.container[0].clientHeight),this.interval&&this.slides[this.index]===e&&this.play(),this.setTimeout(this.options.onslidecomplete,[c,e]))},onload:function(a){this.oncomplete(a)},onerror:function(a){this.oncomplete(a)},onkeydown:function(a){switch(a.which||a.keyCode){case 13:this.options.toggleControlsOnReturn&&(this.preventDefault(a),this.toggleControls());break;case 27:this.options.closeOnEscape&&this.close();break;case 32:this.options.toggleSlideshowOnSpace&&(this.preventDefault(a),this.toggleSlideshow());break;case 37:this.options.enableKeyboardNavigation&&(this.preventDefault(a),this.prev());break;case 39:this.options.enableKeyboardNavigation&&(this.preventDefault(a),this.next())}},handleClick:function(b){var c=this.options,d=b.target||b.srcElement,e=d.parentNode,f=function(b){return a(d).hasClass(b)||a(e).hasClass(b)};f(c.toggleClass)?(this.preventDefault(b),this.toggleControls()):f(c.prevClass)?(this.preventDefault(b),this.prev()):f(c.nextClass)?(this.preventDefault(b),this.next()):f(c.closeClass)?(this.preventDefault(b),this.close()):f(c.playPauseClass)?(this.preventDefault(b),this.toggleSlideshow()):e===this.slidesContainer[0]?(this.preventDefault(b),c.closeOnSlideClick?this.close():this.toggleControls()):e.parentNode&&e.parentNode===this.slidesContainer[0]&&(this.preventDefault(b),this.toggleControls())},onclick:function(a){return this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)?void delete this.touchDelta:this.handleClick(a)},updateEdgeClasses:function(a){a?this.container.removeClass(this.options.leftEdgeClass):this.container.addClass(this.options.leftEdgeClass),a===this.num-1?this.container.addClass(this.options.rightEdgeClass):this.container.removeClass(this.options.rightEdgeClass)},handleSlide:function(a){this.options.continuous||this.updateEdgeClasses(a),this.loadElements(a),this.options.unloadElements&&this.unloadElements(a),this.setTitle(a)},onslide:function(a){this.index=a,this.handleSlide(a),this.setTimeout(this.options.onslide,[a,this.slides[a]])},setTitle:function(a){var b=this.slides[a].firstChild.title,c=this.titleElement;c.length&&(this.titleElement.empty(),b&&c[0].appendChild(document.createTextNode(b)))},setTimeout:function(a,b,c){var d=this;return a&&window.setTimeout(function(){a.apply(d,b||[])},c||0)},imageFactory:function(b,c){var d,e,f,g=this,h=this.imagePrototype.cloneNode(!1),i=b,j=this.options.stretchImages,k=function(b){if(!d){if(b={type:b.type,target:e},!e.parentNode)return g.setTimeout(k,[b]);d=!0,a(h).off("load error",k),j&&"load"===b.type&&(e.style.background='url("'+i+'") center no-repeat',e.style.backgroundSize=j),c(b)}};return"string"!=typeof i&&(i=this.getItemProperty(b,this.options.urlProperty),f=this.getItemProperty(b,this.options.titleProperty)),j===!0&&(j="contain"),j=this.support.backgroundSize&&this.support.backgroundSize[j]&&j,j?e=this.elementPrototype.cloneNode(!1):(e=h,h.draggable=!1),f&&(e.title=f),a(h).on("load error",k),h.src=i,e},createElement:function(b,c){var d=b&&this.getItemProperty(b,this.options.typeProperty),e=d&&this[d.split("/")[0]+"Factory"]||this.imageFactory,f=b&&e.call(this,b,c);return f||(f=this.elementPrototype.cloneNode(!1),this.setTimeout(c,[{type:"error",target:f}])),a(f).addClass(this.options.slideContentClass),f},loadElement:function(b){this.elements[b]||(this.slides[b].firstChild?this.elements[b]=a(this.slides[b]).hasClass(this.options.slideErrorClass)?3:2:(this.elements[b]=1,a(this.slides[b]).addClass(this.options.slideLoadingClass),this.slides[b].appendChild(this.createElement(this.list[b],this.proxyListener))))},loadElements:function(a){var b,c=Math.min(this.num,2*this.options.preloadRange+1),d=a;for(b=0;c>b;b+=1)d+=b*(b%2===0?-1:1),d=this.circle(d),this.loadElement(d)},unloadElements:function(a){var b,c,d;for(b in this.elements)this.elements.hasOwnProperty(b)&&(d=Math.abs(a-b),d>this.options.preloadRange&&d+this.options.preloadRangea?-this.slideWidth:this.indexindex?to:index)-diff-1),this.slideWidth*direction,0)}to=this.circle(to);this.move(index,this.slideWidth*direction,speed);this.move(to,0,speed);if(this.options.continuous){this.move(this.circle(to-direction),-(this.slideWidth*direction),0)}}else{to=this.circle(to);this.animate(index*-this.slideWidth,to*-this.slideWidth,speed)}this.onslide(to)},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){if(this.options.continuous||this.index){this.slide(this.index-1)}},next:function(){if(this.options.continuous||this.index1){this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(to,speed){that.animationFrameId=that.requestAnimationFrame.call(window,function(){that.slide(to,speed)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)}this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout);this.interval=null;this.container.removeClass(this.options.playingClass)},add:function(list){var i;if(!list.concat){list=Array.prototype.slice.call(list)}if(!this.list.concat){this.list=Array.prototype.slice.call(this.list)}this.list=this.list.concat(list);this.num=this.list.length;if(this.num>2&&this.options.continuous===null){this.options.continuous=true;this.container.removeClass(this.options.leftEdgeClass)}this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass);for(i=this.num-list.length;ispeed){that.slidesContainer[0].style.left=to+"px";that.ontransitionend();window.clearInterval(timer);return}that.slidesContainer[0].style.left=(to-from)*(Math.floor(timeElap/speed*100)/100)+from+"px"},4)},preventDefault:function(event){if(event.preventDefault){event.preventDefault()}else{event.returnValue=false}},stopPropagation:function(event){if(event.stopPropagation){event.stopPropagation()}else{event.cancelBubble=true}},onresize:function(){this.initSlides(true)},onmousedown:function(event){if(event.which&&event.which===1&&event.target.nodeName!=="VIDEO"){event.preventDefault();(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchstart(event)}},onmousemove:function(event){if(this.touchStart){(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchmove(event)}},onmouseup:function(event){if(this.touchStart){this.ontouchend(event);delete this.touchStart}},onmouseout:function(event){if(this.touchStart){var target=event.target,related=event.relatedTarget;if(!related||related!==target&&!$.contains(target,related)){this.onmouseup(event)}}},ontouchstart:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0];this.touchStart={x:touches.pageX,y:touches.pageY,time:Date.now()};this.isScrolling=undefined;this.touchDelta={}},ontouchmove:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0],scale=(event.originalEvent||event).scale,index=this.index,touchDeltaX,indices;if(touches.length>1||scale&&scale!==1){return}if(this.options.disableScroll){event.preventDefault()}this.touchDelta={x:touches.pageX-this.touchStart.x,y:touches.pageY-this.touchStart.y};touchDeltaX=this.touchDelta.x;if(this.isScrolling===undefined){this.isScrolling=this.isScrolling||Math.abs(touchDeltaX)0||index===this.num-1&&touchDeltaX<0?Math.abs(touchDeltaX)/this.slideWidth+1:1);indices=[index];if(index){indices.push(index-1)}if(index20||Math.abs(this.touchDelta.x)>slideWidth/2,isPastBounds=!index&&this.touchDelta.x>0||index===this.num-1&&this.touchDelta.x<0,isValidClose=!isValidSlide&&this.options.closeOnSwipeUpOrDown&&(isShortDuration&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2),direction,indexForward,indexBackward,distanceForward,distanceBackward;if(this.options.continuous){isPastBounds=false}direction=this.touchDelta.x<0?-1:1;if(!this.isScrolling){if(isValidSlide&&!isPastBounds){indexForward=index+direction;indexBackward=index-direction;distanceForward=slideWidth*direction;distanceBackward=-slideWidth*direction;if(this.options.continuous){this.move(this.circle(indexForward),distanceForward,0);this.move(this.circle(index-2*direction),distanceBackward,0)}else if(indexForward>=0&&indexForwardthis.container[0].clientHeight){target.style.maxHeight=this.container[0].clientHeight}if(this.interval&&this.slides[this.index]===parent){this.play()}this.setTimeout(this.options.onslidecomplete,[index,parent])},onload:function(event){this.oncomplete(event)},onerror:function(event){this.oncomplete(event)},onkeydown:function(event){switch(event.which||event.keyCode){case 13:if(this.options.toggleControlsOnReturn){this.preventDefault(event);this.toggleControls()}break;case 27:if(this.options.closeOnEscape){this.close()}break;case 32:if(this.options.toggleSlideshowOnSpace){this.preventDefault(event);this.toggleSlideshow()}break;case 37:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.prev()}break;case 39:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.next()}break}},handleClick:function(event){var options=this.options,target=event.target||event.srcElement,parent=target.parentNode,isTarget=function(className){return $(target).hasClass(className)||$(parent).hasClass(className)};if(isTarget(options.toggleClass)){this.preventDefault(event);this.toggleControls()}else if(isTarget(options.prevClass)){this.preventDefault(event);this.prev()}else if(isTarget(options.nextClass)){this.preventDefault(event);this.next()}else if(isTarget(options.closeClass)){this.preventDefault(event);this.close()}else if(isTarget(options.playPauseClass)){this.preventDefault(event);this.toggleSlideshow()}else if(isTarget(options.fullscreenClass)){this.preventDefault(event);this.toggleFullscreen()}else if(parent===this.slidesContainer[0]){this.preventDefault(event);if(options.closeOnSlideClick){this.close()}else{this.toggleControls()}}else if(parent.parentNode&&parent.parentNode===this.slidesContainer[0]){this.preventDefault(event);this.toggleControls()}},onclick:function(event){if(this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)){delete this.touchDelta;return}return this.handleClick(event)},updateEdgeClasses:function(index){if(!index){this.container.addClass(this.options.leftEdgeClass)}else{this.container.removeClass(this.options.leftEdgeClass)}if(index===this.num-1){this.container.addClass(this.options.rightEdgeClass)}else{this.container.removeClass(this.options.rightEdgeClass)}},handleSlide:function(index){if(!this.options.continuous){this.updateEdgeClasses(index)}this.loadElements(index);if(this.options.unloadElements){this.unloadElements(index)}this.setTitle(index)},onslide:function(index){this.index=index;this.handleSlide(index);this.setTimeout(this.options.onslide,[index,this.slides[index]])},setTitle:function(index){var text=this.slides[index].firstChild.title,titleElement=this.titleElement;if(titleElement.length){this.titleElement.empty();if(text){titleElement[0].appendChild(document.createTextNode(text))}}},setTimeout:function(func,args,wait){var that=this;return func&&window.setTimeout(function(){func.apply(that,args||[])},wait||0)},imageFactory:function(obj,callback){var that=this,img=this.imagePrototype.cloneNode(false),url=obj,backgroundSize=this.options.stretchImages,called,element,callbackWrapper=function(event){if(!called){event={type:event.type,target:element};if(!element.parentNode){return that.setTimeout(callbackWrapper,[event])}called=true;$(img).off("load error",callbackWrapper);if(backgroundSize){if(event.type==="load"){element.style.background='url("'+url+'") center no-repeat';element.style.backgroundSize=backgroundSize}}callback(event)}},title;if(typeof url!=="string"){url=this.getItemProperty(obj,this.options.urlProperty);title=this.getItemProperty(obj,this.options.titleProperty)}if(backgroundSize===true){backgroundSize="contain"}backgroundSize=this.support.backgroundSize&&this.support.backgroundSize[backgroundSize]&&backgroundSize;if(backgroundSize){element=this.elementPrototype.cloneNode(false)}else{element=img;img.draggable=false}if(title){element.title=title}$(img).on("load error",callbackWrapper);img.src=url;return element},createElement:function(obj,callback){var type=obj&&this.getItemProperty(obj,this.options.typeProperty),factory=type&&this[type.split("/")[0]+"Factory"]||this.imageFactory,element=obj&&factory.call(this,obj,callback);if(!element){element=this.elementPrototype.cloneNode(false);this.setTimeout(callback,[{type:"error",target:element}])}$(element).addClass(this.options.slideContentClass);return element},loadElement:function(index){if(!this.elements[index]){if(this.slides[index].firstChild){this.elements[index]=$(this.slides[index]).hasClass(this.options.slideErrorClass)?3:2}else{this.elements[index]=1;$(this.slides[index]).addClass(this.options.slideLoadingClass);this.slides[index].appendChild(this.createElement(this.list[index],this.proxyListener))}}},loadElements:function(index){var limit=Math.min(this.num,this.options.preloadRange*2+1),j=index,i;for(i=0;ithis.options.preloadRange&&diff+this.options.preloadRangeindex?-this.slideWidth:this.index Date: Wed, 28 Jan 2015 10:59:05 +0000 Subject: [PATCH 34/42] Activate fullscreen mode for expose view --- etemplate/js/expose.js | 8 +++++--- etemplate/templates/default/etemplate2.css | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/etemplate/js/expose.js b/etemplate/js/expose.js index d8efd75cd3..5f01571ce4 100644 --- a/etemplate/js/expose.js +++ b/etemplate/js/expose.js @@ -12,7 +12,7 @@ /*egw:uses jquery.jquery; - /phpgwapi/js/jquery/blueimp/js/jquery.blueimp-gallery.min.js; + /phpgwapi/js/jquery/blueimp/js/blueimp-gallery.min.js; */ /** @@ -48,7 +48,7 @@ function expose (widget) }; // For filtering to only show things we can handle - var mime_regex = new RegExp(/video\/|image|audio\//); + var mime_regex = new RegExp(/video\/|image\/|audio\//); // Only one gallery var gallery = null; @@ -234,6 +234,8 @@ function expose (widget) closeClass: 'close', // The class for the "play-pause" toggle control: playPauseClass: 'play-pause', + // The class to add for fullscreen button option + fullscreenClass:'fullscreen', // The list object property (or data attribute) with the object type: typeProperty: 'type', // The list object property (or data attribute) with the object title: @@ -347,7 +349,7 @@ function expose (widget) // Gallery Main DIV container var $expose_node = jQuery(document.createElement('div')).attr({id:"blueimp-gallery", class:"blueimp-gallery"}); // Create Gallery DOM NODE - $expose_node.append('

×
    '); + $expose_node.append('

    ×
      '); // Append the gallery Node to DOM $body.append($expose_node); } diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index 48611bb183..1e91229646 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -1810,6 +1810,11 @@ span.et2_egw_action_ddHelper_itemsCnt { right: 42px; top: 23px; } +/*fullScreen button*/ +.blueimp-gallery>.fullscreen{ + right: 75px; + top: 25px; +} /*Give room to Carousel indicator when the gallery controls is on*/ .blueimp-gallery-controls>.slides { height:85%; From e673858e40fcbe9c58716b49b4358b43276ba1a0 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 28 Jan 2015 11:21:20 +0000 Subject: [PATCH 35/42] Fix regex for audio mime type --- etemplate/js/et2_widget_vfs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etemplate/js/et2_widget_vfs.js b/etemplate/js/et2_widget_vfs.js index 96169214c0..c9075ca95b 100644 --- a/etemplate/js/et2_widget_vfs.js +++ b/etemplate/js/et2_widget_vfs.js @@ -327,7 +327,7 @@ var et2_vfsMime = expose(et2_valueWidget.extend([et2_IDetachedDOM], { var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl:egw.webserverUrl; var mediaContent = []; - if (_value && _value.mime && _value.mime.match(/video|audio/,'ig')) + if (_value && _value.mime && _value.mime.match(/video\/|audio\//,'ig')) { mediaContent = [{ title: _value.name, From dd7b4dec558dfb48cf2cd4520cad2a7c9a66f119 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 28 Jan 2015 17:42:14 +0000 Subject: [PATCH 36/42] Add two options to blueimp gallery plugin: - thumbnailsTagIndicators: in order to set custom tag as indicator element. Default value: 'li' - thumbnailWithImgTag: in order to set indicator with img child as thumbnail. Default value: false --- .../blueimp/js/blueimp-gallery-indicator.js | 26 +++++++++++++++---- .../jquery/blueimp/js/blueimp-gallery.min.js | 4 +-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/phpgwapi/js/jquery/blueimp/js/blueimp-gallery-indicator.js b/phpgwapi/js/jquery/blueimp/js/blueimp-gallery-indicator.js index cd0cf7d5ea..7f69d851bf 100644 --- a/phpgwapi/js/jquery/blueimp/js/blueimp-gallery-indicator.js +++ b/phpgwapi/js/jquery/blueimp/js/blueimp-gallery-indicator.js @@ -38,7 +38,11 @@ // used as alternative to a thumbnail child element: thumbnailProperty: 'thumbnail', // Defines if the gallery indicators should display a thumbnail: - thumbnailIndicators: true + thumbnailIndicators: true, + // The tag name of thumbnails indicators + thumbnailsTagIndicators: 'li', + //thumbnail with image tag + thumbnailWithImgTag: false }); var initSlides = Gallery.prototype.initSlides, @@ -51,7 +55,7 @@ $.extend(Gallery.prototype, { createIndicator: function (obj) { - var indicator = this.indicatorPrototype.cloneNode(false), + var indicator = this.indicatorPrototype.cloneNode(this.options.thumbnailWithImgTag), title = this.getItemProperty(obj, this.options.titleProperty), thumbnailProperty = this.options.thumbnailProperty, thumbnailUrl, @@ -64,7 +68,18 @@ thumbnailUrl = this.getItemProperty(obj, thumbnailProperty); } if (thumbnailUrl) { - indicator.style.backgroundImage = 'url("' + thumbnailUrl + '")'; + if (this.options.thumbnailsTagIndicators == 'img') + { + indicator.src = thumbnailUrl; + } + else if (this.options.thumbnailWithImgTag) + { + indicator.children[0].src = thumbnailUrl; + } + else + { + indicator.style.backgroundImage = 'url("' + thumbnailUrl + '")'; + } } } if (title) { @@ -100,8 +115,9 @@ this.options.indicatorContainer ); if (this.indicatorContainer.length) { - this.indicatorPrototype = document.createElement('li'); - this.indicators = this.indicatorContainer[0].children; + this.indicatorPrototype = document.createElement(this.options.thumbnailsTagIndicators); + if (this.options.thumbnailWithImgTag) this.indicatorPrototype.appendChild(document.createElement('img')); + this.indicators = this.indicatorContainer[0].children; } } initSlides.call(this, reload); diff --git a/phpgwapi/js/jquery/blueimp/js/blueimp-gallery.min.js b/phpgwapi/js/jquery/blueimp/js/blueimp-gallery.min.js index 648c3fa74b..1d36dbd698 100644 --- a/phpgwapi/js/jquery/blueimp/js/blueimp-gallery.min.js +++ b/phpgwapi/js/jquery/blueimp/js/blueimp-gallery.min.js @@ -1,2 +1,2 @@ -(function(factory){"use strict";if(typeof define==="function"&&define.amd){define(["./blueimp-helper"],factory)}else{window.blueimp=window.blueimp||{};window.blueimp.Gallery=factory(window.blueimp.helper||window.jQuery)}})(function($){"use strict";function Gallery(list,options){if(document.body.style.maxHeight===undefined){return null}if(!this||this.options!==Gallery.prototype.options){return new Gallery(list,options)}if(!list||!list.length){this.console.log("blueimp Gallery: No or empty list provided as first argument.",list);return}this.list=list;this.num=list.length;this.initOptions(options);this.initialize()}$.extend(Gallery.prototype,{options:{container:"#blueimp-gallery",slidesContainer:"div",titleElement:"h3",displayClass:"blueimp-gallery-display",controlsClass:"blueimp-gallery-controls",singleClass:"blueimp-gallery-single",leftEdgeClass:"blueimp-gallery-left",rightEdgeClass:"blueimp-gallery-right",playingClass:"blueimp-gallery-playing",slideClass:"slide",slideLoadingClass:"slide-loading",slideErrorClass:"slide-error",slideContentClass:"slide-content",toggleClass:"toggle",prevClass:"prev",nextClass:"next",closeClass:"close",playPauseClass:"play-pause",fullscreenClass:"fullscreen",typeProperty:"type",titleProperty:"title",urlProperty:"href",displayTransition:true,clearSlides:true,stretchImages:false,toggleControlsOnReturn:true,toggleSlideshowOnSpace:true,toggleFullscreenOnSlideShow:true,enableKeyboardNavigation:true,closeOnEscape:true,closeOnSlideClick:true,closeOnSwipeUpOrDown:true,emulateTouchEvents:true,stopTouchEventsPropagation:false,hidePageScrollbars:true,disableScroll:true,carousel:false,continuous:true,unloadElements:true,startSlideshow:false,slideshowInterval:5e3,index:0,preloadRange:2,transitionSpeed:400,slideshowTransitionSpeed:undefined,event:undefined,onopen:undefined,onopened:undefined,onslide:undefined,onslideend:undefined,onslidecomplete:undefined,onclose:undefined,onclosed:undefined},carouselOptions:{hidePageScrollbars:false,toggleControlsOnReturn:false,toggleSlideshowOnSpace:false,enableKeyboardNavigation:false,closeOnEscape:false,closeOnSlideClick:false,closeOnSwipeUpOrDown:false,disableScroll:false,startSlideshow:true},console:window.console&&typeof window.console.log==="function"?window.console:{log:function(){}},support:function(element){var support={touch:window.ontouchstart!==undefined||window.DocumentTouch&&document instanceof DocumentTouch},transitions={webkitTransition:{end:"webkitTransitionEnd",prefix:"-webkit-"},MozTransition:{end:"transitionend",prefix:"-moz-"},OTransition:{end:"otransitionend",prefix:"-o-"},transition:{end:"transitionend",prefix:""}},elementTests=function(){var transition=support.transition,prop,translateZ;document.body.appendChild(element);if(transition){prop=transition.name.slice(0,-9)+"ransform";if(element.style[prop]!==undefined){element.style[prop]="translateZ(0)";translateZ=window.getComputedStyle(element).getPropertyValue(transition.prefix+"transform");support.transform={prefix:transition.prefix,name:prop,translate:true,translateZ:!!translateZ&&translateZ!=="none"}}}if(element.style.backgroundSize!==undefined){support.backgroundSize={};element.style.backgroundSize="contain";support.backgroundSize.contain=window.getComputedStyle(element).getPropertyValue("background-size")==="contain";element.style.backgroundSize="cover";support.backgroundSize.cover=window.getComputedStyle(element).getPropertyValue("background-size")==="cover"}document.body.removeChild(element)};(function(support,transitions){var prop;for(prop in transitions){if(transitions.hasOwnProperty(prop)&&element.style[prop]!==undefined){support.transition=transitions[prop];support.transition.name=prop;break}}})(support,transitions);if(document.body){elementTests()}else{$(document).on("DOMContentLoaded",elementTests)}return support}(document.createElement("div")),requestAnimationFrame:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame,initialize:function(){this.initStartIndex();if(this.initWidget()===false){return false}this.initEventListeners();this.onslide(this.index);this.ontransitionend();if(this.options.startSlideshow){this.play()}},slide:function(to,speed){window.clearTimeout(this.timeout);var index=this.index,direction,naturalDirection,diff;if(index===to||this.num===1){return}if(!speed){speed=this.options.transitionSpeed}if(this.support.transform){if(!this.options.continuous){to=this.circle(to)}direction=Math.abs(index-to)/(index-to);if(this.options.continuous){naturalDirection=direction;direction=-this.positions[this.circle(to)]/this.slideWidth;if(direction!==naturalDirection){to=-direction*this.num+to}}diff=Math.abs(index-to)-1;while(diff){diff-=1;this.move(this.circle((to>index?to:index)-diff-1),this.slideWidth*direction,0)}to=this.circle(to);this.move(index,this.slideWidth*direction,speed);this.move(to,0,speed);if(this.options.continuous){this.move(this.circle(to-direction),-(this.slideWidth*direction),0)}}else{to=this.circle(to);this.animate(index*-this.slideWidth,to*-this.slideWidth,speed)}this.onslide(to)},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){if(this.options.continuous||this.index){this.slide(this.index-1)}},next:function(){if(this.options.continuous||this.index1){this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(to,speed){that.animationFrameId=that.requestAnimationFrame.call(window,function(){that.slide(to,speed)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)}this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout);this.interval=null;this.container.removeClass(this.options.playingClass)},add:function(list){var i;if(!list.concat){list=Array.prototype.slice.call(list)}if(!this.list.concat){this.list=Array.prototype.slice.call(this.list)}this.list=this.list.concat(list);this.num=this.list.length;if(this.num>2&&this.options.continuous===null){this.options.continuous=true;this.container.removeClass(this.options.leftEdgeClass)}this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass);for(i=this.num-list.length;ispeed){that.slidesContainer[0].style.left=to+"px";that.ontransitionend();window.clearInterval(timer);return}that.slidesContainer[0].style.left=(to-from)*(Math.floor(timeElap/speed*100)/100)+from+"px"},4)},preventDefault:function(event){if(event.preventDefault){event.preventDefault()}else{event.returnValue=false}},stopPropagation:function(event){if(event.stopPropagation){event.stopPropagation()}else{event.cancelBubble=true}},onresize:function(){this.initSlides(true)},onmousedown:function(event){if(event.which&&event.which===1&&event.target.nodeName!=="VIDEO"){event.preventDefault();(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchstart(event)}},onmousemove:function(event){if(this.touchStart){(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchmove(event)}},onmouseup:function(event){if(this.touchStart){this.ontouchend(event);delete this.touchStart}},onmouseout:function(event){if(this.touchStart){var target=event.target,related=event.relatedTarget;if(!related||related!==target&&!$.contains(target,related)){this.onmouseup(event)}}},ontouchstart:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0];this.touchStart={x:touches.pageX,y:touches.pageY,time:Date.now()};this.isScrolling=undefined;this.touchDelta={}},ontouchmove:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0],scale=(event.originalEvent||event).scale,index=this.index,touchDeltaX,indices;if(touches.length>1||scale&&scale!==1){return}if(this.options.disableScroll){event.preventDefault()}this.touchDelta={x:touches.pageX-this.touchStart.x,y:touches.pageY-this.touchStart.y};touchDeltaX=this.touchDelta.x;if(this.isScrolling===undefined){this.isScrolling=this.isScrolling||Math.abs(touchDeltaX)0||index===this.num-1&&touchDeltaX<0?Math.abs(touchDeltaX)/this.slideWidth+1:1);indices=[index];if(index){indices.push(index-1)}if(index20||Math.abs(this.touchDelta.x)>slideWidth/2,isPastBounds=!index&&this.touchDelta.x>0||index===this.num-1&&this.touchDelta.x<0,isValidClose=!isValidSlide&&this.options.closeOnSwipeUpOrDown&&(isShortDuration&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2),direction,indexForward,indexBackward,distanceForward,distanceBackward;if(this.options.continuous){isPastBounds=false}direction=this.touchDelta.x<0?-1:1;if(!this.isScrolling){if(isValidSlide&&!isPastBounds){indexForward=index+direction;indexBackward=index-direction;distanceForward=slideWidth*direction;distanceBackward=-slideWidth*direction;if(this.options.continuous){this.move(this.circle(indexForward),distanceForward,0);this.move(this.circle(index-2*direction),distanceBackward,0)}else if(indexForward>=0&&indexForwardthis.container[0].clientHeight){target.style.maxHeight=this.container[0].clientHeight}if(this.interval&&this.slides[this.index]===parent){this.play()}this.setTimeout(this.options.onslidecomplete,[index,parent])},onload:function(event){this.oncomplete(event)},onerror:function(event){this.oncomplete(event)},onkeydown:function(event){switch(event.which||event.keyCode){case 13:if(this.options.toggleControlsOnReturn){this.preventDefault(event);this.toggleControls()}break;case 27:if(this.options.closeOnEscape){this.close()}break;case 32:if(this.options.toggleSlideshowOnSpace){this.preventDefault(event);this.toggleSlideshow()}break;case 37:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.prev()}break;case 39:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.next()}break}},handleClick:function(event){var options=this.options,target=event.target||event.srcElement,parent=target.parentNode,isTarget=function(className){return $(target).hasClass(className)||$(parent).hasClass(className)};if(isTarget(options.toggleClass)){this.preventDefault(event);this.toggleControls()}else if(isTarget(options.prevClass)){this.preventDefault(event);this.prev()}else if(isTarget(options.nextClass)){this.preventDefault(event);this.next()}else if(isTarget(options.closeClass)){this.preventDefault(event);this.close()}else if(isTarget(options.playPauseClass)){this.preventDefault(event);this.toggleSlideshow()}else if(isTarget(options.fullscreenClass)){this.preventDefault(event);this.toggleFullscreen()}else if(parent===this.slidesContainer[0]){this.preventDefault(event);if(options.closeOnSlideClick){this.close()}else{this.toggleControls()}}else if(parent.parentNode&&parent.parentNode===this.slidesContainer[0]){this.preventDefault(event);this.toggleControls()}},onclick:function(event){if(this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)){delete this.touchDelta;return}return this.handleClick(event)},updateEdgeClasses:function(index){if(!index){this.container.addClass(this.options.leftEdgeClass)}else{this.container.removeClass(this.options.leftEdgeClass)}if(index===this.num-1){this.container.addClass(this.options.rightEdgeClass)}else{this.container.removeClass(this.options.rightEdgeClass)}},handleSlide:function(index){if(!this.options.continuous){this.updateEdgeClasses(index)}this.loadElements(index);if(this.options.unloadElements){this.unloadElements(index)}this.setTitle(index)},onslide:function(index){this.index=index;this.handleSlide(index);this.setTimeout(this.options.onslide,[index,this.slides[index]])},setTitle:function(index){var text=this.slides[index].firstChild.title,titleElement=this.titleElement;if(titleElement.length){this.titleElement.empty();if(text){titleElement[0].appendChild(document.createTextNode(text))}}},setTimeout:function(func,args,wait){var that=this;return func&&window.setTimeout(function(){func.apply(that,args||[])},wait||0)},imageFactory:function(obj,callback){var that=this,img=this.imagePrototype.cloneNode(false),url=obj,backgroundSize=this.options.stretchImages,called,element,callbackWrapper=function(event){if(!called){event={type:event.type,target:element};if(!element.parentNode){return that.setTimeout(callbackWrapper,[event])}called=true;$(img).off("load error",callbackWrapper);if(backgroundSize){if(event.type==="load"){element.style.background='url("'+url+'") center no-repeat';element.style.backgroundSize=backgroundSize}}callback(event)}},title;if(typeof url!=="string"){url=this.getItemProperty(obj,this.options.urlProperty);title=this.getItemProperty(obj,this.options.titleProperty)}if(backgroundSize===true){backgroundSize="contain"}backgroundSize=this.support.backgroundSize&&this.support.backgroundSize[backgroundSize]&&backgroundSize;if(backgroundSize){element=this.elementPrototype.cloneNode(false)}else{element=img;img.draggable=false}if(title){element.title=title}$(img).on("load error",callbackWrapper);img.src=url;return element},createElement:function(obj,callback){var type=obj&&this.getItemProperty(obj,this.options.typeProperty),factory=type&&this[type.split("/")[0]+"Factory"]||this.imageFactory,element=obj&&factory.call(this,obj,callback);if(!element){element=this.elementPrototype.cloneNode(false);this.setTimeout(callback,[{type:"error",target:element}])}$(element).addClass(this.options.slideContentClass);return element},loadElement:function(index){if(!this.elements[index]){if(this.slides[index].firstChild){this.elements[index]=$(this.slides[index]).hasClass(this.options.slideErrorClass)?3:2}else{this.elements[index]=1;$(this.slides[index]).addClass(this.options.slideLoadingClass);this.slides[index].appendChild(this.createElement(this.list[index],this.proxyListener))}}},loadElements:function(index){var limit=Math.min(this.num,this.options.preloadRange*2+1),j=index,i;for(i=0;ithis.options.preloadRange&&diff+this.options.preloadRangeindex?-this.slideWidth:this.indexindex?to:index)-diff-1),this.slideWidth*direction,0)}to=this.circle(to);this.move(index,this.slideWidth*direction,speed);this.move(to,0,speed);if(this.options.continuous){this.move(this.circle(to-direction),-(this.slideWidth*direction),0)}}else{to=this.circle(to);this.animate(index*-this.slideWidth,to*-this.slideWidth,speed)}this.onslide(to)},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){if(this.options.continuous||this.index){this.slide(this.index-1)}},next:function(){if(this.options.continuous||this.index1){this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(to,speed){that.animationFrameId=that.requestAnimationFrame.call(window,function(){that.slide(to,speed)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)}this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout);this.interval=null;this.container.removeClass(this.options.playingClass)},add:function(list){var i;if(!list.concat){list=Array.prototype.slice.call(list)}if(!this.list.concat){this.list=Array.prototype.slice.call(this.list)}this.list=this.list.concat(list);this.num=this.list.length;if(this.num>2&&this.options.continuous===null){this.options.continuous=true;this.container.removeClass(this.options.leftEdgeClass)}this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass);for(i=this.num-list.length;ispeed){that.slidesContainer[0].style.left=to+"px";that.ontransitionend();window.clearInterval(timer);return}that.slidesContainer[0].style.left=(to-from)*(Math.floor(timeElap/speed*100)/100)+from+"px"},4)},preventDefault:function(event){if(event.preventDefault){event.preventDefault()}else{event.returnValue=false}},stopPropagation:function(event){if(event.stopPropagation){event.stopPropagation()}else{event.cancelBubble=true}},onresize:function(){this.initSlides(true)},onmousedown:function(event){if(event.which&&event.which===1&&event.target.nodeName!=="VIDEO"){event.preventDefault();(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchstart(event)}},onmousemove:function(event){if(this.touchStart){(event.originalEvent||event).touches=[{pageX:event.pageX,pageY:event.pageY}];this.ontouchmove(event)}},onmouseup:function(event){if(this.touchStart){this.ontouchend(event);delete this.touchStart}},onmouseout:function(event){if(this.touchStart){var target=event.target,related=event.relatedTarget;if(!related||related!==target&&!$.contains(target,related)){this.onmouseup(event)}}},ontouchstart:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0];this.touchStart={x:touches.pageX,y:touches.pageY,time:Date.now()};this.isScrolling=undefined;this.touchDelta={}},ontouchmove:function(event){if(this.options.stopTouchEventsPropagation){this.stopPropagation(event)}var touches=(event.originalEvent||event).touches[0],scale=(event.originalEvent||event).scale,index=this.index,touchDeltaX,indices;if(touches.length>1||scale&&scale!==1){return}if(this.options.disableScroll){event.preventDefault()}this.touchDelta={x:touches.pageX-this.touchStart.x,y:touches.pageY-this.touchStart.y};touchDeltaX=this.touchDelta.x;if(this.isScrolling===undefined){this.isScrolling=this.isScrolling||Math.abs(touchDeltaX)0||index===this.num-1&&touchDeltaX<0?Math.abs(touchDeltaX)/this.slideWidth+1:1);indices=[index];if(index){indices.push(index-1)}if(index20||Math.abs(this.touchDelta.x)>slideWidth/2,isPastBounds=!index&&this.touchDelta.x>0||index===this.num-1&&this.touchDelta.x<0,isValidClose=!isValidSlide&&this.options.closeOnSwipeUpOrDown&&(isShortDuration&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2),direction,indexForward,indexBackward,distanceForward,distanceBackward;if(this.options.continuous){isPastBounds=false}direction=this.touchDelta.x<0?-1:1;if(!this.isScrolling){if(isValidSlide&&!isPastBounds){indexForward=index+direction;indexBackward=index-direction;distanceForward=slideWidth*direction;distanceBackward=-slideWidth*direction;if(this.options.continuous){this.move(this.circle(indexForward),distanceForward,0);this.move(this.circle(index-2*direction),distanceBackward,0)}else if(indexForward>=0&&indexForwardthis.container[0].clientHeight){target.style.maxHeight=this.container[0].clientHeight}if(this.interval&&this.slides[this.index]===parent){this.play()}this.setTimeout(this.options.onslidecomplete,[index,parent])},onload:function(event){this.oncomplete(event)},onerror:function(event){this.oncomplete(event)},onkeydown:function(event){switch(event.which||event.keyCode){case 13:if(this.options.toggleControlsOnReturn){this.preventDefault(event);this.toggleControls()}break;case 27:if(this.options.closeOnEscape){this.close()}break;case 32:if(this.options.toggleSlideshowOnSpace){this.preventDefault(event);this.toggleSlideshow()}break;case 37:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.prev()}break;case 39:if(this.options.enableKeyboardNavigation){this.preventDefault(event);this.next()}break}},handleClick:function(event){var options=this.options,target=event.target||event.srcElement,parent=target.parentNode,isTarget=function(className){return $(target).hasClass(className)||$(parent).hasClass(className)};if(isTarget(options.toggleClass)){this.preventDefault(event);this.toggleControls()}else if(isTarget(options.prevClass)){this.preventDefault(event);this.prev()}else if(isTarget(options.nextClass)){this.preventDefault(event);this.next()}else if(isTarget(options.closeClass)){this.preventDefault(event);this.close()}else if(isTarget(options.playPauseClass)){this.preventDefault(event);this.toggleSlideshow()}else if(isTarget(options.fullscreenClass)){this.preventDefault(event);this.toggleFullscreen()}else if(parent===this.slidesContainer[0]){this.preventDefault(event);if(options.closeOnSlideClick){this.close()}else{this.toggleControls()}}else if(parent.parentNode&&parent.parentNode===this.slidesContainer[0]){this.preventDefault(event);this.toggleControls()}},onclick:function(event){if(this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)){delete this.touchDelta;return}return this.handleClick(event)},updateEdgeClasses:function(index){if(!index){this.container.addClass(this.options.leftEdgeClass)}else{this.container.removeClass(this.options.leftEdgeClass)}if(index===this.num-1){this.container.addClass(this.options.rightEdgeClass)}else{this.container.removeClass(this.options.rightEdgeClass)}},handleSlide:function(index){if(!this.options.continuous){this.updateEdgeClasses(index)}this.loadElements(index);if(this.options.unloadElements){this.unloadElements(index)}this.setTitle(index)},onslide:function(index){this.index=index;this.handleSlide(index);this.setTimeout(this.options.onslide,[index,this.slides[index]])},setTitle:function(index){var text=this.slides[index].firstChild.title,titleElement=this.titleElement;if(titleElement.length){this.titleElement.empty();if(text){titleElement[0].appendChild(document.createTextNode(text))}}},setTimeout:function(func,args,wait){var that=this;return func&&window.setTimeout(function(){func.apply(that,args||[])},wait||0)},imageFactory:function(obj,callback){var that=this,img=this.imagePrototype.cloneNode(false),url=obj,backgroundSize=this.options.stretchImages,called,element,callbackWrapper=function(event){if(!called){event={type:event.type,target:element};if(!element.parentNode){return that.setTimeout(callbackWrapper,[event])}called=true;$(img).off("load error",callbackWrapper);if(backgroundSize){if(event.type==="load"){element.style.background='url("'+url+'") center no-repeat';element.style.backgroundSize=backgroundSize}}callback(event)}},title;if(typeof url!=="string"){url=this.getItemProperty(obj,this.options.urlProperty);title=this.getItemProperty(obj,this.options.titleProperty)}if(backgroundSize===true){backgroundSize="contain"}backgroundSize=this.support.backgroundSize&&this.support.backgroundSize[backgroundSize]&&backgroundSize;if(backgroundSize){element=this.elementPrototype.cloneNode(false)}else{element=img;img.draggable=false}if(title){element.title=title}$(img).on("load error",callbackWrapper);img.src=url;return element},createElement:function(obj,callback){var type=obj&&this.getItemProperty(obj,this.options.typeProperty),factory=type&&this[type.split("/")[0]+"Factory"]||this.imageFactory,element=obj&&factory.call(this,obj,callback);if(!element){element=this.elementPrototype.cloneNode(false);this.setTimeout(callback,[{type:"error",target:element}])}$(element).addClass(this.options.slideContentClass);return element},loadElement:function(index){if(!this.elements[index]){if(this.slides[index].firstChild){this.elements[index]=$(this.slides[index]).hasClass(this.options.slideErrorClass)?3:2}else{this.elements[index]=1;$(this.slides[index]).addClass(this.options.slideLoadingClass);this.slides[index].appendChild(this.createElement(this.list[index],this.proxyListener))}}},loadElements:function(index){var limit=Math.min(this.num,this.options.preloadRange*2+1),j=index,i;for(i=0;ithis.options.preloadRange&&diff+this.options.preloadRangeindex?-this.slideWidth:this.index Date: Wed, 28 Jan 2015 17:45:10 +0000 Subject: [PATCH 37/42] Set thumbnailWithImgTag to true to get thumbnail indicators as img tag in order to be able to set right image size ratio --- etemplate/js/expose.js | 2 ++ etemplate/templates/default/etemplate2.css | 42 ++++++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/etemplate/js/expose.js b/etemplate/js/expose.js index 5f01571ce4..e7fdec50b6 100644 --- a/etemplate/js/expose.js +++ b/etemplate/js/expose.js @@ -307,6 +307,8 @@ function expose (widget) thumbnailProperty: 'thumbnail', // Defines if the gallery indicators should display a thumbnail: thumbnailIndicators: true, + //thumbnail with image tag + thumbnailWithImgTag: true, // Callback function executed when the Gallery is initialized. // Is called with the gallery instance as "this" object: onopen: jQuery.proxy(this.expose_onopen,this), diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index 1e91229646..7843f8cbcf 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -1774,36 +1774,40 @@ span.et2_egw_action_ddHelper_itemsCnt { /*Carousel thumbnails*/ .blueimp-gallery>.indicator>li { display: inline-block; - width: 134px; + width: auto; height: 100px; - margin: none; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - border: 1px solid transparent; - background: #ccc; - background: rgba(255, 255, 255, 0.17)center no-repeat; - border-radius: 5px; - box-shadow: 0 0 2px #000; + margin: 0; + background: transparent; opacity: 1; cursor: pointer; - background-repeat: no-repeat; - margin: 1px; + border-radius: 0; border:0; - background-size: content; } + +.blueimp-gallery > .indicator > li > img { + display: inline-block; + width: auto; + height: 100px; + margin:0; + cursor: pointer; + z-index: -1; + position: relative; +} + /*Thumbnail border on hover*/ .blueimp-gallery>.indicator>li:hover { - -webkit-box-shadow: inset 0px 1px 0px 4px rgba(255, 255, 255, 1); - -moz-box-shadow: inset 0px 1px 0px 4px rgba(255, 255, 255, 1); - box-shadow: inset 0px 1px 0px 4px rgba(255, 255, 255, 1); + -webkit-box-shadow: inset 0px 0px 0px 4px rgba(255, 255, 255, 1); + -moz-box-shadow: inset 0px 0px 0px 4px rgba(255, 255, 255, 1); + box-shadow: inset 0px 0px 0px 4px rgba(255, 255, 255, 1); + background: transparent; } /*Active thumbnail border*/ .blueimp-gallery>.indicator>.active { - -webkit-box-shadow: inset 0px 1px 0px 4px #0c5da5; - -moz-box-shadow: inset 0px 1px 0px 4px #0c5da5; - box-shadow: inset 0px 1px 0px 4px #0c5da5; + -webkit-box-shadow: inset 0px 0px 0px 4px #0c5da5; + -moz-box-shadow: inset 0px 0px 0px 4px #0c5da5; + box-shadow: inset 0px 0px 0px 4px #0c5da5; + background: transparent; } /*Slideshow Play/Pause button*/ .blueimp-gallery>.play-pause{ From b15029a0fce7b9f6a781299dea1a98a93314fbed Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 29 Jan 2015 13:31:43 +0000 Subject: [PATCH 38/42] * Calendar/CalDAV: fixed synced events still contained deleted exceptions --- calendar/inc/class.calendar_so.inc.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 5326bfcc05..33729f4f91 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -1042,15 +1042,24 @@ class calendar_so // set data, if recurrence is requested if (isset($events[$id])) $events[$id]['participants'][$uid] = $status; } - // query recurrance exceptions, if needed - if (!$params['enum_recuring']) + // query recurrance exceptions, if needed: enum_recuring && !daywise is used in calendar_groupdav::get_series($uid,...) + if (!$params['enum_recuring'] || !$params['daywise']) { foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array( 'cal_id' => $ids, 'recur_exception' => true, ), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row) { - $events[$row['cal_id']]['recur_exception'][] = $row['cal_start']; + // for enum_recurring events are not indexed by cal_id, but $cal_id.'-'.$cal_start + // find master, which is first recurrence + if (!isset($events[$id=$row['cal_id']])) + { + foreach($events as $id => $event) + { + if ($event['id'] == $row['cal_id']) break; + } + } + $events[$id]['recur_exception'][] = $row['cal_start']; } } //custom fields are not shown in the regular views, so we only query them, if explicitly required From 9c35bfa3f3f3652d0e0a7f6be4cfaae811c83cc3 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 29 Jan 2015 13:54:34 +0000 Subject: [PATCH 39/42] updated todo and docu --- phpgwapi/inc/class.egw_sharing.inc.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/phpgwapi/inc/class.egw_sharing.inc.php b/phpgwapi/inc/class.egw_sharing.inc.php index 9f6b7badbe..668f749e30 100644 --- a/phpgwapi/inc/class.egw_sharing.inc.php +++ b/phpgwapi/inc/class.egw_sharing.inc.php @@ -16,7 +16,13 @@ * Token generation uses openssl_random_pseudo_bytes, if available, otherwise * mt_rand based auth::randomstring is used. * - * @todo handle existing user sessions eg. by mounting share under it's token into vfs and redirect to regular filemanager + * Existing user sessions are kept whenever possible by an additional mount into regular VFS: + * - share owner is current user (no problems with rights, they simply match) + * - share owner has owner-right for share: we create a temp. eACL for current user + * --> in all other cases session will be replaced with one of the anonymous user, + * as we dont support mounting with rights of share owner (VFS uses Vfs::$user!) + * + * @todo handle mounts of an entry directory /apps/$app/$id * @todo handle mounts inside shared directory (they get currently lost) * @todo handle absolute symlinks (wont work as we use share as root) */ From bec89939c7a3d62c5dcf4de68d7cbd1b7b6d08f7 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 29 Jan 2015 13:55:40 +0000 Subject: [PATCH 40/42] fix not working clearing of cache for files backend --- phpgwapi/inc/class.egw_cache_files.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpgwapi/inc/class.egw_cache_files.inc.php b/phpgwapi/inc/class.egw_cache_files.inc.php index abb0f9650e..ff86e53a35 100644 --- a/phpgwapi/inc/class.egw_cache_files.inc.php +++ b/phpgwapi/inc/class.egw_cache_files.inc.php @@ -152,7 +152,7 @@ class egw_cache_files extends egw_cache_provider_check implements egw_cache_prov } else { - if (!unlink($path.'/'.$file)) return false; + if (!unlink($file)) return false; } } return rmdir($path); From 6e25cc598a63a41e20c8f4cab34ea5cefd47162d Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 29 Jan 2015 16:33:36 +0000 Subject: [PATCH 41/42] Disable autoComplete fixer for all browser but Chrome --- etemplate/js/etemplate2.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 37406eb871..cb14c1ffb3 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -591,6 +591,10 @@ etemplate2.prototype.isDirty = function() */ etemplate2.prototype.autocomplete_fixer = function (_node,_id) { + // Enable autocomplete fixer only for Chrome as + // it's the only tested browser which work 100% with this method + if (!navigator.userAgent.match(/chrome/i)) return; + var node = _node||jQuery('.et2_contianer'); var id = _id||this.uniqueId; @@ -604,10 +608,7 @@ etemplate2.prototype.autocomplete_fixer = function (_node,_id) var $form = jQuery(document.createElement('form')) .attr({id:id + '_form_autocomplete_fixer',action:'about:blank', target:'egw_iframe_autocomplete helper'}) .css({height:"100%"}); - // Firefox give a security warning when transmitting to "about:blank" from a https site - // we work around that by giving existing etemplate/empty.html url - if (navigator.userAgent.match(/firefox/i)) - $form.attr({action: egw.webserverUrl+'/etemplate/empty.html',method:'post'}); + //Creates iframe for fake submission $et2_container.before(jQuery(document.createElement('iframe')) .attr({id:id + '_iframe_autocomplete_fixer',src:'about:blank',name:'egw_iframe_autocomplete helper'}).hide()); @@ -618,11 +619,12 @@ etemplate2.prototype.autocomplete_fixer = function (_node,_id) setTimeout(function(){ var $a = jQuery(node).parent(); if ($a.length>0 && typeof $a.parent() != 'undefined') $a.submit(); //Submit to iframe - //Clean up the mess from DOM - if(jQuery(node).parent().is('form')) - jQuery(node).unwrap(); - jQuery('iframe#'+id + '_iframe_autocomplete_fixer').remove(); - },0); + setTimeout(function(){ + //Clean up the mess from DOM + if(jQuery(node).parent().is('form')) jQuery(node).unwrap(); + jQuery('iframe#'+id + '_iframe_autocomplete_fixer').remove(); + },1); + },1); }; /** From 7a81c7bfa41a01bf2a868a34dcb9cd0a1f10babd Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 29 Jan 2015 17:07:13 +0000 Subject: [PATCH 42/42] mime icons for .css and .js, thanks to Pixelegg guys --- .../images/mime128_application_javascript.png | Bin 0 -> 14688 bytes .../default/images/mime128_text_css.png | Bin 0 -> 10781 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 etemplate/templates/default/images/mime128_application_javascript.png create mode 100644 etemplate/templates/default/images/mime128_text_css.png diff --git a/etemplate/templates/default/images/mime128_application_javascript.png b/etemplate/templates/default/images/mime128_application_javascript.png new file mode 100644 index 0000000000000000000000000000000000000000..7838b0341552eeeaeb85704931b19a8fb07ccb14 GIT binary patch literal 14688 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