diff --git a/lib/api/file_get.php b/lib/api/file_get.php index b7698a4..50d0e85 100644 --- a/lib/api/file_get.php +++ b/lib/api/file_get.php @@ -1,95 +1,97 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_get extends file_api_common { + protected $driver; + /** * Request handler */ public function handle() { parent::handle(); $this->api->output_type = file_api_core::OUTPUT_HTML; if (!isset($this->args['file']) || $this->args['file'] === '') { header("HTTP/1.0 ".file_api_core::ERROR_CODE." Missing file name"); } $params = array( 'force-download' => rcube_utils::get_boolean((string) $this->args['force-download']), 'force-type' => $this->args['force-type'], ); list($this->driver, $path) = $this->api->get_driver($this->args['file']); if (!empty($this->args['viewer'])) { $this->file_view($path, $this->args, $params); } try { $this->driver->file_get($path, $params); } catch (Exception $e) { header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage()); } exit; } /** * File vieweing request handler */ protected function file_view($file, $args, $params) { $viewer = $args['viewer']; $path = __DIR__ . "/../viewers/$viewer.php"; $class = "file_viewer_$viewer"; if (!file_exists($path)) { return; } // get file info try { $info = $this->driver->file_info($file); } catch (Exception $e) { header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage()); exit; } include_once $path; $viewer = new $class($this->api); // check if specified viewer supports file type // otherwise return (fallback to file_get action) if (!$viewer->supports($info['type'])) { return; } $viewer->output($args['file'], $info['type']); exit; } } diff --git a/lib/api/folder_list.php b/lib/api/folder_list.php index 5e5017d..6bb8d97 100644 --- a/lib/api/folder_list.php +++ b/lib/api/folder_list.php @@ -1,109 +1,117 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_folder_list extends file_api_common { /** * Request handler */ public function handle() { parent::handle(); - // get folders from main driver + // List parameters + $params = array(); + if (!empty($this->args['unsubscribed']) && rcube_utils::get_boolean((string) $this->args['unsubscribed'])) { + $params['type'] = file_storage::FILTER_UNSUBSCRIBED; + } + if (isset($this->args['search']) && strlen($this->args['search'])) { + $params['search'] = $this->args['search']; + $search = mb_strtoupper($this->args['search']); + } + + // get folders from default driver $backend = $this->api->get_backend(); - $folders = $backend->folder_list(); + $folders = $this->folder_list($backend, $params); // old result format if ($this->api->client_version() < 2) { return $folders; } $drivers = $this->api->get_drivers(true); $has_more = false; $errors = array(); // get folders from external sources foreach ($drivers as $driver) { $title = $driver->title(); $prefix = $title . file_storage::SEPARATOR; // folder exists in main source, replace it with external one if (($idx = array_search($title, $folders)) !== false) { foreach ($folders as $idx => $folder) { if ($folder == $title || strpos($folder, $prefix) === 0) { unset($folders[$idx]); } } } - $folders[] = $title; - $has_more = true; + if (!isset($search) || strpos(mb_strtoupper($title), $search) !== false) { + $folders[] = $title; + $has_more = count($folders) > 0; + } if ($driver != $backend) { try { - foreach ($driver->folder_list() as $folder) { + foreach ($this->folder_list($driver, $params) as $folder) { $folders[] = $prefix . $folder; + $has_more = true; } } catch (Exception $e) { if ($e->getCode() == file_storage::ERROR_NOAUTH) { // inform UI about to ask user for credentials $errors[$title] = $this->parse_metadata($driver->driver_metadata()); } } } } // re-sort the list if ($has_more) { - usort($folders, array($this, 'sort_folder_comparator')); + usort($folders, array('file_utils', 'sort_folder_comparator')); } return array( 'list' => $folders, 'auth_errors' => $errors, ); } /** - * Callback for uasort() that implements correct - * locale-aware case-sensitive sorting + * Wrapper for folder_list() method on specified driver */ - protected function sort_folder_comparator($str1, $str2) + protected function folder_list($driver, $params) { - $path1 = explode(file_storage::SEPARATOR, $str1); - $path2 = explode(file_storage::SEPARATOR, $str2); - - foreach ($path1 as $idx => $folder1) { - $folder2 = $path2[$idx]; - - if ($folder1 === $folder2) { - continue; + if ($params['type'] == file_storage::FILTER_UNSUBSCRIBED) { + $caps = $driver->capabilities(); + if (empty($caps[file_storage::CAPS_SUBSCRIPTIONS])) { + return array(); } - - return strcoll($folder1, $folder2); } + + return $driver->folder_list($params); } } diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php index 94da26d..9cd9a7e 100644 --- a/lib/drivers/kolab/kolab_file_storage.php +++ b/lib/drivers/kolab/kolab_file_storage.php @@ -1,1252 +1,1325 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_file_storage implements file_storage { /** * @var rcube */ protected $rc; /** * @var array */ protected $folders; /** * @var array */ protected $config = array(); /** * @var string */ protected $title; /** * Class constructor */ public function __construct() { $this->rc = rcube::get_instance(); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array) $this->rc->config->get('fileapi_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab'))); // Kolab WebDAV server supports plugins, no need to overwrite object if (!is_a($this->rc->plugins, 'rcube_plugin_api')) { // Initialize/load plugins $this->rc->plugins = kolab_file_plugin_api::get_instance(); $this->rc->plugins->init($this, ''); } // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->rc->plugins->load_plugin($plugin, true); } $this->init(); } /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @param bool True on success, False on failure */ public function authenticate($username, $password) { $auth = $this->rc->plugins->exec_hook('authenticate', array( 'host' => $this->select_host($username), 'user' => $username, 'pass' => $password, 'valid' => true, )); // Authenticate - get Roundcube user ID if ($auth['valid'] && !$auth['abort'] && ($this->login($auth['user'], $auth['pass'], $auth['host']))) { return true; } $this->rc->plugins->exec_hook('login_failed', array( 'host' => $auth['host'], 'user' => $auth['user'], )); } /** * Get password and name of authenticated user * * @return array Authenticated user data */ public function auth_info() { return array( 'username' => $this->config['username'] ?: $_SESSION['username'], 'password' => $this->config['password'] ?: $this->rc->decrypt($_SESSION['password']), ); } /** * Storage host selection */ private function select_host($username) { // Get IMAP host $host = $this->rc->config->get('default_host'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP */ private function login($username, $password, $host) { if (empty($username)) { return false; } $login_lc = $this->rc->config->get('login_lc'); $default_port = $this->rc->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP $storage = $this->rc->get_storage(); if (!$storage->connect($host, $username, $password, $port, $ssl)) { return false; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->rc->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return false; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return false; } } // set session vars $_SESSION['user_id'] = $user->ID; $_SESSION['username'] = $user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $this->rc->encrypt($password); $this->init($user); // force reloading of mailboxes list/data $storage->clear_cache('mailboxes', true); return true; } protected function init($user = null) { if ($_SESSION['user_id'] || $user) { // overwrite config with user preferences $this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']); $this->rc->config->set_user_prefs((array)$this->rc->user->get_prefs()); $storage = $this->rc->get_storage(); $storage->set_charset($this->rc->config->get('default_charset', RCUBE_CHARSET)); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); } } /** * Configures environment * * @param array $config Configuration * @param string $title Source identifier */ public function configure($config, $title = null) { $this->config = array_merge($this->config, $config); // @TODO: this is currently not possible to have multiple sessions in Roundcube } /** * Returns current instance title * * @return string Instance title (mount point) */ public function title() { return ''; } /** * Storage driver capabilities * * @return array List of capabilities */ public function capabilities() { // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } $storage = $this->rc->get_storage(); $quota = $storage->get_capability('QUOTA'); return array( file_storage::CAPS_MAX_UPLOAD => $max_filesize, file_storage::CAPS_QUOTA => $quota, file_storage::CAPS_LOCKS => true, file_storage::CAPS_SUBSCRIPTIONS => true, ); } /** * Save configuration of external driver (mount point) * * @param array $driver Driver data * * @throws Exception */ public function driver_create($driver) { $drivers = $this->driver_list(); if ($drivers[$driver['title']]) { throw new Exception("Driver exists", file_storage::ERROR); } $config = kolab_storage_config::get_instance(); $status = $config->save($driver, 'file_driver'); if (!$status) { throw new Exception("Driver create failed", file_storage::ERROR); } $this->driver_list = null; } /** * Delete configuration of external driver (mount point) * * @param string $name Driver instance name * * @throws Exception */ public function driver_delete($name) { $drivers = $this->driver_list(); if ($driver = $drivers[$name]) { $config = kolab_storage_config::get_instance(); $status = $config->delete($driver['uid']); if (!$status) { throw new Exception("Driver delete failed", file_storage::ERROR); } $this->driver_list = null; return; } throw new Exception("Driver not found", file_storage::ERROR); } /** * Return list of registered drivers (mount points) * * @return array List of drivers data * @throws Exception */ public function driver_list() { // use internal cache, this is specifically for iRony // which may call this code path many times in one request if ($this->driver_list !== null) { return $this->driver_list; } // get current relations state $config = kolab_storage_config::get_instance(); $default = true; $filter = array( array('type', '=', 'file_driver'), ); $drivers = $config->get_objects($filter, $default, 100); $result = array(); foreach ($drivers as $driver) { $result[$driver['title']] = $driver; } return $this->driver_list = $result; } /** * Update configuration of external driver (mount point) * * @param string $title Driver instance title * @param array $driver Driver data * * @throws Exception */ public function driver_update($title, $driver) { $drivers = $this->driver_list(); if (!$drivers[$title]) { throw new Exception("Driver not found", file_storage::ERROR); } $config = kolab_storage_config::get_instance(); $status = $config->save($driver, 'file_driver'); if (!$status) { throw new Exception("Driver update failed", file_storage::ERROR); } $this->driver_list = null; } /** * Returns metadata of the driver * * @return array Driver meta data (image, name, form) */ public function driver_metadata() { $image_content = file_get_contents(__DIR__ . '/kolab.png'); $metadata = array( 'image' => 'data:image/png;base64,' . base64_encode($image_content), 'name' => 'Kolab Groupware', 'ref' => 'http://kolab.org', 'description' => 'Kolab Groupware server', 'form' => array( 'host' => 'hostname', 'username' => 'username', 'password' => 'password', ), ); return $metadata; } /** * Validate metadata (config) of the driver * * @param array $metadata Driver metadata * * @return array Driver meta data to be stored in configuration * @throws Exception */ public function driver_validate($metadata) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Create a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_create($file_name, $file) { $exists = $this->get_file_object($file_name, $folder); if (!empty($exists)) { throw new Exception("Storage error. File exists.", file_storage::ERROR); } $object = $this->to_file_object(array( 'name' => $file_name, 'type' => $file['type'], 'path' => $file['path'], 'content' => $file['content'], )); // save the file object in IMAP $saved = $folder->save($object, 'file'); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving object to Kolab server"), true, false); throw new Exception("Storage error. Saving file failed.", file_storage::ERROR); } } /** * Update a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_update($file_name, $file) { $file_object = $this->get_file_object($file_name, $folder); if (empty($file_object)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $key = key($file_object['_attachments']); $file_object['_attachments'] = array( 0 => array( 'name' => $file_name, 'path' => $file['path'], 'content' => $file['content'], 'mimetype' => $file['type'], ), $key => false, ); // save the file object in IMAP $saved = $folder->save($file_object, 'file', $file_object['_msguid']); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving object to Kolab server"), true, false); throw new Exception("Storage error. Saving file failed.", file_storage::ERROR); } } /** * Delete a file. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_delete($file_name) { $file = $this->get_file_object($file_name, $folder); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $deleted = $folder->delete($file); if (!$deleted) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error deleting object from Kolab server"), true, false); throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR); } } /** * Return file body. * * @param string $file_name Name of a file (with folder path) * @param array $params Parameters (force-download) * @param resource $fp Print to file pointer instead (send no headers) * * @throws Exception */ public function file_get($file_name, $params = array(), $fp = null) { $file = $this->get_file_object($file_name, $folder); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $file = $this->from_file_object($file); // write to file pointer, send no headers if ($fp) { if ($file['size']) { $folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], false, $fp); } return; } if (!empty($params['force-download'])) { $disposition = 'attachment'; header("Content-Type: application/octet-stream"); // @TODO // if ($browser->ie) // header("Content-Type: application/force-download"); } else { $mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']); $disposition = 'inline'; header("Content-Transfer-Encoding: binary"); header("Content-Type: $mimetype"); } $filename = addcslashes($file['name'], '"'); // Workaround for nasty IE bug (#1488844) // If Content-Disposition header contains string "attachment" e.g. in filename // IE handles data as attachment not inline /* @TODO if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) { $filename = str_ireplace('attachment', 'attach', $filename); } */ header("Content-Length: " . $file['size']); header("Content-Disposition: $disposition; filename=\"$filename\""); if ($file['size']) { $folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], true); } } /** * Returns file metadata. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_info($file_name) { $file = $this->get_file_object($file_name, $folder); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $file = $this->from_file_object($file); return array( 'name' => $file['name'], 'size' => (int) $file['size'], 'type' => (string) $file['type'], 'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '', 'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '', 'modified' => $file['changed'] ? $file['changed']->format('U') : 0, 'created' => $file['created'] ? $file['created']->format('U') : 0, ); } /** * List files in a folder. * * @param string $folder_name Name of a folder with full path * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix') * * @return array List of files (file properties array indexed by filename) * @throws Exception */ public function file_list($folder_name, $params = array()) { $filter = array(array('type', '=', 'file')); if (!empty($params['search'])) { foreach ($params['search'] as $idx => $value) { switch ($idx) { case 'name': $filter[] = array('filename', '~', $value); break; case 'class': foreach (file_utils::class2mimetypes($value) as $tag) { $for[] = array('tags', '~', ' ' . $tag); } $filter[] = array($for, 'OR'); break; } } } // get files list $folder = $this->get_folder_object($folder_name); $files = $folder->select($filter); $result = array(); // convert to kolab_storage files list data format foreach ($files as $idx => $file) { $file = $this->from_file_object($file); if (!isset($file['name'])) { continue; } $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name']; $result[$filename] = array( 'name' => $file['name'], 'size' => (int) $file['size'], 'type' => (string) $file['type'], 'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '', 'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '', 'modified' => $file['changed'] ? $file['changed']->format('U') : 0, 'created' => $file['created'] ? $file['created']->format('U') : 0, ); unset($files[$idx]); } // @TODO: pagination, search (by filename, mimetype) // Sorting $sort = !empty($params['sort']) ? $params['sort'] : 'name'; $index = array(); if ($sort == 'mtime') { $sort = 'modified'; } if (in_array($sort, array('name', 'size', 'modified'))) { foreach ($result as $key => $val) { $index[$key] = $val[$sort]; } array_multisort($index, SORT_ASC, SORT_NUMERIC, $result); } if ($params['reverse']) { $result = array_reverse($result, true); } return $result; } /** * Copy a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_copy($file_name, $new_name) { $file = $this->get_file_object($file_name, $folder); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $new = $this->get_file_object($new_name, $new_folder); if (!empty($new)) { throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS); } $file = $this->from_file_object($file); // Save to temp file // @TODO: use IMAP CATENATE extension $temp_dir = unslashify($this->rc->config->get('temp_dir')); $file_path = tempnam($temp_dir, 'rcmAttmnt'); $fh = fopen($file_path, 'w'); if (!$fh) { throw new Exception("Storage error. File copying failed.", file_storage::ERROR); } if ($file['size']) { $folder->get_attachment($file['uid'], $file['fileid'], null, false, $fh, true); } fclose($fh); if (!file_exists($file_path)) { throw new Exception("Storage error. File copying failed.", file_storage::ERROR); } // Update object $file['_attachments'] = array( 0 => array( 'name' => $file['name'], 'path' => $file_path, 'mimetype' => $file['type'], 'size' => $file['size'], )); $fields = array('created', 'changed', '_attachments', 'notes', 'sensitivity', 'categories', 'x-custom'); $file = array_intersect_key($file, array_combine($fields, $fields)); $saved = $new_folder->save($file, 'file'); @unlink($file_path); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error updating object on Kolab server"), true, false); throw new Exception("Storage error. File copying failed.", file_storage::ERROR); } } /** * Move (or rename) a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_move($file_name, $new_name) { $file = $this->get_file_object($file_name, $folder); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $new = $this->get_file_object($new_name, $new_folder); if (!empty($new)) { throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS); } // Move the file if ($folder->name != $new_folder->name) { $saved = $folder->move($file['uid'], $new_folder->name); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error moving object on Kolab server"), true, false); throw new Exception("Storage error. File move failed.", file_storage::ERROR); } $folder = $new_folder; } if ($file_name === $new_name) { return; } // Update object (changing the name) $cid = key($file['_attachments']); $file['_attachments'][$cid]['name'] = $new_name; $file['_attachments'][0] = $file['_attachments'][$cid]; $file['_attachments'][$cid] = false; $saved = $folder->save($file, 'file'); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error updating object on Kolab server"), true, false); throw new Exception("Storage error. File rename failed.", file_storage::ERROR); } } /** * Create a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_create($folder_name) { $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); $success = kolab_storage::folder_create($folder_name, 'file', true); if (!$success) { - throw new Exception("Storage error. Unable to create folder", file_storage::ERROR); + throw new Exception("Storage error. Unable to create the folder", file_storage::ERROR); } } /** * Delete a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_delete($folder_name) { $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); $success = kolab_storage::folder_delete($folder_name); if (!$success) { - throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR); + throw new Exception("Storage error. Unable to delete the folder.", file_storage::ERROR); } } /** * Move/Rename a folder. * * @param string $folder_name Name of a folder with full path * @param string $new_name New name of a folder with full path * * @throws Exception on error */ public function folder_move($folder_name, $new_name) { $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); $success = kolab_storage::folder_rename($folder_name, $new_name); if (!$success) { - throw new Exception("Storage error. Unable to rename folder", file_storage::ERROR); + throw new Exception("Storage error. Unable to rename the folder", file_storage::ERROR); + } + } + + /** + * Subscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_subscribe($folder_name) + { + $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); + $storage = $this->rc->get_storage(); + + if (!$storage->subscribe($folder_name)) { + throw new Exception("Storage error. Unable to subscribe the folder", file_storage::ERROR); + } + } + + /** + * Unsubscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_unsubscribe($folder_name) + { + $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); + $storage = $this->rc->get_storage(); + + if (!$storage->unsubscribe($folder_name)) { + throw new Exception("Storage error. Unable to unsubsribe the folder", file_storage::ERROR); } } /** * Returns list of folders. * + * @param array $params List parameters ('type', 'search') + * * @return array List of folders * @throws Exception */ - public function folder_list() + public function folder_list($params = array()) { - $folders = kolab_storage::list_folders('', '*', 'file', true); + $unsubscribed = $params['type'] & file_storage::FILTER_UNSUBSCRIBED; + $folders = kolab_storage::list_folders('', '*', 'file', true); if (!is_array($folders)) { throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR); } // create/subscribe 'Files' folder in case there's no folder of type 'file' - if (empty($folders)) { + if (empty($folders) && $subscribed) { $imap = $this->rc->get_storage(); $default = 'Files'; // the folder may exist but be unsubscribed if (!$imap->folder_exists($default)) { if (kolab_storage::folder_create($default, 'file', true)) { $folders[] = $default; } } else if (kolab_storage::folder_type($default) == 'file') { if ($imap->subscribe($default)) { $folders[] = $default; } } } else { + if ($unsubscribed) { + $subscribed = $folders; + $folders = kolab_storage::list_folders('', '*', 'file', false); + $folders = array_diff($folders, $subscribed); + } + $callback = function($folder) { return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET); }; $folders = array_map($callback, $folders); } + // searching + if (isset($params['search'])) { + $imap = $this->rc->get_storage(); + $search = mb_strtoupper($params['search']); + $prefix = null; + $ns = $imap->get_namespace('other'); + + if (!empty($ns)) { + $prefix = rcube_charset::convert($ns[0][0], 'UTF7-IMAP', RCUBE_CHARSET); + } + + $folders = array_filter($folders, function($folder) use ($search, $prefix) { + $path = explode('/', $folder); + + // search in folder name not the full path + if (strpos(mb_strtoupper($path[count($path)-1]), $search) !== false) { + return true; + } + // if it is an other user folder, we'll match the user name + // and return all folders of the matching user + else if (strpos($folder, $prefix) === 0 && strpos(mb_strtoupper($path[1]), $search) !== false) { + return true; + } + + return false; + }); + } + + $folders = array_values($folders); + return $folders; } /** * Returns a list of locks * * This method should return all the locks for a particular URI, including * locks that might be set on a parent URI. * * If child_locks is set to true, this method should also look for * any locks in the subtree of the URI for locks. * * @param string $uri URI * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($uri, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); // get locks list $list = $this->lock_db->lock_list($uri, $child_locks); // convert back resource string into URIs foreach ($list as $idx => $lock) { $list[$idx]['uri'] = $this->resource2uri($lock['uri']); } return $list; } /** * Locks a URI * * @param string $uri URI * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->lock($uri, $lock)) { throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR); } } /** * Removes a lock from a URI * * @param string $path URI * @param array $lock Lock data * * @throws Exception */ public function unlock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->unlock($uri, $lock)) { throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR); } } /** * Return disk quota information for specified folder. * * @param string $folder_name Name of a folder with full path * * @return array Quota * @throws Exception */ public function quota($folder) { $storage = $this->rc->get_storage(); $quota = $storage->get_quota(); $quota = $this->rc->plugins->exec_hook('quota', $quota); unset($quota['abort']); return $quota; } /** * Get file object. * * @param string $file_name Name of a file (with folder path) * @param kolab_storage_folder $folder Reference to folder object * * @return array File data * @throws Exception */ protected function get_file_object(&$file_name, &$folder = null) { // extract file path and file name $path = explode(file_storage::SEPARATOR, $file_name); $file_name = array_pop($path); $folder_name = implode(file_storage::SEPARATOR, $path); if ($folder_name === '') { throw new Exception("Missing folder name", file_storage::ERROR); } // get folder object $folder = $this->get_folder_object($folder_name); $files = $folder->select(array( array('type', '=', 'file'), array('filename', '=', $file_name) )); return $files[0]; } /** * Get folder object. * * @param string $folder_name Name of a folder with full path * * @return kolab_storage_folder Folder object * @throws Exception */ protected function get_folder_object($folder_name) { if ($folder_name === null || $folder_name === '') { throw new Exception("Missing folder name", file_storage::ERROR); } if (empty($this->folders[$folder_name])) { $storage = $this->rc->get_storage(); $separator = $storage->get_hierarchy_delimiter(); $folder_name = str_replace(file_storage::SEPARATOR, $separator, $folder_name); $imap_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); $folder = kolab_storage::get_folder($imap_name, 'file'); if (!$folder || !$folder->valid) { $error = $folder->get_error(); if ($error === kolab_storage::ERROR_IMAP_CONN || $error === kolab_storage::ERROR_CACHE_DB) { throw new Exception("The storage is temporarily unavailable.", file_storage::ERROR_UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new Exception("Storage error. Access not permitted", file_storage::ERROR_FORBIDDEN); } throw new Exception("Storage error. Folder not found.", file_storage::ERROR); } $this->folders[$folder_name] = $folder; } return $this->folders[$folder_name]; } /** * Simplify internal structure of the file object */ protected function from_file_object($file) { if (empty($file['_attachments'])) { return $file; } $attachment = array_shift($file['_attachments']); $file['name'] = $attachment['name']; $file['size'] = $attachment['size']; $file['type'] = $attachment['mimetype']; $file['fileid'] = $attachment['id']; unset($file['_attachments']); return $file; } /** * Convert to kolab_format internal structure of the file object */ protected function to_file_object($file) { // @TODO if path is empty and fileid exists it is an update // get attachment body and save it in path $file['_attachments'] = array( 0 => array( 'name' => $file['name'], 'path' => $file['path'], 'content' => $file['content'], 'mimetype' => $file['type'], 'size' => $file['size'], )); unset($file['name']); unset($file['size']); unset($file['type']); unset($file['path']); unset($file['fileid']); return $file; } protected function uri2resource($uri) { $storage = $this->rc->get_storage(); $namespace = $storage->get_namespace(); $separator = $storage->get_hierarchy_delimiter(); $uri = str_replace(file_storage::SEPARATOR, $separator, $uri); $owner = $this->rc->get_user_name(); // find the owner and remove namespace prefix foreach ($namespace as $type => $ns) { foreach ($ns as $root) { if (is_array($root) && $root[0] && strpos($uri, $root[0]) === 0) { $uri = substr($uri, strlen($root[0])); switch ($type) { case 'shared': // in theory there can be more than one shared root // we add it to dummy user name, so we can revert conversion $owner = "shared({$root[0]})"; break; case 'other': list($user, $uri) = explode($separator, $uri, 2); if (strpos($user, '@') === false) { $domain = strstr($owner, '@'); if (!empty($domain)) { $user .= $domain; } } $owner = $user; break; } break 2; } } } // convert to imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP'); return 'imap://' . urlencode($owner) . '@' . $storage->options['host'] . '/' . $uri; } protected function resource2uri($resource) { if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) { throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR); } $storage = $this->rc->get_storage(); $separator = $storage->get_hierarchy_delimiter(); $owner = $this->rc->get_user_name(); $user = urldecode($matches[1]); $uri = $matches[3]; // convert from imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET); // personal namespace if ($user == $owner) { // do nothing // Note: that might not work if personal namespace uses e.g. INBOX/ prefix. } // shared namespace else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) { $uri = $matches[1] . $uri; } // other users namespace else { $namespace = $storage->get_namespace('other'); list($local, $domain) = explode('@', $user); // here we assume there's only one other users namespace root $uri = $namespace[0][0] . $local . $separator . $uri; } $uri = str_replace($separator, file_storage::SEPARATOR, $uri); return $uri; } /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } } diff --git a/lib/drivers/seafile/seafile_file_storage.php b/lib/drivers/seafile/seafile_file_storage.php index 4413859..649a91f 100644 --- a/lib/drivers/seafile/seafile_file_storage.php +++ b/lib/drivers/seafile/seafile_file_storage.php @@ -1,1231 +1,1237 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class seafile_file_storage implements file_storage { /** * @var rcube */ protected $rc; /** * @var array */ protected $config = array(); /** * @var seafile_api */ protected $api; /** * List of SeaFile libraries * * @var array */ protected $libraries; /** * Instance title (mount point) * * @var string */ protected $title; /** * Class constructor */ public function __construct() { $this->rc = rcube::get_instance(); } /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @param bool True on success, False on failure */ public function authenticate($username, $password) { $this->init(true); $token = $this->api->authenticate($username, $password); if ($token) { $_SESSION[$this->title . 'seafile_user'] = $username; $_SESSION[$this->title . 'seafile_token'] = $this->rc->encrypt($token); $_SESSION[$this->title . 'seafile_pass'] = $this->rc->encrypt($password); return true; } $this->api = false; return false; } /** * Get password and name of authenticated user * * @return array Authenticated user data */ public function auth_info() { return array( 'username' => $_SESSION[$this->title . 'seafile_user'], 'password' => $this->rc->decrypt($_SESSION[$this->title . 'seafile_pass']), ); } /** * Initialize SeaFile Web API connection */ protected function init($skip_auth = false) { if ($this->api !== null) { return $this->api !== false; } // read configuration $config = array( 'host' => $this->rc->config->get('fileapi_seafile_host', 'localhost'), 'ssl_verify_peer' => $this->rc->config->get('fileapi_seafile_ssl_verify_peer', true), 'ssl_verify_host' => $this->rc->config->get('fileapi_seafile_ssl_verify_host', true), 'cache' => $this->rc->config->get('fileapi_seafile_cache'), 'cache_ttl' => $this->rc->config->get('fileapi_seafile_cache_ttl', '14d'), 'debug' => $this->rc->config->get('fileapi_seafile_debug', false), ); $this->config = array_merge($config, $this->config); // initialize Web API $this->api = new seafile_api($this->config); if ($skip_auth) { return true; } // try session token if ($_SESSION[$this->title . 'seafile_token'] && ($token = $this->rc->decrypt($_SESSION[$this->title . 'seafile_token'])) ) { $valid = $this->api->ping($token); } if (!$valid) { // already authenticated in session if ($_SESSION[$this->title . 'seafile_user']) { $user = $_SESSION[$this->title . 'seafile_user']; $pass = $this->rc->decrypt($_SESSION[$this->title . 'seafile_pass']); } // try user/pass of the main driver else { $user = $this->config['username']; $pass = $this->config['password']; } if ($user) { $valid = $this->authenticate($user, $pass); } } // throw special exception, so we can ask user for the credentials if (!$valid && empty($_SESSION[$this->title . 'seafile_user'])) { throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH); } else if (!$valid && $this->api->is_error() == seafile_api::TOO_MANY_REQUESTS) { throw new Exception("SeaFile storage temporarily unavailable (too many requests)", file_storage::ERROR); } return $valid; } /** * Configures environment * * @param array $config Configuration * @param string $title Source identifier */ public function configure($config, $title = null) { $this->config = array_merge($this->config, $config); $this->title = $title; } /** * Returns current instance title * * @return string Instance title (mount point) */ public function title() { return $this->title; } /** * Storage driver capabilities * * @return array List of capabilities */ public function capabilities() { // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } return array( file_storage::CAPS_MAX_UPLOAD => $max_filesize, file_storage::CAPS_QUOTA => true, file_storage::CAPS_LOCKS => true, ); } /** * Save configuration of external driver (mount point) * * @param array $driver Driver data * * @throws Exception */ public function driver_create($driver) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Delete configuration of external driver (mount point) * * @param string $title Driver instance name * * @throws Exception */ public function driver_delete($title) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Return list of registered drivers (mount points) * * @return array List of drivers data * @throws Exception */ public function driver_list() { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Update configuration of external driver (mount point) * * @param string $title Driver instance name * @param array $driver Driver data * * @throws Exception */ public function driver_update($title, $driver) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Returns metadata of the driver * * @return array Driver meta data (image, name, form) */ public function driver_metadata() { $image_content = file_get_contents(__DIR__ . '/seafile.png'); $metadata = array( 'image' => 'data:image/png;base64,' . base64_encode($image_content), 'name' => 'SeaFile', 'ref' => 'http://seafile.com', 'description' => 'Storage implementing SeaFile API access', 'form' => array( 'host' => 'hostname', 'username' => 'username', 'password' => 'password', ), ); // these are returned when authentication on folders list fails if ($this->config['username']) { $metadata['form_values'] = array( 'host' => $this->config['host'], 'username' => $this->config['username'], ); } return $metadata; } /** * Validate metadata (config) of the driver * * @param array $metadata Driver metadata * * @return array Driver meta data to be stored in configuration * @throws Exception */ public function driver_validate($metadata) { if (!is_string($metadata['username']) || !strlen($metadata['username'])) { throw new Exception("Missing user name.", file_storage::ERROR); } if (!is_string($metadata['password']) || !strlen($metadata['password'])) { throw new Exception("Missing user password.", file_storage::ERROR); } if (!is_string($metadata['host']) || !strlen($metadata['host'])) { throw new Exception("Missing host name.", file_storage::ERROR); } $this->config['host'] = $metadata['host']; if (!$this->authenticate($metadata['username'], $metadata['password'])) { throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH); } return array( 'host' => $metadata['host'], 'username' => $metadata['username'], 'password' => $metadata['password'], ); } /** * Create a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_create($file_name, $file) { list($fn, $repo_id) = $this->find_library($file_name); if (empty($repo_id)) { throw new Exception("Storage error. Folder not found.", file_storage::ERROR); } if ($file['path']) { $file['data'] = $file['path']; } else if (is_resource($file['content'])) { $file['data'] = $file['content']; } else { $fp = fopen('php://temp', 'wb'); fwrite($fp, $file['content'], strlen($file['content'])); $file['data'] = $fp; unset($file['content']); } $created = $this->api->file_upload($repo_id, $fn, $file); if ($fp) { fclose($fp); } if (!$created) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving file to SeaFile server"), true, false); throw new Exception("Storage error. Saving file failed.", file_storage::ERROR); } } /** * Update a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_update($file_name, $file) { list($fn, $repo_id) = $this->find_library($file_name); if (empty($repo_id)) { throw new Exception("Storage error. Folder not found.", file_storage::ERROR); } if ($file['path']) { $file['data'] = $file['path']; } else if (is_resource($file['content'])) { $file['data'] = $file['content']; } else { $fp = fopen('php://temp', 'wb'); fwrite($fp, $file['content'], strlen($file['content'])); $file['data'] = $fp; unset($file['content']); } $saved = $this->api->file_update($repo_id, $fn, $file); if ($fp) { fclose($fp); } if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving file to SeaFile server"), true, false); throw new Exception("Storage error. Saving file failed.", file_storage::ERROR); } } /** * Delete a file. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_delete($file_name) { list($file_name, $repo_id) = $this->find_library($file_name); if ($repo_id && $file_name != '/') { $deleted = $this->api->file_delete($repo_id, $file_name); } if (!$deleted) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error deleting object from SeaFile server"), true, false); throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR); } } /** * Return file body. * * @param string $file_name Name of a file (with folder path) * @param array $params Parameters (force-download) * @param resource $fp Print to file pointer instead (send no headers) * * @throws Exception */ public function file_get($file_name, $params = array(), $fp = null) { list($fn, $repo_id) = $this->find_library($file_name); $file = $this->api->file_info($repo_id, $fn); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $file = $this->from_file_object($file); // get file location on SeaFile server for download if ($file['size']) { $link = $this->api->file_get($repo_id, $fn); } // write to file pointer, send no headers if ($fp) { if ($file['size']) { $this->save_file_content($link, $fp); } return; } if (!empty($params['force-download'])) { $disposition = 'attachment'; header("Content-Type: application/octet-stream"); // @TODO // if ($browser->ie) // header("Content-Type: application/force-download"); } else { $mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']); $disposition = 'inline'; header("Content-Transfer-Encoding: binary"); header("Content-Type: $mimetype"); } $filename = addcslashes($file['name'], '"'); // Workaround for nasty IE bug (#1488844) // If Content-Disposition header contains string "attachment" e.g. in filename // IE handles data as attachment not inline /* @TODO if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) { $filename = str_ireplace('attachment', 'attach', $filename); } */ header("Content-Length: " . $file['size']); header("Content-Disposition: $disposition; filename=\"$filename\""); // just send redirect to SeaFile server if ($file['size']) { // In view-mode we can't redirect to SeaFile server because: // - it responds with Content-Disposition: attachment, which causes that // e.g. previewing images is not possible // - pdf/odf viewers can't follow redirects for some reason (#4590) if (empty($params['force-download'])) { if ($fp = fopen('php://output', 'wb')) { $this->save_file_content($link, $fp); fclose($fp); die; } } header("Location: $link"); } die; } /** * Returns file metadata. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_info($file_name) { list($file, $repo_id) = $this->find_library($file_name); $file = $this->api->file_info($repo_id, $file); if (empty($file)) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $file = $this->from_file_object($file); return array( 'name' => $file['name'], 'size' => (int) $file['size'], 'type' => (string) $file['type'], 'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '', 'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '', 'modified' => $file['changed'] ? $file['changed']->format('U') : 0, 'created' => $file['created'] ? $file['created']->format('U') : 0, ); } /** * List files in a folder. * * @param string $folder_name Name of a folder with full path * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix') * * @return array List of files (file properties array indexed by filename) * @throws Exception */ public function file_list($folder_name, $params = array()) { list($folder, $repo_id) = $this->find_library($folder_name); // prepare search filter if (!empty($params['search'])) { foreach ($params['search'] as $idx => $value) { if ($idx == 'name') { $params['search'][$idx] = mb_strtoupper($value); } else if ($idx == 'class') { $params['search'][$idx] = file_utils::class2mimetypes($value); } } } // get directory entries $entries = $this->api->directory_entries($repo_id, $folder); $result = array(); foreach ((array) $entries as $idx => $file) { if ($file['type'] != 'file') { continue; } $file = $this->from_file_object($file); // search filter if (!empty($params['search'])) { foreach ($params['search'] as $idx => $value) { if ($idx == 'name') { if (strpos(mb_strtoupper($file['name']), $value) === false) { continue 2; } } else if ($idx == 'class') { foreach ($value as $v) { if (stripos($file['type'], $v) !== false) { continue 2; } } continue 2; } } } $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name']; $result[$filename] = array( 'name' => $file['name'], 'size' => (int) $file['size'], 'type' => (string) $file['type'], 'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '', 'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '', 'modified' => $file['changed'] ? $file['changed']->format('U') : 0, 'created' => $file['created'] ? $file['created']->format('U') : 0, ); unset($entries[$idx]); } // @TODO: pagination, search (by filename, mimetype) // Sorting $sort = !empty($params['sort']) ? $params['sort'] : 'name'; $index = array(); if ($sort == 'mtime') { $sort = 'modified'; } if (in_array($sort, array('name', 'size', 'modified'))) { foreach ($result as $key => $val) { $index[$key] = $val[$sort]; } array_multisort($index, SORT_ASC, SORT_NUMERIC, $result); } if ($params['reverse']) { $result = array_reverse($result, true); } return $result; } /** * Copy a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_copy($file_name, $new_name) { list($src_name, $repo_id) = $this->find_library($file_name); list($dst_name, $dst_repo_id) = $this->find_library($new_name); if ($repo_id && $dst_repo_id) { $path_src = explode('/', $src_name); $path_dst = explode('/', $dst_name); $f_src = array_pop($path_src); $f_dst = array_pop($path_dst); $src_dir = '/' . ltrim(implode('/', $path_src), '/'); $dst_dir = '/' . ltrim(implode('/', $path_dst), '/'); $success = $this->api->file_copy($repo_id, $f_src, $src_dir, $dst_dir, $dst_repo_id); // now rename the file if needed if ($success && $f_src != $f_dst) { $success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst); } } if (!$success) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error copying file on SeaFile server"), true, false); throw new Exception("Storage error. File copying failed.", file_storage::ERROR); } } /** * Move (or rename) a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_move($file_name, $new_name) { list($src_name, $repo_id) = $this->find_library($file_name); list($dst_name, $dst_repo_id) = $this->find_library($new_name); if ($repo_id && $dst_repo_id) { $path_src = explode('/', $src_name); $path_dst = explode('/', $dst_name); $f_src = array_pop($path_src); $f_dst = array_pop($path_dst); $src_dir = '/' . ltrim(implode('/', $path_src), '/'); $dst_dir = '/' . ltrim(implode('/', $path_dst), '/'); if ($src_dir == $dst_dir && $repo_id == $dst_repo_id) { $success = true; } else { $success = $this->api->file_move($repo_id, $src_name, $dst_dir, $dst_repo_id); } // now rename the file if needed if ($success && $f_src != $f_dst) { $success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst); } } if (!$success) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error moving file on SeaFile server"), true, false); throw new Exception("Storage error. File rename failed.", file_storage::ERROR); } } /** * Create a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_create($folder_name) { list($folder, $repo_id) = $this->find_library($folder_name, true); if (empty($repo_id)) { $success = $this->api->library_create($folder_name); } else if ($folder != '/') { $success = $this->api->directory_create($repo_id, $folder); } if (!$success) { throw new Exception("Storage error. Unable to create folder", file_storage::ERROR); } // clear the cache if (empty($repo_id)) { $this->libraries = null; } } /** * Delete a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_delete($folder_name) { list($folder, $repo_id) = $this->find_library($folder_name, true); if ($repo_id && $folder == '/') { $success = $this->api->library_delete($repo_id); } else if ($repo_id) { $success = $this->api->directory_delete($repo_id, $folder); } if (!$success) { throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR); } } /** * Move/Rename a folder. * * @param string $folder_name Name of a folder with full path * @param string $new_name New name of a folder with full path * * @throws Exception on error */ public function folder_move($folder_name, $new_name) { list($folder, $repo_id, $library) = $this->find_library($folder_name, true); list($dest_folder, $dest_repo_id) = $this->find_library($new_name, true); // folders rename/move is possible only in the same library and folder // @TODO: support folder move between libraries and folders // @TODO: support converting library into a folder and vice-versa // library rename if ($repo_id && !$dest_repo_id && $folder == '/' && strpos($new_name, '/') === false) { $success = $this->api->library_rename($repo_id, $new_name, $library['desc']); } // folder rename else if ($folder != '/' && $dest_folder != '/' && $repo_id && $repo_id == $dest_repo_id) { $path_src = explode('/', $folder); $path_dst = explode('/', $dest_folder); $f_src = array_pop($path_src); $f_dst = array_pop($path_dst); $src_dir = implode('/', $path_src); $dst_dir = implode('/', $path_dst); if ($src_dir == $dst_dir) { $success = $this->api->directory_rename($repo_id, $folder, $f_dst); } } if (!$success) { throw new Exception("Storage error. Unable to rename/move folder", file_storage::ERROR); } } + /** + * Subscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_subscribe($folder_name) + { + throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); + } + + /** + * Unsubscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_unsubscribe($folder_name) + { + throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); + } + /** * Returns list of folders. * + * @param array $params List parameters ('type', 'search') + * * @return array List of folders * @throws Exception */ - public function folder_list() + public function folder_list($params = array()) { $libraries = $this->libraries(); $folders = array(); if ($this->config['cache']) { $cache = $this->rc->get_cache('seafile_' . $this->title, $this->config['cache'], $this->config['cache_ttl'], true); if ($cache) { $cached = $cache->get('folders'); } } foreach ($libraries as $library) { if ($library['virtual'] || $library['encrypted']) { continue; } $folders[$library['name']] = $library['mtime']; if ($folder_tree = $this->folders_tree($library, '', $library, $cached)) { $folders = array_merge($folders, $folder_tree); } } if (empty($folders)) { throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR); } if ($cache) { $cache->set('folders', $folders); } // sort folders $folders = array_keys($folders); - usort($folders, array($this, 'sort_folder_comparator')); + usort($folders, array('file_utils', 'sort_folder_comparator')); return $folders; } /** * Returns a list of locks * * This method should return all the locks for a particular URI, including * locks that might be set on a parent URI. * * If child_locks is set to true, this method should also look for * any locks in the subtree of the URI for locks. * * @param string $uri URI * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($uri, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); // get locks list $list = $this->lock_db->lock_list($uri, $child_locks); // convert back resource string into URIs foreach ($list as $idx => $lock) { $list[$idx]['uri'] = $this->resource2uri($lock['uri']); } return $list; } /** * Locks a URI * * @param string $uri URI * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->lock($uri, $lock)) { throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR); } } /** * Removes a lock from a URI * * @param string $path URI * @param array $lock Lock data * * @throws Exception */ public function unlock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->unlock($uri, $lock)) { throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR); } } /** * Return disk quota information for specified folder. * * @param string $folder_name Name of a folder with full path * * @return array Quota * @throws Exception */ public function quota($folder) { if (!$this->init()) { throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR); } $account_info = $this->api->account_info(); if (empty($account_info)) { throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR); } $quota = array( // expected values in kB 'total' => intval($account_info['total'] / 1024), 'used' => intval($account_info['usage'] / 1024), ); return $quota; } /** * Recursively builds folders list */ protected function folders_tree($library, $path, $folder, $cached) { $folders = array(); $fname = strlen($path) ? $path . $folder['name'] : '/'; $root = $library['name'] . ($fname != '/' ? $fname : ''); // nothing changed, use cached folders tree of this folder if ($cached && $cached[$root] && $cached[$root] == $folder['mtime']) { foreach ($cached as $folder_name => $mtime) { if (strpos($folder_name, $root . '/') === 0) { $folders[$folder_name] = $mtime; } } } // get folder content (files and sub-folders) // there's no API method to get only folders else if ($content = $this->api->directory_entries($library['id'], $fname)) { if ($fname != '/') { $fname .= '/'; } foreach ($content as $item) { if ($item['type'] == 'dir' && strlen($item['name'])) { $folders[$root . '/' . $item['name']] = $item['mtime']; // get subfolders recursively $folders_tree = $this->folders_tree($library, $fname, $item, $cached); if (!empty($folders_tree)) { $folders = array_merge($folders, $folders_tree); } } } } return $folders; } - /** - * Callback for uasort() that implements correct - * locale-aware case-sensitive sorting - */ - protected function sort_folder_comparator($str1, $str2) - { - $path1 = explode('/', $str1); - $path2 = explode('/', $str2); - - foreach ($path1 as $idx => $folder1) { - $folder2 = $path2[$idx]; - - if ($folder1 === $folder2) { - continue; - } - - return strcoll($folder1, $folder2); - } - } - /** * Get list of SeaFile libraries */ protected function libraries() { // get from memory, @TODO: cache in rcube_cache? if ($this->libraries !== null) { return $this->libraries; } if (!$this->init()) { throw new Exception("Storage error. Unable to get list of SeaFile libraries.", file_storage::ERROR); } if ($list = $this->api->library_list()) { $this->libraries = $list; } else { $this->libraries = array(); } return $this->libraries; } /** * Find library ID from folder name */ protected function find_library($folder_name, $no_exception = false) { $libraries = $this->libraries(); foreach ($libraries as $lib) { $path = $lib['name'] . '/'; if ($folder_name == $lib['name'] || strpos($folder_name, $path) === 0) { if (empty($library) || strlen($library['name']) < strlen($lib['name'])) { $library = $lib; } } } if (empty($library)) { if (!$no_exception) { throw new Exception("Storage error. Library not found.", file_storage::ERROR); } } else { $folder = substr($folder_name, strlen($library['name']) + 1); } return array( '/' . ($folder ? $folder : ''), $library['id'], $library ); } /** * Get file object. * * @param string $file_name Name of a file (with folder path) * @param kolab_storage_folder $folder Reference to folder object * * @return array File data * @throws Exception */ protected function get_file_object(&$file_name, &$folder = null) { // extract file path and file name $path = explode(file_storage::SEPARATOR, $file_name); $file_name = array_pop($path); $folder_name = implode(file_storage::SEPARATOR, $path); if ($folder_name === '') { throw new Exception("Missing folder name", file_storage::ERROR); } // get folder object $folder = $this->get_folder_object($folder_name); $files = $folder->select(array( array('type', '=', 'file'), array('filename', '=', $file_name) )); return $files[0]; } /** * Simplify internal structure of the file object */ protected function from_file_object($file) { if ($file['type'] != 'file') { return null; } // file modification time if ($file['mtime']) { try { $file['changed'] = new DateTime('@' . $file['mtime']); } catch (Exception $e) { } } // find file mimetype from extension $file['type'] = file_utils::ext_to_type($file['name']); unset($file['id']); unset($file['mtime']); return $file; } /** * Save remote file into file pointer */ protected function save_file_content($location, $fp) { if (!$fp || !$location) { return false; } $config = array_merge($this->config, array('store_bodies' => true)); $request = seafile_api::http_request($config); if (!$request) { return false; } $observer = new seafile_request_observer(); $observer->set_fp($fp); try { $request->setUrl($location); $request->attach($observer); $response = $request->send(); $status = $response->getStatus(); $response->getBody(); // returns nothing $request->detach($observer); if ($status != 200) { throw new Exception("Unable to save file. Status $status."); } } catch (Exception $e) { rcube::raise_error($e, true, false); return false; } return true; } protected function uri2resource($uri) { list($file, $repo_id, $library) = $this->find_library($uri); // convert to imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP'); return 'seafile://' . urlencode($library['owner']) . '@' . $this->config['host'] . '/' . $uri; } protected function resource2uri($resource) { if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) { throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR); } $user = urldecode($matches[1]); $uri = $matches[3]; // convert from imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET); return $uri; } /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } } diff --git a/lib/drivers/webdav/webdav_file_storage.php b/lib/drivers/webdav/webdav_file_storage.php index 052969b..edb82c0 100644 --- a/lib/drivers/webdav/webdav_file_storage.php +++ b/lib/drivers/webdav/webdav_file_storage.php @@ -1,972 +1,978 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ require 'SabreDAV/vendor/autoload.php'; use Sabre\DAV\Client; class webdav_file_storage implements file_storage { /** * @var rcube */ protected $rc; /** * @var array */ protected $config = array(); /** * @var string */ protected $title; /** * @var Sabre\DAV\Client */ protected $client; /** * Class constructor */ public function __construct() { $this->rc = rcube::get_instance(); } /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @param bool True on success, False on failure */ public function authenticate($username, $password) { $settings = array( 'baseUri' => $this->config['baseuri'], 'userName' => $username, 'password' => $password, 'authType' => Client::AUTH_BASIC, ); $client = new Client($settings); try { $client->propfind('', array()); } catch (Exception $e) { return false; } if ($this->title) { $_SESSION[$this->title . '_webdav_user'] = $username; $_SESSION[$this->title . '_webdav_pass'] = $this->rc->encrypt($password); $this->client = $client; } return true; } /** * Get password and name of authenticated user * * @return array Authenticated user data */ public function auth_info() { return array( 'username' => $this->config['username'], 'password' => $this->config['password'], ); } /** * Configures environment * * @param array $config Configuration * @param string $title Source identifier */ public function configure($config, $title = null) { if (!empty($config['host'])) { $config['baseuri'] = $config['host']; } $this->config = array_merge($this->config, $config); $this->title = $title; } /** * Initializes WebDAV client */ protected function init() { if ($this->client !== null) { return true; } // Load configuration for main driver $config['baseuri'] = $this->rc->config->get('fileapi_webdav_baseuri'); if (!empty($config['baseuri'])) { $config['username'] = $_SESSION['username']; $config['password'] = $this->rc->decrypt($_SESSION['password']); } $this->config = array_merge($config, $this->config); // Use session username if not set in configuration if (!isset($this->config['username'])) { $this->config['username'] = $_SESSION[$this->title . '_webdav_user']; } if (!isset($this->config['password'])) { $this->config['password'] = $this->rc->decrypt($_SESSION[$this->title . '_webdav_pass']); } if (empty($this->config['baseuri'])) { throw new Exception("Missing base URI of WebDAV server", file_storage::ERROR_NOAUTH); } $this->client = new Client(array( 'baseUri' => $this->config['baseuri'], 'userName' => $this->config['username'], 'password' => $this->config['password'], 'authType' => Client::AUTH_BASIC, )); } /** * Returns current instance title * * @return string Instance title (mount point) */ public function title() { return $this->title; } /** * Storage driver capabilities * * @return array List of capabilities */ public function capabilities() { // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } return array( file_storage::CAPS_MAX_UPLOAD => $max_filesize, file_storage::CAPS_QUOTA => true, file_storage::CAPS_LOCKS => true, //TODO: Implement WebDAV locks ); } /** * Save configuration of external driver (mount point) * * @param array $driver Driver data * * @throws Exception */ public function driver_create($driver) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Delete configuration of external driver (mount point) * * @param string $name Driver instance name * * @throws Exception */ public function driver_delete($name) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Return list of registered drivers (mount points) * * @return array List of drivers data * @throws Exception */ public function driver_list() { return array(); //TODO: Stub. Not implemented. } /** * Update configuration of external driver (mount point) * * @param string $title Driver instance title * @param array $driver Driver data * * @throws Exception */ public function driver_update($title, $driver) { throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Returns metadata of the driver * * @return array Driver meta data (image, name, form) */ public function driver_metadata() { $image_content = file_get_contents(__DIR__ . '/webdav.png'); $metadata = array( 'image' => 'data:image/png;base64,' . base64_encode($image_content), 'name' => 'WebDAV', 'ref' => 'http://www.webdav.org/', 'description' => 'WebDAV client', 'form' => array( 'baseuri' => 'baseuri', 'username' => 'username', 'password' => 'password', ), ); // these are returned when authentication on folders list fails if ($this->config['username']) { $metadata['form_values'] = array( 'baseuri' => $this->config['baseuri'], 'username' => $this->config['username'], ); } return $metadata; } /** * Validate metadata (config) of the driver * * @param array $metadata Driver metadata * * @return array Driver meta data to be stored in configuration * @throws Exception */ public function driver_validate($metadata) { if (!is_string($metadata['username']) || !strlen($metadata['username'])) { throw new Exception("Missing user name.", file_storage::ERROR); } if (!is_string($metadata['password']) || !strlen($metadata['password'])) { throw new Exception("Missing user password.", file_storage::ERROR); } if (!is_string($metadata['baseuri']) || !strlen($metadata['baseuri'])) { throw new Exception("Missing base URL.", file_storage::ERROR); } // Ensure baseUri ends with a slash $base_uri = $metadata['baseuri']; if (substr($base_uri, -1) != '/') { $base_uri .= '/'; } $this->config['baseuri'] = $base_uri; if (!$this->authenticate($metadata['username'], $metadata['password'])) { throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH); } return array( 'host' => $base_uri, 'port' => 0, 'username' => $metadata['username'], 'password' => $metadata['password'], ); } /** * Create a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_create($file_name, $file) { $this->init(); if ($file['path']) { $data = fopen($file['path'], 'r'); } else { // Resource or data $data = $file['content']; } $file_name = $this->encode_path($file_name); $response = $this->client->request('PUT', $file_name, $data); if ($response['statusCode'] != 201) { throw new Exception("Storage error. " . $response['body'], file_storage::ERROR); } } /** * Update a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path, type) * * @throws Exception */ public function file_update($file_name, $file) { $this->init(); if ($file['path']) { $data = fopen($file['path'], 'r'); } else { //Resource or data $data = $file['content']; } $file_name = $this->encode_path($file_name); $response = $this->client->request('PUT', $file_name, $data); if ($response['statusCode'] != 204) { throw new Exception("Storage error. " . $response['body'], file_storage::ERROR); } } /** * Delete a file. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_delete($file_name) { $this->init(); $file_name = $this->encode_path($file_name); $response = $this->client->request('DELETE', $file_name); if ($response['statusCode'] != 204) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } /** * Return file body. * * @param string $file_name Name of a file (with folder path) * @param array $params Parameters (force-download) * @param resource $fp Print to file pointer instead (send no headers) * * @throws Exception */ public function file_get($file_name, $params = array(), $fp = null) { $this->init(); // TODO: Write directly to $fp $file_name = $this->encode_path($file_name); $response = $this->client->request('GET', $file_name); if ($response['statusCode'] != 200) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $size = $response['headers']['content-length'][0]; // write to file pointer, send no headers if ($fp) { if ($size) { fwrite($fp, $response['body']); } return; } if (!empty($params['force-download'])) { $disposition = 'attachment'; header("Content-Type: application/octet-stream"); // @TODO // if ($browser->ie) // header("Content-Type: application/force-download"); } else { $mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']); $disposition = 'inline'; header("Content-Transfer-Encoding: binary"); header("Content-Type: $mimetype"); } $filename = addcslashes(end(explode('/', $file_name)), '"'); // Workaround for nasty IE bug (#1488844) // If Content-Disposition header contains string "attachment" e.g. in filename // IE handles data as attachment not inline /* @TODO if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) { $filename = str_ireplace('attachment', 'attach', $filename); } */ header("Content-Length: " . $size); header("Content-Disposition: $disposition; filename=\"$filename\""); if ($size) echo $response['body']; } /** * Returns file metadata. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_info($file_name) { $this->init(); try { $props = $this->client->propfind($this->encode_path($file_name), array( '{DAV:}resourcetype', '{DAV:}getcontentlength', '{DAV:}getcontenttype', '{DAV:}getlastmodified', '{DAV:}creationdate' ), 0); } catch (Exception $e) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } $mtime = new DateTime($props['{DAV:}getlastmodified']); $ctime = new DateTime($props['{DAV:}creationdate']); return array ( 'name' => end(explode('/', $file_name)), 'size' => (int) $props['{DAV:}getcontentlength'], 'type' => (string) $props['{DAV:}getcontenttype'], 'mtime' => $mtime ? $mtime->format($this->config['date_format']) : '', 'ctime' => $ctime ? $ctime->format($this->config['date_format']) : '', 'modified' => $mtime ? $mtime->format('U') : 0, 'created' => $ctime ? $ctime->format('U') : 0, ); } /** * List files in a folder. * * @param string $folder_name Name of a folder with full path * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix') * * @return array List of files (file properties array indexed by filename) * @throws Exception */ public function file_list($folder_name, $params = array()) { $this->init(); if (!empty($params['search'])) { foreach ($params['search'] as $idx => $value) { switch ($idx) { case 'name': $params['search']['name'] = mb_strtoupper($value); break; case 'class': $params['search']['class'] = file_utils::class2mimetypes($params['search']['class']); break; } } } try { $items = $this->client->propfind($this->encode_path($folder_name), array( '{DAV:}resourcetype', '{DAV:}getcontentlength', '{DAV:}getcontenttype', '{DAV:}getlastmodified', '{DAV:}creationdate' ), 1); } catch (Exception $e) { throw new Exception("Storage error. Folder not found.", file_storage::ERROR); } $result = array(); foreach ($items as $file => $props) { //Skip directories $is_dir = in_array('{DAV:}collection', $props['{DAV:}resourcetype']->resourceType); if ($is_dir) { continue; } $mtime = new DateTime($props['{DAV:}getlastmodified']); $ctime = new DateTime($props['{DAV:}creationdate']); $ctype = (string) $props['{DAV:}getcontenttype']; $path = $this->get_full_url($file); $path = $this->decode_path($path); $fname = end(explode('/', $path)); if (!empty($params['search'])) { foreach ($params['search'] as $idx => $value) { switch ($idx) { case 'name': if (stripos(mb_strtoupper($fname), $value) === false) { continue 3; // skip the file } break; case 'class': foreach ($value as $type) { if (stripos($ctype, $type) !== false) { continue 3; } } continue 3; // skip the file break; } } } $result[$path] = array( 'name' => $fname, 'size' => (int) $props['{DAV:}getcontentlength'], 'type' => $ctype, 'mtime' => $mtime ? $mtime->format($this->config['date_format']) : '', 'ctime' => $ctime ? $ctime->format($this->config['date_format']) : '', 'modified' => $mtime ? $mtime->format('U') : 0, 'created' => $ctime ? $ctime->format('U') : 0, ); } // @TODO: pagination // Sorting $sort = !empty($params['sort']) ? $params['sort'] : 'name'; $index = array(); if ($sort == 'mtime') { $sort = 'modified'; } if (in_array($sort, array('name', 'size', 'modified'))) { foreach ($result as $key => $val) { $index[$key] = $val[$sort]; } array_multisort($index, SORT_ASC, SORT_NUMERIC, $result); } if ($params['reverse']) { $result = array_reverse($result, true); } return $result; } /** * Copy a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_copy($file_name, $new_name) { $this->init(); $request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name)); $file_name = $this->encode_path($file_name); $response = $this->client->request('COPY', $file_name, null, $request); if ($response['statusCode'] != 201) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } /** * Move (or rename) a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_move($file_name, $new_name) { $this->init(); $request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name)); $file_name = $this->encode_path($file_name); $response = $this->client->request('MOVE', $file_name, null, $request); if ($response['statusCode'] != 201) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } /** * Create a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_create($folder_name) { $this->init(); $folder_name = $this->encode_path($folder_name); $response = $this->client->request('MKCOL', $folder_name); if ($response['statusCode'] != 201) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } /** * Delete a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception on error */ public function folder_delete($folder_name) { $this->init(); $folder_name = $this->encode_path($folder_name); $response = $this->client->request('DELETE', $folder_name); if ($response['statusCode'] != 204) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } /** * Move/Rename a folder. * * @param string $folder_name Name of a folder with full path * @param string $new_name New name of a folder with full path * * @throws Exception on error */ public function folder_move($folder_name, $new_name) { $this->init(); $request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name)); $folder_name = $this->encode_path($folder_name); $response = $this->client->request('MOVE', $folder_name, null, $request); if ($response['statusCode'] != 201) { throw new Exception("Storage error: " . $response['body'], file_storage::ERROR); } } + /** + * Subscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_subscribe($folder_name) + { + throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); + } + + /** + * Unsubscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_unsubscribe($folder_name) + { + throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED); + } + /** * Returns list of folders. * + * @param array $params List parameters ('type', 'search') + * * @return array List of folders * @throws Exception */ - public function folder_list() + public function folder_list($params = array()) { $this->init(); try { $items = $this->client->propfind('', array( '{DAV:}resourcetype', ), 'infinity'); // TODO: Replace infinity by recursion // Many servers just do not support 'Depth: infinity' for security reasons // E.g. SabreDAV has this optional and disabled by default } catch (Exception $e) { throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH); } $result = array(); foreach ($items as $file => $props) { // Skip files $is_dir = in_array('{DAV:}collection', $props['{DAV:}resourcetype']->resourceType); if (!$is_dir) { continue; } $path = $this->get_relative_url($file); $path = $this->decode_path($path); if ($path !== '') { $result[] = $path; } } // ensure sorted folders - usort($result, array($this, 'sort_folder_comparator')); + usort($result, array('file_utils', 'sort_folder_comparator')); return $result; } /** * Returns a list of locks * * This method should return all the locks for a particular URI, including * locks that might be set on a parent URI. * * If child_locks is set to true, this method should also look for * any locks in the subtree of the URI for locks. * * @param string $uri URI * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($uri, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); // get locks list $list = $this->lock_db->lock_list($uri, $child_locks); // convert back resource string into URIs foreach ($list as $idx => $lock) { $list[$idx]['uri'] = $this->resource2uri($lock['uri']); } return $list; } /** * Locks a URI * * @param string $uri URI * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->lock($uri, $lock)) { throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR); } } /** * Removes a lock from a URI * * @param string $path URI * @param array $lock Lock data * * @throws Exception */ public function unlock($uri, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($uri); if (!$this->lock_db->unlock($uri, $lock)) { throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR); } } /** * Return disk quota information for specified folder. * * @param string $folder_name Name of a folder with full path * * @return array Quota * @throws Exception */ public function quota($folder) { $this->init(); $props = $this->client->propfind($this->encode_path($folder), array( '{DAV:}quota-available-bytes', '{DAV:}quota-used-bytes', ), 0); $used = $props['{DAV:}quota-used-bytes']; $available = $props['{DAV:}quota-available-bytes']; return array( // expected values in kB 'total' => ($used + $available) / 1024, 'used' => $used / 1024, ); } /** * Gets the relative URL of a resource * * @param string $url WebDAV URL * @return string Path relative to root (title/.) */ protected function get_relative_url($url) { $url = $this->client->getAbsoluteUrl($url); return trim(str_replace($this->config['baseuri'], '', $url), '/'); } /** * Gets the full URL of a resource * * @param string $url WebDAV URL * @return string Path relative to chwala root */ protected function get_full_url($url) { if (!empty($this->title)) { return $this->title . '/' . $this->get_relative_url($url); } return $this->get_relative_url($url); } /** * Encode folder/file names in the path * so it can be used as URL * * @param string $path File/folder path * * @return string Encoded URL */ protected function encode_path($path) { $path = explode('/', $path); $path = array_map('rawurlencode', $path); return implode('/', $path); } /** * Decode folder/file URL path * * @param string $path File/folder path * * @return string Decoded path */ protected function decode_path($path) { $path = explode('/', $path); $path = array_map('rawurldecode', $path); return implode('/', $path); } protected function uri2resource($uri) { // convert to imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP'); $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']); return 'webdav://' . urlencode($base) . '/' . $uri; } protected function resource2uri($resource) { if (!preg_match('|^webdav://(.*)$|', $resource, $matches)) { throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR); } $uri = explode('/', $matches[1], 2); $uri = end($uri); // convert from imap charset (to be safe to store in DB) $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET); return $uri; } /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } - - /** - * Callback for uasort() that implements correct - * locale-aware case-sensitive sorting - */ - protected function sort_folder_comparator($str1, $str2) - { - $path1 = explode('/', $str1); - $path2 = explode('/', $str2); - - foreach ($path1 as $idx => $folder1) { - $folder2 = $path2[$idx]; - - if ($folder1 === $folder2) { - continue; - } - - return strcoll($folder1, $folder2); - } - } } diff --git a/lib/file_api_core.php b/lib/file_api_core.php index e962ba0..6034a23 100644 --- a/lib/file_api_core.php +++ b/lib/file_api_core.php @@ -1,324 +1,324 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_core extends file_locale { const API_VERSION = 2; const ERROR_CODE = 500; const ERROR_INVALID = 501; const OUTPUT_JSON = 'application/json'; const OUTPUT_HTML = 'text/html'; public $config = array( 'date_format' => 'Y-m-d H:i', 'language' => 'en_US', ); protected $app_name = 'Kolab File API'; protected $drivers = array(); protected $backend; /** * Returns API version */ public function client_version() { return self::API_VERSION; } /** * Initialise authentication/configuration backend class * * @return file_storage Main storage driver */ public function get_backend() { if ($this->backend) { return $this->backend; } $rcube = rcube::get_instance(); $driver = $rcube->config->get('fileapi_backend', 'kolab'); $this->backend = $this->load_driver_object($driver); // configure api $this->backend->configure($this->config); return $this->backend; } /** * Return supported/enabled external storage instances * * @param bool $as_objects Return drivers as objects not config data * * @return array List of storage drivers */ public function get_drivers($as_objects = false) { $rcube = rcube::get_instance(); $enabled = $rcube->config->get('fileapi_drivers'); $preconf = $rcube->config->get('fileapi_sources'); $result = array(); $all = array(); $iRony = defined('KOLAB_DAV_ROOT'); if (!empty($enabled)) { $backend = $this->get_backend(); $drivers = $backend->driver_list(); foreach ($drivers as $item) { // Disable webdav sources/drivers in iRony // It does not work when the API is used where // some SabreDAV classes are redefined if ($iRony && $item['driver'] == 'webdav') { continue; } $all[] = $item['title']; if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) { $result[] = $as_objects ? $this->get_driver_object($item) : $item; } } } if (empty($result) && !empty($preconf)) { foreach ((array) $preconf as $title => $item) { if (!in_array($title, $all)) { $item['title'] = $title; $item['admin'] = true; $result[] = $as_objects ? $this->get_driver_object($item) : $item; } } } return $result; } /** * Return driver for specified file/folder path * * @param string $path Folder/file path * * @return array Storage driver object, modified path, driver config */ public function get_driver($path) { $drivers = $this->get_drivers(); foreach ($drivers as $item) { $prefix = $item['title'] . file_storage::SEPARATOR; if ($path == $item['title'] || strpos($path, $prefix) === 0) { $selected = $item; break; } } if (empty($selected)) { return array($this->get_backend(), $path); } $path = substr($path, strlen($selected['title']) + 1); return array($this->get_driver_object($selected), $path, $selected); } /** * Initialize driver instance * * @param array $config Driver config * * @return file_storage Storage driver instance */ public function get_driver_object($config) { $key = $config['title']; if (empty($this->drivers[$key])) { $this->drivers[$key] = $driver = $this->load_driver_object($config['driver']); if ($config['username'] == '%u') { $backend = $this->get_backend(); $auth_info = $backend->auth_info(); $config['username'] = $auth_info['username']; $config['password'] = $auth_info['password']; } else if (!empty($config['password']) && empty($config['admin']) && !empty($key)) { $config['password'] = $this->decrypt($config['password']); } // configure api $driver->configure(array_merge($config, $this->config), $key); } return $this->drivers[$key]; } /** * Loads a driver */ public function load_driver_object($name) { $class = $name . '_file_storage'; if (!class_exists($class, false)) { $include_path = __DIR__ . "/drivers/$name" . PATH_SEPARATOR; $include_path .= ini_get('include_path'); set_include_path($include_path); } return new $class; } /** * Returns storage(s) capabilities * * @return array Capabilities */ public function capabilities() { $rcube = rcube::get_instance(); $backend = $this->get_backend(); $caps = array(); // check support for upload progress if (($progress_sec = $rcube->config->get('upload_progress')) && ini_get('apc.rfc1867') && function_exists('apc_fetch') ) { $caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name'); $caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec; } // get capabilities of main storage module foreach ($backend->capabilities() as $name => $value) { // skip disabled capabilities if ($value !== false) { $caps[$name] = $value; } } // get capabilities of other drivers $drivers = $this->get_drivers(true); foreach ($drivers as $driver) { if ($driver != $backend) { $title = $driver->title(); foreach ($driver->capabilities() as $name => $value) { // skip disabled capabilities if ($value !== false) { - $caps['roots'][$title][$name] = $value; + $caps['MOUNTPOINTS'][$title][$name] = $value; } } } } return $caps; } /** * Return mimetypes list supported by built-in viewers * * @return array List of mimetypes */ protected function supported_mimetypes() { $mimetypes = array(); $dir = __DIR__ . '/viewers'; if ($handle = opendir($dir)) { while (false !== ($file = readdir($handle))) { if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) { include_once $dir . '/' . $file; $class = 'file_viewer_' . $matches[1]; $viewer = new $class($this); $mimetypes = array_merge($mimetypes, $viewer->supported_mimetypes()); } } closedir($handle); } return $mimetypes; } /** * Encrypts data with current user password * * @param string $str A string to encrypt * * @return string Encrypted string (and base64-encoded) */ public function encrypt($str) { $rcube = rcube::get_instance(); $key = $this->get_crypto_key(); return $rcube->encrypt($str, $key, true); } /** * Decrypts data encrypted with encrypt() method * * @param string $str Encrypted string (base64-encoded) * * @return string Decrypted string */ public function decrypt($str) { $rcube = rcube::get_instance(); $key = $this->get_crypto_key(); return $rcube->decrypt($str, $key, true); } /** * Set encryption password */ protected function get_crypto_key() { $key = 'chwala_crypto_key'; $rcube = rcube::get_instance(); $backend = $this->get_backend(); $user = $backend->auth_info(); $password = $user['password'] . $user['username']; // encryption password must be 24 characters, no less, no more if (($len = strlen($password)) > 24) { $password = substr($password, 0, 24); } else { $password = $password . substr($rcube->config->get('des_key'), 0, 24 - $len); } $rcube->config->set($key, $password); return $key; } } diff --git a/lib/file_api_lib.php b/lib/file_api_lib.php index c26d320..940fc50 100644 --- a/lib/file_api_lib.php +++ b/lib/file_api_lib.php @@ -1,185 +1,187 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * This class gives access to Chwala API as a library */ class file_api_lib extends file_api_core { /** * API methods handler */ public function __call($name, $arguments) { $this->init(); switch ($name) { case 'configure': foreach (array_keys($this->config) as $name) { if (isset($arguments[0][$name])) { $this->config[$name] = $arguments[0][$name]; } } return $this->config; case 'mimetypes': return $this->supported_mimetypes(); case 'file_list': $args = array( 'folder' => $arguments[0], ); break; case 'file_create': case 'file_update': $args = array( 'file' => $arguments[0], 'path' => $arguments[1]['path'], 'content' => $arguments[1]['content'], 'content-type' => $arguments[1]['type'], ); break; case 'file_delete': case 'file_info': $args = array( 'file' => $arguments[0], ); break; case 'file_copy': case 'file_move': $args = array( 'file' => array($arguments[0] => $arguments[1]), ); break; case 'file_get': // override default action, we need only to support // writes to file handle list($driver, $path) = $this->get_driver($arguments[0]); $driver->file_get($path, $arguments[1], $arguments[2]); return; case 'folder_list': // no arguments $args = array(); break; case 'folder_create': + case 'folder_subscribe': + case 'folder_unsubscribe': case 'folder_delete': $args = array( 'folder' => $arguments[0], ); break; case 'folder_move': $args = array( 'folder' => $arguments[0], 'new' => $arguments[1], ); break; case 'lock_create': case 'lock_delete': $args = $arguments[1]; $args['uri'] = $arguments[0]; break; case 'lock_list': $args = array( 'uri' => $arguments[0], 'child_locks' => $arguments[1], ); break; default: throw new Exception("Invalid method name", \file_storage::ERROR_UNSUPPORTED); } require_once __DIR__ . "/api/$name.php"; $class = "file_api_$name"; $handler = new $class($this, $args); return $handler->handle(); } /** * Configure environment (this is to be overriden by implementation class) */ protected function init() { } } /** * Common handler class, from which action handler classes inherit */ class file_api_common { protected $api; protected $rc; protected $args; public function __construct($api, $args) { $this->rc = rcube::get_instance(); $this->api = $api; $this->args = $args; } /** * Request handler */ public function handle() { // disable script execution time limit, so we can handle big files @set_time_limit(0); } /** * Parse driver metadata information */ protected function parse_metadata($metadata, $default = false) { if ($default) { unset($metadata['form']); $metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')'; } // localize form labels foreach ($metadata['form'] as $key => $val) { $label = $this->api->translate('form.' . $val); if (strpos($label, 'form.') !== 0) { $metadata['form'][$key] = $label; } } return $metadata; } } diff --git a/lib/file_storage.php b/lib/file_storage.php index 5190b56..81042e7 100644 --- a/lib/file_storage.php +++ b/lib/file_storage.php @@ -1,314 +1,337 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ interface file_storage { // capabilities const CAPS_ACL = 'ACL'; const CAPS_MAX_UPLOAD = 'MAX_UPLOAD'; const CAPS_PROGRESS_NAME = 'PROGRESS_NAME'; const CAPS_PROGRESS_TIME = 'PROGRESS_TIME'; const CAPS_QUOTA = 'QUOTA'; const CAPS_LOCKS = 'LOCKS'; const CAPS_SUBSCRIPTIONS = 'SUBSCRIPTIONS'; // config const SEPARATOR = '/'; // error codes const ERROR_LOCKED = 423; const ERROR = 500; const ERROR_UNAVAILABLE = 503; const ERROR_FORBIDDEN = 530; const ERROR_FILE_EXISTS = 550; const ERROR_UNSUPPORTED = 570; const ERROR_NOAUTH = 580; // locks const LOCK_SHARED = 'shared'; const LOCK_EXCLUSIVE = 'exclusive'; const LOCK_INFINITE = 'infinite'; + // list filters + const FILTER_UNSUBSCRIBED = 'unsubscribed'; + /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @return bool True on success, False on failure */ public function authenticate($username, $password); /** * Get password and name of authenticated user * * @return array Authenticated user data */ public function auth_info(); /** * Configures environment * * @param array $config Configuration * @param string $title Driver instance identifier */ public function configure($config, $title = null); /** * Returns current instance title * * @return string Instance title (mount point) */ public function title(); /** * Storage driver capabilities * * @return array List of capabilities */ public function capabilities(); /** * Save configuration of external driver (mount point) * * @param array $driver Driver data * * @throws Exception */ public function driver_create($driver); /** * Delete configuration of external driver (mount point) * * @param string $title Driver instance title * * @throws Exception */ public function driver_delete($title); /** * Return list of registered drivers (mount points) * * @return array List of drivers data * @throws Exception */ public function driver_list(); /** * Returns metadata of the driver * * @return array Driver meta data (image, name, form) */ public function driver_metadata(); /** * Validate metadata (config) of the driver * * @param array $metadata Driver metadata * * @return array Driver meta data to be stored in configuration * @throws Exception */ public function driver_validate($metadata); /** * Update configuration of external driver (mount point) * * @param string $title Driver instance title * @param array $driver Driver data * * @throws Exception */ public function driver_update($title, $driver); /** * Create a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path/content, type), where * content might be a string or resource * * @throws Exception */ public function file_create($file_name, $file); /** * Update a file. * * @param string $file_name Name of a file (with folder path) * @param array $file File data (path/content, type) * * @throws Exception */ public function file_update($file_name, $file); /** * Delete a file. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_delete($file_name); /** * Returns file body. * * @param string $file_name Name of a file (with folder path) * @param array $params Parameters (force-download) * @param resource $fp Print to file pointer instead (send no headers) * * @throws Exception */ public function file_get($file_name, $params = array(), $fp = null); /** * Move (or rename) a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_move($file_name, $new_name); /** * Copy a file. * * @param string $file_name Name of a file (with folder path) * @param string $new_name New name of a file (with folder path) * * @throws Exception */ public function file_copy($file_name, $new_name); /** * Returns file metadata. * * @param string $file_name Name of a file (with folder path) * * @throws Exception */ public function file_info($file_name); /** * List files in a folder. * * @param string $folder_name Name of a folder with full path * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix') * * @return array List of files (file properties array indexed by filename) * @throws Exception */ public function file_list($folder_name, $params = array()); /** * Create a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception */ public function folder_create($folder_name); /** * Delete a folder. * * @param string $folder_name Name of a folder with full path * * @throws Exception */ public function folder_delete($folder_name); /** * Move/Rename a folder. * * @param string $folder_name Name of a folder with full path * @param string $new_name New name of a folder with full path * * @throws Exception */ public function folder_move($folder_name, $new_name); + /** + * Subscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_subscribe($folder_name); + + /** + * Unsubscribe a folder. + * + * @param string $folder_name Name of a folder with full path + * + * @throws Exception + */ + public function folder_unsubscribe($folder_name); + /** * Returns list of folders. * + * @param array $params List parameters ('type', 'search') + * * @return array List of folders * @throws Exception */ - public function folder_list(); + public function folder_list($params = array()); /** * Returns a list of locks * * This method should return all the locks for a particular URI, including * locks that might be set on a parent URI. * * If child_locks is set to true, this method should also look for * any locks in the subtree of the URI for locks. * * @param string $uri URI * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($uri, $child_locks = false); /** * Locks a URI * * @param string $uri URI * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($uri, $lock); /** * Removes a lock from a URI * * @param string $path URI * @param array $lock Lock data * * @throws Exception */ public function unlock($uri, $lock); /** * Return disk quota information for specified folder. * * @param string $folder_name Name of a folder with full path * * @return array Quota * @throws Exception */ public function quota($folder); } diff --git a/lib/file_utils.php b/lib/file_utils.php index eebd53a..b376c03 100644 --- a/lib/file_utils.php +++ b/lib/file_utils.php @@ -1,189 +1,211 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_utils { static $class_map = array( 'document' => array( // text 'text/', 'application/rtf', 'application/x-rtf', 'application/xml', // office 'application/wordperfect', 'application/excel', 'application/msword', 'application/msexcel', 'application/mspowerpoint', 'application/vnd.ms-word', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument', 'application/vnd.oasis.opendocument', 'application/vnd.sun.xml.calc', 'application/vnd.sun.xml.writer', 'application/vnd.stardivision.calc', 'application/vnd.stardivision.writer', // pdf 'application/pdf', 'application/x-pdf', 'application/acrobat', 'application/vnd.pdf', ), 'audio' => array( 'audio/', ), 'video' => array( 'video/', ), 'image' => array( 'image/', 'application/dxf', 'application/acad', ), 'empty' => array( 'application/x-empty', ), ); // list of known file extensions, more in Roundcube config static $ext_map = array( 'doc' => 'application/msword', 'eml' => 'message/rfc822', 'gz' => 'application/gzip', 'htm' => 'text/html', 'html' => 'text/html', 'mp3' => 'audio/mpeg', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odt' => 'application/vnd.oasis.opendocument.text', 'ogg' => 'application/ogg', 'pdf' => 'application/pdf', 'ppt' => 'application/vnd.ms-powerpoint', 'rar' => 'application/x-rar-compressed', 'tgz' => 'application/gzip', 'txt' => 'text/plain', 'zip' => 'application/zip', ); /** * Return list of mimetype prefixes for specified file class * * @param string $class Class name * * @return array List of mimetype prefixes */ static function class2mimetypes($class) { return isset(self::$class_map[$class]) ? self::$class_map[$class] : self::$class_map['empty']; } /** * Finds class of specified mimetype * * @param string $mimetype File mimetype * * @return string Class name */ static function mimetype2class($mimetype) { $mimetype = strtolower($mimetype); foreach (self::$class_map as $class => $prefixes) { foreach ($prefixes as $prefix) { if (strpos($mimetype, $prefix) === 0) { return $class; } } } } /** * Apply some fixes on file mimetype string * * @param string $mimetype File type * * @return string File type */ static function real_mimetype($mimetype) { if (preg_match('/^text\/(.+)/i', $mimetype, $m)) { // fix pdf mimetype if (preg_match('/^(pdf|x-pdf)$/i', $m[1])) { $mimetype = 'application/pdf'; } } return $mimetype; } /** * Find mimetype from file name (extension) * * @param string $filename File name * @param string $fallback Follback mimetype * * @return string File mimetype */ static function ext_to_type($filename, $fallback = 'application/octet-stream') { static $mime_ext = array(); $config = rcube::get_instance()->config; $ext = substr($filename, strrpos($filename, '.') + 1); if (empty($mime_ext)) { $mime_ext = self::$ext_map; foreach ($config->resolve_paths('mimetypes.php') as $fpath) { $mime_ext = array_merge($mime_ext, (array) @include($fpath)); } } if (is_array($mime_ext) && $ext) { $mimetype = $mime_ext[strtolower($ext)]; } return $mimetype ?: $fallback; } /** * Returns script URI * * @return string Script URI */ static function script_uri() { if (!empty($_SERVER['SCRIPT_URI'])) { return $_SERVER['SCRIPT_URI']; } $uri = $_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://'; $uri .= $_SERVER['HTTP_HOST']; $uri .= preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']); return $uri; } + + /** + * Callback for uasort() that implements correct + * locale-aware case-sensitive sorting + */ + public static function sort_folder_comparator($str1, $str2) + { + $path1 = explode(file_storage::SEPARATOR, $str1); + $path2 = explode(file_storage::SEPARATOR, $str2); + + foreach ($path1 as $idx => $folder1) { + $folder2 = $path2[$idx]; + + if ($folder1 === $folder2) { + continue; + } + + return strcoll($folder1, $folder2); + } + + return 0; + } } diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js index 76df8a7..cccccd4 100644 --- a/public_html/js/files_api.js +++ b/public_html/js/files_api.js @@ -1,552 +1,584 @@ /* +--------------------------------------------------------------------------+ | This file is part of the Kolab File API | | | | Copyright (C) 2012-2013, Kolab Systems AG | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ function files_api() { var ref = this; // default config this.translations = {}; this.env = { url: 'api/', directory_separator: '/', resources_dir: 'resources' }; /*********************************************************/ /********* Basic utilities *********/ /*********************************************************/ // set environment variable(s) this.set_env = function(p, value) { if (p != null && typeof p === 'object' && !value) for (var n in p) this.env[n] = p[n]; else this.env[p] = value; }; // add a localized label(s) to the client environment this.tdef = function(p, value) { if (typeof p == 'string') this.translations[p] = value; else if (typeof p == 'object') $.extend(this.translations, p); }; // return a localized string this.t = function(label) { if (this.translations[label]) return this.translations[label]; else return label; }; // print a message into browser console this.log = function(msg) { if (window.console && console.log) console.log(msg); }; /********************************************************/ /********* Remote request methods *********/ /********************************************************/ // send a http POST request to the API service this.post = function(action, data, func) { var url = this.env.url + '?method=' + action; if (!func) func = 'response'; this.set_request_time(); return $.ajax({ type: 'POST', url: url, data: JSON.stringify(data), dataType: 'json', contentType: 'application/json; charset=utf-8', success: function(response) { ref[func](response); }, error: function(o, status, err) { ref.http_error(o, status, err); }, cache: false, beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); } }); }; // send a http GET request to the API service this.get = function(action, data, func) { var url = this.env.url; if (!func) func = 'response'; this.set_request_time(); data.method = action; return $.ajax({ type: 'GET', url: url, data: data, dataType: 'json', success: function(response) { ref[func](response); }, error: function(o, status, err) { ref.http_error(o, status, err); }, cache: false, beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); } }); }; // send request with auto-selection of POST/GET method this.request = function(action, data, func) { // Use POST for modification actions with probable big request size var method = /(create|delete|move|copy|update|auth)/.test(action) ? 'post' : 'get'; return this[method](action, data, func); }; // handle HTTP request errors this.http_error = function(request, status, err) { var errmsg = request.statusText; this.set_busy(false); request.abort(); if (request.status && errmsg) this.display_message(this.t('servererror') + ' (' + errmsg + ')', 'error'); }; this.response = function(response) { this.update_request_time(); this.set_busy(false); return this.response_parse(response); }; this.response_parse = function(response) { if (!response || response.status != 'OK') { // Logout on invalid-session error if (response && response.code == 403) this.logout(response); else this.display_message(response && response.reason ? response.reason : this.t('servererror'), 'error'); return false; } return true; }; /*********************************************************/ /********* Utilities *********/ /*********************************************************/ // Called on "session expired" session this.logout = function(response) {}; // set state this.set_busy = function(a, message) {}; // displays error message this.display_message = function(label) {}; // called when a request timed out this.request_timed_out = function() {}; // called on start of the request this.set_request_time = function() {}; // called on request response this.update_request_time = function() {}; /*********************************************************/ /********* Helpers *********/ /*********************************************************/ // compose a valid url with the given parameters this.url = function(action, query) { var k, param = {}, querystring = typeof query === 'string' ? '&' + query : ''; if (typeof action !== 'string') query = action; else if (!query || typeof query !== 'object') query = {}; // overwrite task name if (action) query.method = action; // remove undefined values for (k in query) { if (query[k] !== undefined && query[k] !== null) param[k] = query[k]; } return '?' + $.param(param) + querystring; }; // Folder list parser, converts it into structure - this.folder_list_parse = function(list, num) + this.folder_list_parse = function(list, num, subscribed) { - var i, n, items, items_len, f, tmp, folder, + var i, n, j, items, items_len, f, tmp, folder, + subs_support, subs_prefixes = {}, found, + separator = this.env.directory_separator, len = list ? list.length : 0, folders = {}; if (!num) num = 1; + if (subscribed === undefined) + subscribed = true; + + // prepare subscriptions support detection + if (len && this.env.caps) { + subs_support = !!this.env.caps.SUBSCRIPTIONS; + $.each(this.env.caps.MOUNTPOINTS || [], function(i, v) { + subs_prefixes[i] = !!v.SUBSCRIPTIONS; + }); + } + for (i=0; i= 0) folders[folder].tree[diff] = folders[folder].tree[diff] ? folders[folder].tree[diff] + 2 : 2; } tree.push(i); } for (i in folders) { if (tree = folders[i].tree) { var html = '', divs = []; for (n=0; n 2) divs.push({'class': 'l3', width: 15}); else if (tree[n] > 1) divs.push({'class': 'l2', width: 15}); else if (tree[n] > 0) divs.push({'class': 'l1', width: 15}); // separator else if (divs.length && !divs[divs.length-1]['class']) divs[divs.length-1].width += 15; else divs.push({'class': null, width: 15}); } for (n=divs.length-1; n>=0; n--) { if (divs[n]['class']) html += ''; else html += ''; } if (html) $('#' + folders[i].id + ' span.branch').html(html); } } }; // convert content-type string into class name this.file_type_class = function(type) { if (!type) return ''; type = type.replace(/[^a-z0-9]/g, '_'); return type; }; // convert bytes into number with size unit this.file_size = function(size) { if (size >= 1073741824) return parseFloat(size/1073741824).toFixed(2) + ' GB'; if (size >= 1048576) return parseFloat(size/1048576).toFixed(2) + ' MB'; if (size >= 1024) return parseInt(size/1024) + ' kB'; return parseInt(size || 0) + ' B'; }; // Extract file name from full path this.file_name = function(path) { var path = path.split(this.env.directory_separator); return path.pop(); }; // Extract file path from full path this.file_path = function(path) { var path = path.split(this.env.directory_separator); path.pop(); return path.join(this.env.directory_separator); }; // compare two sortable objects this.sort_compare = function(data1, data2) { var key = this.env.sort_col || 'name'; if (key == 'mtime') key = 'modified'; data1 = data1[key]; data2 = data2[key]; if (key == 'size' || key == 'modified') // numeric comparison return this.env.sort_reverse ? data2 - data1 : data1 - data2; else { // use Array.sort() for string comparison var arr = [data1, data2]; arr.sort(function (a, b) { // @TODO: use localeCompare() arguments for better results return a.localeCompare(b); }); if (this.env.sort_reverse) arr.reverse(); return arr[0] === data2 ? 1 : -1; } }; // Checks if specified mimetype is supported natively by the browser (return 1) // or can be displayed in the browser using File API viewer (return 2) this.file_type_supported = function(type) { var i, t, regexps = [], img = 'jpg|jpeg|gif|bmp|png', caps = this.env.browser_capabilities || {}; if (caps.tif) img += '|tiff'; if ((new RegExp('^image/(' + img + ')$', 'i')).test(type)) return 1; // prefer text viewer for any text type if (/^text\/(?!(pdf|x-pdf))/i.test(type)) return 2; if (caps.pdf) { regexps.push(/^application\/(pdf|x-pdf|acrobat|vnd.pdf)/i); regexps.push(/^text\/(pdf|x-pdf)/i); } if (caps.flash) regexps.push(/^application\/x-shockwave-flash/i); for (i in regexps) if (regexps[i].test(type)) return 1; for (i in navigator.mimeTypes) { t = navigator.mimeTypes[i].type; if (t == type && navigator.mimeTypes[i].enabledPlugin) return 1; } // types with viewer support if ($.inArray(type, this.env.supported_mimetypes) > -1) return 2; }; // Return browser capabilities this.browser_capabilities = function() { var i, caps = [], ctypes = ['pdf', 'flash', 'tif']; for (i in ctypes) if (this.env.browser_capabilities[ctypes[i]]) caps.push(ctypes[i]); return caps; }; // Checks browser capabilities eg. PDF support, TIF support this.browser_capabilities_check = function() { if (!this.env.browser_capabilities) this.env.browser_capabilities = {}; if (this.env.browser_capabilities.pdf === undefined) this.env.browser_capabilities.pdf = this.pdf_support_check(); if (this.env.browser_capabilities.flash === undefined) this.env.browser_capabilities.flash = this.flash_support_check(); if (this.env.browser_capabilities.tif === undefined) this.tif_support_check(); }; this.tif_support_check = function() { var img = new Image(), ref = this; img.onload = function() { ref.env.browser_capabilities.tif = 1; }; img.onerror = function() { ref.env.browser_capabilities.tif = 0; }; img.src = this.env.resources_dir + '/blank.tif'; }; this.pdf_support_check = function() { var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {}, plugins = navigator.plugins, len = plugins.length, regex = /Adobe Reader|PDF|Acrobat/i, ref = this; if (plugin && plugin.enabledPlugin) return 1; if (window.ActiveXObject) { try { if (axObj = new ActiveXObject("AcroPDF.PDF")) return 1; } catch (e) {} try { if (axObj = new ActiveXObject("PDF.PdfCtrl")) return 1; } catch (e) {} } for (i=0; i= 60*60*24) return '-'; return (new Date(1970, 1, 1, 0, 0, s, 0)).toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1'); }; }; // Add escape() method to RegExp object // http://dev.rubyonrails.org/changeset/7271 RegExp.escape = function(str) { return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }; // define String's startsWith() method for old browsers if (!String.prototype.startsWith) { String.prototype.startsWith = function(search, position) { position = position || 0; return this.slice(position, search.length) === search; }; }; // make a string URL safe (and compatible with PHP's rawurlencode()) function urlencode(str) { if (window.encodeURIComponent) return encodeURIComponent(str).replace('*', '%2A'); return escape(str) .replace('+', '%2B') .replace('*', '%2A') .replace('/', '%2F') .replace('@', '%40'); }; function escapeHTML(str) { return str === undefined ? '' : String(str) .replace(/&/g, '&') .replace(/>/g, '>') .replace(/