diff --git a/lib/api/autocomplete.php b/lib/api/autocomplete.php new file mode 100644 index 0000000..81072ae --- /dev/null +++ b/lib/api/autocomplete.php @@ -0,0 +1,57 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +class file_api_autocomplete extends file_api_common +{ + /** + * Request handler + */ + public function handle() + { + parent::handle(); + + if (!isset($this->args['search']) || $this->args['search'] === '') { + throw new Exception("Missing search keyword", file_api_core::ERROR_CODE); + } + + if (isset($this->args['folder']) && $this->args['folder'] !== '') { + list($driver, $path) = $this->api->get_driver($this->args['folder']); + } + else { + $driver = $this->api->get_backend(); + } + + if (!empty($this->args['mode'])) { + $mode = 0; + $mode += stripos($this->args['mode'], 'user') !== false ? file_storage::SEARCH_USER : 0; + $mode += stripos($this->args['mode'], 'group') !== false ? file_storage::SEARCH_GROUP : 0; + } + + if (empty($mode)) { + $mode = file_storage::SEARCH_USER; + } + + return $driver->autocomplete($this->args['search'], $mode); + } +} diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php index cf8113e..4f3eed8 100644 --- a/lib/drivers/kolab/kolab_file_storage.php +++ b/lib/drivers/kolab/kolab_file_storage.php @@ -1,1589 +1,1604 @@ | +--------------------------------------------------------------------------+ | 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; /** * @var array */ protected $icache = array(); /** * 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', 'kolab_folders')); $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'], $err))) { return true; } if ($err) { $err_str = $this->rc->get_storage()->get_error_str(); } kolab_auth::log_login_error($auth['user'], $err_str ?: $err); $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, &$error = null) { 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)) { $error = $storage->get_error_code(); 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 // Roundcube already does that (T1050) //$storage->clear_cache('mailboxes', true); return true; } protected function init($user = null) { $this->rc->plugins->exec_hook('startup'); 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, file_storage::CAPS_ACL => 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, true); 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'] && empty($params['head'])) { $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, true); 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_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']), '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 $files = $this->get_files($folder_name, $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_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']), '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' => $new_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 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 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 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', 'extended', 'permissions') * * @return array List of folders * @throws Exception */ public function folder_list($params = array()) { $unsubscribed = $params['type'] & file_storage::FILTER_UNSUBSCRIBED; $rights = ($params['type'] & file_storage::FILTER_WRITABLE) ? 'w' : null; $imap = $this->rc->get_storage(); $folders = $imap->list_folders_subscribed('', '*', 'file', $rights); 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) && !$unsubscribed) { $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 = $imap->list_folders('', '*', 'file', $rights); $folders = array_diff($folders, $subscribed); } // convert folder names to UTF-8 $callback = function($folder) { if (strpos($folder, '&') !== false) { return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET); } return $folder; }; $folders = array_map($callback, $folders); } // searching if (isset($params['search'])) { $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); // In extended format we return array of arrays if ($params['extended']) { if (!$rights && $params['permissions']) { // get list of known writable folders from cache $cache_key = 'mailboxes.permissions'; $permissions = (array) $imap->get_cache($cache_key); } foreach ($folders as $idx => $folder_name) { $folder = array('folder' => $folder_name); // check if folder is readonly if (isset($permissions)) { if (!array_key_exists($folder_name, $permissions)) { $acl = $this->folder_rights($folder_name); $permissions[$folder_name] = $acl; } if (!($permissions[$folder_name] & file_storage::ACL_WRITE)) { $folder['readonly'] = true; } } $folders[$idx] = $folder; } if ($cache_key) { $imap->update_cache($cache_key, $permissions); } } return $folders; } /** * Check folder rights. * * @param string $folder Folder name * * @return int Folder rights (sum of file_storage::ACL_*) */ public function folder_rights($folder) { $storage = $this->rc->get_storage(); $folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP'); $rights = file_storage::ACL_READ; // get list of known writable folders from cache $cache_key = 'mailboxes.permissions'; $permissions = (array) $storage->get_cache($cache_key); if (array_key_exists($folder, $permissions)) { return $permissions[$folder]; } // For better performance, assume personal folders are writeable if ($storage->folder_namespace($folder) == 'personal') { $rights |= file_storage::ACL_WRITE; } else { $myrights = $storage->my_rights($folder); if (in_array('t', (array) $myrights)) { $rights |= file_storage::ACL_WRITE; } $permissions[$folder] = $rights; $storage->update_cache($cache_key, $permissions); } return $rights; } /** * 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 $path File/folder path * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($path, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); // 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->uri2path($lock['uri']); } return $list; } /** * Locks a URI * * @param string $path File/folder path * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($path, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); 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 File/folder path * @param array $lock Lock data * * @throws Exception */ public function unlock($path, $lock) { $this->init_lock_db(); // convert path to global resource string $uri = $this->path2uri($path); 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; } /** * Sharing interface * * @param string $folder_name Name of a folder with full path * @param int $mode Sharing action mode * @param array $args POST/GET parameters * * @return mixed Sharing response * @throws Exception */ public function sharing($folder, $mode, $args = array()) { $folder_name = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP'); $storage = $this->rc->get_storage(); $folder_info = $storage->folder_info($folder_name); if (!is_array($folder_info['rights'])) { throw new Exception("Storage error. Failed to get folder permissions.", file_storage::ERROR); } if (!in_array('a', $folder_info['rights'])) { throw new Exception("No permissions to administer this folder.", file_storage::ERROR_FORBIDDEN); } if ($mode == file_storage::SHARING_MODE_FORM) { $form = array( 'shares' => array( 'title' => 'share.permissions', 'form' => array( 'user' => array( 'title' => 'share.usergroup', 'type' => 'input', ), 'right' => array( 'title' => 'share.permission', 'type' => 'select', 'options' => array( 'r' => 'share.readonly', 'rw' => 'share.readwrite', 'a' => 'share.admin', ), ), ), 'extra_fields' => array( 'type' => 'user', 'id' => '', ), ), ); return $form; } if ($mode == file_storage::SHARING_MODE_RIGHTS) { $result = array(); $acl_list = $storage->get_acl($folder_name); foreach ((array) $acl_list as $name => $acl) { if ($name == $_SESSION['username']) { continue; } if (in_array('a', $acl)) { $right = 'a'; } else if (in_array('i', $acl)) { $right = 'rw'; } else if (in_array('r', $acl)) { $right = 'r'; } else { continue; } $type = strpos($name, 'group:') === 0 ? 'group' : 'user'; $id = $name; if ($type == 'group') { $name = substr($name, 6); } $result[] = array( 'mode' => 'shares', 'type' => $type, 'right' => $right, 'user' => $name, 'id' => $id, ); } return $result; } if ($mode == file_storage::SHARING_MODE_UPDATE) { if ($args['mode'] == 'shares') { $user = $args['id']; if (!$user) { $user = ($args['type'] == 'group' ? 'group:' : '') . preg_replace('/^group:/', '', $args['user']); } switch ($args['right']) { case 'r': $acl = 'lrs'; break; case 'rw': $acl = 'lrswite'; break; case 'a': $acl = 'lrswiteax'; break; } if (empty($user) || (empty($acl) && $args['action'] != 'delete')) { throw new Exception("Invalid input.", file_storage::ERROR); } switch ($args['action']) { case 'submit': case 'update': $result = $storage->set_acl($folder_name, $user, $acl); break; case 'delete': $result = $storage->delete_acl($folder_name, $user); break; } } else { throw new Exception("Invalid input.", file_storage::ERROR); } if (empty($result)) { throw new Exception("Storage error. Failed to update share.", file_storage::ERROR); } return true; } } + /** + * User/group search (autocompletion) + * + * @param string $search Search string + * @param int $mode Search mode + * + * @return array Users/Groups list + * @throws Exception + */ + public function autocomplete($search, $mode) + { + // TODO + throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED); + } + /** * Convert file/folder path into a global URI. * * @param string $path File/folder path * * @return string URI * @throws Exception */ public function path2uri($path) { $storage = $this->rc->get_storage(); $namespace = $storage->get_namespace(); $separator = $storage->get_hierarchy_delimiter(); $_path = str_replace(file_storage::SEPARATOR, $separator, $path); $owner = $this->rc->get_user_name(); // find the owner and remove namespace prefix foreach (array_filter($namespace) as $type => $ns) { foreach ($ns as $root) { if (is_array($root) && $root[0] && strpos($_path, $root[0]) === 0) { $path = substr($path, 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, $path) = explode(file_storage::SEPARATOR, $path, 2); if (strpos($user, '@') === false) { $domain = strstr($owner, '@'); if (!empty($domain)) { $user .= $domain; } } $owner = $user; break; } break 2; } } } return 'imap://' . rawurlencode($owner) . '@' . $storage->options['host'] . '/' . file_utils::encode_path($path); } /** * Convert global URI into file/folder path. * * @param string $uri URI * * @return string File/folder path * @throws Exception */ public function uri2path($uri) { if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $uri, $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 = rawurldecode($matches[1]); $path = file_utils::decode_path($matches[3]); // 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)) { $path = $matches[1] . $path; } // 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 $path = $namespace[0][0] . $local . file_storage::SEPARATOR . $path; } return $path; } /** * Get files from a folder (with performance fix) */ protected function get_files($folder, $filter, $all = true) { if (!($folder instanceof kolab_storage_folder)) { $folder = $this->get_folder_object($folder); } // for better performance it's good to assume max. number of records $folder->set_order_and_limit(null, $all ? 0 : 1); return $folder->select($filter); } /** * Get file object. * * @param string $file_name Name of a file (with folder path) * @param kolab_storage_folder $folder Reference to folder object * @param bool $cache Use internal cache * * @return array File data * @throws Exception */ protected function get_file_object(&$file_name, &$folder = null, $cache = false) { $original_name = $file_name; // 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); } $folder = $this->get_folder_object($folder_name); if ($cache && !empty($this->icache[$original_name])) { return $this->icache[$original_name]; } $filter = array( array('type', '=', 'file'), array('filename', '=', $file_name) ); $files = $this->get_files($folder, $filter, false); $file = $files[0]; if ($cache) { $this->icache[$original_name] = $file; } return $file; } /** * 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; } /** * 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_api.php b/lib/drivers/seafile/seafile_api.php index 5c10bd4..2bfb66c 100644 --- a/lib/drivers/seafile/seafile_api.php +++ b/lib/drivers/seafile/seafile_api.php @@ -1,1050 +1,1106 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Class implementing access via SeaFile Web API v2 */ class seafile_api { const STATUS_OK = 200; const CREATED = 201; const ACCEPTED = 202; const MOVED_PERMANENTLY = 301; const BAD_REQUEST = 400; const FORBIDDEN = 403; const NOT_FOUND = 404; const CONFLICT = 409; const TOO_MANY_REQUESTS = 429; const REPO_PASSWD_REQUIRED = 440; const REPO_PASSWD_MAGIC_REQUIRED = 441; const INTERNAL_SERVER_ERROR = 500; const OPERATION_FAILED = 520; const CONNECTION_ERROR = 550; /** * Specifies how long max. we'll wait and renew throttled request (in seconds) */ const WAIT_LIMIT = 30; /** * Configuration * * @var array */ protected $config = array(); /** * HTTP request handle * * @var HTTP_Request */ protected $request; /** * Web API URI prefix * * @var string */ protected $url; /** * Session token * * @var string */ protected $token; /** * API URL prefix (schema and host[:port]) * * @var string */ protected $url_prefix; public function __construct($config = array()) { $this->config = $config; // set Web API URI $this->url = rtrim(trim($config['host']), '/') ?: 'localhost'; if (!preg_match('|^https?://|i', $this->url)) { $this->url = 'https://' . $this->url; } if (!preg_match('|/api2$|', $this->url)) { $this->url .= '/api2/'; } $this->url_prefix = preg_replace('|^(https?://[^/]+).*$|i', '\\1', $this->url); } /** * * @param array Configuration for this Request instance, that will be merged * with default configuration * * @return HTTP_Request2 Request object */ public static function http_request($config = array()) { // load HTTP_Request2 require_once 'HTTP/Request2.php'; // remove unknown config, otherwise HTTP_Request will throw an error $config = array_intersect_key($config, array_flip(array( 'connect_timeout', 'timeout', 'use_brackets', 'protocol_version', 'buffer_size', 'store_body', 'follow_redirects', 'max_redirects', 'strict_redirects', 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase' ))); // force CURL adapter, this allows to handle correctly // compressed responses with simple SplObserver registered $config['adapter'] = 'HTTP_Request2_Adapter_Curl'; try { $request = new HTTP_Request2(); $request->setConfig($config); } catch (Exception $e) { rcube::raise_error($e, true, false); return; } return $request; } /** * Send HTTP request * * @param string $method Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT') * @param string $url Request API URL * @param array $get GET parameters * @param array $post POST parameters * @param array $upload Uploaded files data * * @return string|array Server response */ protected function request($method, $url, $get = null, $post = null, $upload = null) { if (!preg_match('/^https?:\/\//', $url)) { $url = $this->url . $url; // Note: It didn't work for me without the last backslash $url = rtrim($url, '/') . '/'; } else { $url = $this->mod_url($url); } if (!$this->request) { $this->config['store_body'] = true; // some methods respond with 301 redirect, we'll not follow them // also because of https://github.com/haiwen/seahub/issues/288 $this->config['follow_redirects'] = false; $this->request = self::http_request($this->config); if (!$this->request) { $this->status = self::CONNECTION_ERROR; return; } } // cleanup try { $this->request->setBody(''); $this->request->setUrl($url); } catch (Exception $e) { rcube::raise_error($e, true, false); $this->status = self::CONNECTION_ERROR; return; } if ($this->config['debug']) { $log_line = "SeaFile $method: $url"; $json_opt = PHP_VERSION_ID >= 50400 ? JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE : 0; if (!empty($get)) { $log_line .= ", GET: " . @json_encode($get, $json_opt); } if (!empty($post)) { $log_line .= ", POST: " . preg_replace('/("password":)[^\},]+/', '\\1"*"', @json_encode($post, $json_opt)); } if (!empty($upload)) { $log_line .= ", Files: " . @json_encode(array_keys($upload), $json_opt); } rcube::write_log('console', $log_line); } $this->request->setMethod($method ?: HTTP_Request2::METHOD_GET); if (!empty($get)) { $_url = $this->request->getUrl(); $_url->setQueryVariables($get); $this->request->setUrl($_url); } if (!empty($post)) { $this->request->addPostParameter($post); } if (!empty($upload)) { foreach ($upload as $field_name => $file) { $this->request->addUpload($field_name, $file['data'], $file['name'], $file['type']); } } if ($this->token) { $this->request->setHeader('Authorization', "Token " . $this->token); } // some HTTP server configurations require this header $this->request->setHeader('Accept', "application/json,text/javascript,*/*"); // proxy User-Agent string $this->request->setHeader('User-Agent', $_SERVER['HTTP_USER_AGENT']); // send request to the SeaFile API server try { $response = $this->request->send(); $this->status = $response->getStatus(); $body = $response->getBody(); } catch (Exception $e) { rcube::raise_error($e, true, false); $this->status = self::CONNECTION_ERROR; } if ($this->config['debug']) { rcube::write_log('console', "SeaFile Response [$this->status]: " . trim($body)); } // request throttled, try again if ($this->status == self::TOO_MANY_REQUESTS) { if (preg_match('/([0-9]+) second/', $body, $m) && ($seconds = $m[1]) < self::WAIT_LIMIT) { sleep($seconds); return $this->request($method, $url, $get, $post, $upload); } } // decode response return $this->status >= 400 ? false : @json_decode($body, true); } /** * Return error code of last operation */ public function is_error() { return $this->status >= 400 ? $this->status : false; } /** * Authenticate to SeaFile API and get auth token * * @param string $username User name (email) * @param string $password User password * * @return string Authentication token */ public function authenticate($username, $password) { // sanity checks if ($username === '' || !is_string($username) || $password === '' || !is_string($password)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', 'auth-token', null, array( 'username' => $username, 'password' => $password, )); if ($result['token']) { return $this->token = $result['token']; } } /** * Get account information * * @return array Account info (usage, total, email) */ public function account_info() { return $this->request('GET', "account/info"); } /** * Delete a directory * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool True on success, False on failure */ public function directory_delete($repo_id, $dir) { // sanity checks if ($dir === '' || $dir === '/' || !is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id/dir", array('p' => $dir)); return $this->is_error() === false; } /** * Rename a directory * * @param string $repo_id Library identifier * @param string $src_dir Directory name (with path) * @param string $dest_dir New directory name (with path) * * @return bool True on success, False on failure */ public function directory_rename($repo_id, $src_dir, $dest_dir) { // sanity checks if ($src_dir === '' || $src_dir === '/' || !is_string($src_dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dest_dir === '' || $dest_dir === '/' || !is_string($dest_dir) || $dest_dir === $src_dir) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/dir", array('p' => $src_dir), array( 'operation' => 'rename', 'newname' => $dest_dir, )); return $this->is_error() === false; } /** * Rename a directory * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool True on success, False on failure */ public function directory_create($repo_id, $dir) { // sanity checks if ($dir === '' || $dir === '/' || !is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/dir", array('p' => $dir), array( 'operation' => 'mkdir', )); return $this->is_error() === false; } /** * List directory entries (files and directories) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $type Entry type ('dir' or 'file') (requires Seafile 4.4.1) * @param bool $recursive Enable recursive call for 'dir' listing (requires Seafile 4.4.1) * * @return bool|array List of directories/files on success, False on failure */ public function directory_entries($repo_id, $dir, $type = null, $recursive = false) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // args: p=<$name> ('/' is a root, default), oid=? // sample result // [{ // "id": "0000000000000000000000000000000000000000", // "type": "file", // "name": "test1.c", // "size": 0 // },{ // "id": "e4fe14c8cda2206bb9606907cf4fca6b30221cf9", // "type": "dir", // "name": "test_dir" // }] $params = array('p' => $dir); if ($type) { $params['t'] = $type == 'dir' ? 'd' : 'f'; } if ($recursive && $type == 'dir') { $params['recursive'] = 1; } return $this->request('GET', "repos/$repo_id/dir", $params); } /** * Update a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param array $file File data (data, type, name) * * @return bool True on success, False on failure */ public function file_update($repo_id, $filename, $file) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } // first get the update link $result = $this->request('GET', "repos/$repo_id/update-link"); if ($this->is_error() || empty($result)) { return false; } $path = explode('/', $filename); $fn = array_pop($path); // then update file $result = $this->request('POST', $result, null, array( 'filename' => $fn, 'target_file' => $filename, ), array('file' => $file) ); return $this->is_error() === false; } /** * Upload a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param array $file File data (data, type, name) * * @return bool True on success, False on failure */ public function file_upload($repo_id, $filename, $file) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } // first get upload link $result = $this->request('GET', "repos/$repo_id/upload-link"); if ($this->is_error() || empty($result)) { return false; } $path = explode('/', $filename); $filename = array_pop($path); $dir = '/' . ltrim(implode('/', $path), '/'); $file['name'] = $filename; // then update file $result = $this->request('POST', $result, null, array( 'parent_dir' => $dir ), array('file' => $file) ); return $this->is_error() === false; } /** * Delete a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool True on success, False on failure */ public function file_delete($repo_id, $filename) { // sanity check if ($filename === '' || $filename === '/' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id/file", array('p' => $filename)); return $this->is_error() === false; } /** * Copy file(s) (no rename here) * * @param string $repo_id Library identifier * @param string|array $files List of files (without path) * @param string $src_dir Source directory * @param string $dest_dir Destination directory * @param string $dest_repo Destination library (optional) * * @return bool True on success, False on failure */ public function file_copy($repo_id, $files, $src_dir, $dest_dir, $dest_repo) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($src_dir === '' || !is_string($src_dir)) { $this->status = self::BAD_REQUEST; return false; } if ($dest_dir === '' || !is_string($dest_dir)) { $this->status = self::BAD_REQUEST; return false; } if ((!is_array($files) && !strlen($files)) || (is_array($files) && empty($files))) { $this->status = self::BAD_REQUEST; return false; } if (empty($dest_repo)) { $dest_repo = $repo_id; } $result = $this->request('POST', "repos/$repo_id/fileops/copy", array('p' => $src_dir), array( 'file_names' => implode(':', (array) $files), 'dst_dir' => $dest_dir, 'dst_repo' => $dest_repo, )); return $this->is_error() === false; } /** * Move a file (no rename here) * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param string $dst_dir Destination directory * @param string $dst_repo Destination library (optional) * * @return bool True on success, False on failure */ public function file_move($repo_id, $filename, $dst_dir, $dst_repo = null) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } if ($dst_dir === '' || !is_string($dst_dir)) { $this->status = self::BAD_REQUEST; return false; } if (empty($dst_repo)) { $dst_repo = $repo_id; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'move', 'dst_dir' => $dst_dir, 'dst_repo' => $dst_repo, )); return $this->is_error() === false; } /** * Rename a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param string $new_name New file name (without path) * * @return bool True on success, False on failure */ public function file_rename($repo_id, $filename, $new_name) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } if ($new_name === '' || !is_string($new_name)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'rename', 'newname' => $new_name, )); return $this->is_error() === false; } /** * Create an empty file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool True on success, False on failure */ public function file_create($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'create', )); return $this->is_error() === false; } /** * Get file info * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool|array File info on success, False on failure */ public function file_info($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } // sample result: // "id": "013d3d38fed38b3e8e26b21bb3463eab6831194f", // "mtime": 1398148877, // "type": "file", // "name": "foo.py", // "size": 22 return $this->request('GET', "repos/$repo_id/file/detail", array('p' => $filename)); } /** * Get file content * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool|string File download URI on success, False on failure */ public function file_get($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('GET', "repos/$repo_id/file", array('p' => $filename)); } /** * List libraries (repositories) * * @return array|bool List of libraries on success, False on failure */ public function library_list() { $result = $this->request('GET', "repos"); // sample result // [{ // "permission": "rw", // "encrypted": false, // "mtime": 1400054900, // "owner": "user@mail.com", // "id": "f158d1dd-cc19-412c-b143-2ac83f352290", // "size": 0, // "name": "foo", // "type": "repo", // "virtual": false, // "desc": "new library", // "root": "0000000000000000000000000000000000000000" // }] return $result; } /** * Get library info * * @param string $repo_id Library identifier * * @return array|bool Library info on success, False on failure */ public function library_info($repo_id) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('GET', "repos/$repo_id"); } /** * Create library * * @param string $name Library name * @param string $description Library description * * @return bool|array Library info on success, False on failure */ public function library_create($name, $description = '') { if ($name === '' || !is_string($name)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('POST', "repos", null, array( 'name' => $name, 'desc' => $description, )); } /** * Rename library * * @param string $repo_id Library identifier * @param string $new_name Library description * * @return bool True on success, False on failure */ public function library_rename($repo_id, $name, $description = '') { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($name === '' || !is_string($name)) { $this->status = self::BAD_REQUEST; return false; } // Note: probably by mistake the 'op' is a GET parameter // maybe changed in future to be consistent with other methods $this->request('POST', "repos/$repo_id", array('op' => 'rename'), array( 'repo_name' => $name, 'repo_desc' => $description, )); return $this->is_error() === false; } /** * Delete library * * @param string $repo_id Library identifier * * @return bool True on success, False on failure */ public function library_delete($repo_id) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id"); return $this->is_error() === false; } /** * Ping the API server * * @param string $token If set, auth token will be used * * @param bool True on success, False on failure */ public function ping($token = null) { // can be used to check if token is still valid if ($token) { $this->token = $token; $result = $this->request('GET', 'auth/ping', null, null); } // or if api works else { $result = $this->request('GET', 'ping', null, null); } return $this->is_error() === false; } /** * Share a directory (or library) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $right Permission ('r' or 'rw' or 'admin') * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * @param bool $update Update an existing entry * * @return bool True on success, False on failure */ public function shared_item_add($repo_id, $dir, $right, $mode, $who, $update = false) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($mode != 'user' && $mode != 'group') { $this->status = self::BAD_REQUEST; return false; } if ($right != 'r' && $right != 'rw' && $right != 'admin') { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $post = array( 'permission' => $right, 'share_type' => $mode, ); $post[$mode == 'group' ? 'group_id' : 'username'] = $who; $this->request($update ? 'POST' : 'PUT', "repos/$repo_id/dir/shared_items", array('p' => $dir), $post); return $this->is_error() === false; } /** * Update shared item permissions * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $right Permission ('r' or 'rw' or 'admin') * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * * @return bool True on success, False on failure */ public function shared_item_update($repo_id, $dir, $right, $mode, $who) { return $this->shared_item_add($repo_id, $dir, $right, $mode, $who, true); } /** * Un-Share a directory (or library) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * * @return bool True on success, False on failure */ public function shared_item_delete($repo_id, $dir, $mode, $who) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($mode != 'user' && $mode != 'group') { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $get = array( 'share_type' => $mode, 'p' => $dir ); $get[$mode == 'group' ? 'group_id' : 'username'] = $who; $this->request('DELETE', "repos/$repo_id/dir/shared_items", $get); return $this->is_error() === false; } /** * List directory permissions (shares) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool|array List of user/group info on success, False on failure */ public function shared_item_list($repo_id, $dir) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // Example result: // [ // { // "group_info": { "id": 17, "name": "Group Name" }, // "is_admin": false, // "share_type": "group", // "permission": "rw" // }, // { // "user_info": { "nickname": "user", "name": "user@domain.com" }, // "share_type": "user", // "permission": "r" // } // ] return $this->request('GET', "repos/$repo_id/dir/shared_items", array('p' => $dir)); } + /** + * List user groups + * + * @return bool|array List of groups on success, False on failure + */ + public function group_list() + { + // Sample result: + // [ + // { + // "ctime": 1398134171327948, + // "creator": "user@example.com", + // "msgnum": 0, + // "mtime": 1398231100, + // "id": 1, + // "name": "lian" + // } + // ] }, + + $result = $this->request('GET', "groups"); + + if (is_array($result)) { + $result = (array) $result['groups']; + } + + return $result; + } + + /** + * List users + * + * @param string $search Search keyword + * + * @return bool|array List of users on success, False on failure + */ + public function user_search($search = null) + { + // Sample response: + // [ + // { + // 'avatar_url': 'https://cloud.seafile.com/media/avatars/default.png', + // 'contact_email': 'foo@foo.com', + // 'email': 'foo@foo.com', + // 'name': 'foo' + // } + // ] + + $result = $this->request('GET', "search-user", array('q' => $search)); + + if (is_array($result)) { + $result = (array) $result['users']; + } + + return $result; + } + /** * Parse and fix API request URLs */ public function mod_url($url) { // If Seafile is behind a proxy and different port, it will return // wrong URL for file uploads/downloads. We force the original URL prefix here if (stripos($url, $this->url_prefix) !== 0) { $url = $this->url_prefix . preg_replace('|^(https?://[^/]+)|i', '', $url); } return $url; } } diff --git a/lib/drivers/seafile/seafile_file_storage.php b/lib/drivers/seafile/seafile_file_storage.php index fe7bc2c..96ea38c 100644 --- a/lib/drivers/seafile/seafile_file_storage.php +++ b/lib/drivers/seafile/seafile_file_storage.php @@ -1,1466 +1,1530 @@ | +--------------------------------------------------------------------------+ | 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, file_storage::CAPS_ACL => 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, force-type, head) * @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'] && empty($params['head'])) { $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'] && empty($params['head'])) { $allow_redirects = $this->rc->config->get('fileapi_seafile_allow_redirects'); // 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 ($allow_redirects && !empty($params['force-download'])) { header("Location: $link"); } else if ($fp = fopen('php://output', 'wb')) { $this->save_file_content($link, $fp); fclose($fp); } } } /** * 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_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']), '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, 'file'); $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_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']), '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($params = array()) { $writable = ($params['type'] & file_storage::FILTER_WRITABLE) ? true : false; $libraries = $this->libraries(); $folders = array(); if ($this->config['cache']) { $repos = array(); $repo_ids = array(); $cache = $this->rc->get_cache('seafile_' . $this->title, $this->config['cache'], $this->config['cache_ttl'], true); if ($cache) { $repos = (array) $cache->get('repos'); } // Mark unmodified repos foreach ($libraries as $idx => $library) { if ($mtime = $repos[$library['id']]) { if ($mtime == $library['mtime']) { $libraries[$idx]['use-cache'] = true; } else { $cache_update = true; } } else { $cache_update = true; } $repos[$library['id']] = $library['mtime']; $repo_ids[] = $library['id']; } } foreach ($libraries as $library) { if ($library['virtual'] || $library['encrypted']) { continue; } if (strpos($library['permission'], 'w') === false) { $readonly_prefixes[] = $library['name']; } $folders[$library['name']] = array( 'mtime' => $library['mtime'], 'permission' => $library['permission'], ); foreach ($this->folders_tree($library) as $folder_name => $folder) { $folders[$library['name'] . '/' . $folder_name] = $folder; } } if (empty($folders)) { throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR); } if ($cache && $cache_update) { // Cleanup repos data $repos = array_intersect_key($repos, array_flip($repo_ids)); $cache->set('repos', $repos); } // remove read-only folders when requested if ($writable) { foreach ($folders as $folder_name => $folder) { if (strpos($folder['permission'], 'w') === false) { unset($folders[$folder_name]); } } } // In extended format we return array of arrays if (!empty($params['extended'])) { foreach ($folders as $folder_name => $folder) { $item = array('folder' => $folder_name); // check if folder is readonly if (!$writable && $params['permissions']) { if (strpos($folder['permission'], 'w') === false) { $item['readonly'] = true; } } $folders[$folder_name] = $item; } } else { $folders = array_keys($folders); } // sort folders usort($folders, array('file_utils', 'sort_folder_comparator')); return $folders; } /** * Check folder rights. * * @param string $folder_name Name of a folder with full path * * @return int Folder rights (sum of file_storage::ACL_*) */ public function folder_rights($folder_name) { // It is not possible (yet) to assign a specified library/folder // to the mount point. So, it is a "virtual" folder. if (!strlen($folder_name)) { return 0; } list($folder, $repo_id, $library) = $this->find_library($folder_name); // @TODO: we should check directory permission not library // However, there's no API for this, we'd need to get a list // of directories of a parent folder/library /* if (strpos($folder, '/')) { // @TODO } else { $acl = $library['permission']; } */ $acl = $library['permission']; $rights = 0; $map = array( 'r' => file_storage::ACL_READ, 'w' => file_storage::ACL_WRITE, ); foreach ($map as $key => $value) { if (strpos($acl, $key) !== false) { $rights |= $value; } } return $rights; } /** * 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 $path File/folder path * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($path, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); // 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->uri2path($lock['uri']); } return $list; } /** * Locks a URI * * @param string $path File/folder path * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($path, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->uri2resource($path); 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 File/folder path * @param array $lock Lock data * * @throws Exception */ public function unlock($path, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); 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; } /** * Sharing interface * * @param string $folder_name Name of a folder with full path * @param int $mode Sharing action mode * @param array $args POST/GET parameters * * @return mixed Sharing response * @throws Exception */ public function sharing($folder, $mode, $args = array()) { if ($mode == file_storage::SHARING_MODE_FORM) { $form = array( // 'upload-links' // 'download-links' 'shares' => array( 'title' => 'share.permissions', 'form' => array( 'user' => array( 'title' => 'share.usergroup', 'type' => 'input', + 'autocomplete' => 'user,group', ), 'right' => array( 'title' => 'share.permission', 'type' => 'select', 'options' => array( 'r' => 'share.readonly', 'rw' => 'share.readwrite', ), ), ), 'extra_fields' => array( 'type' => 'user', 'id' => '', ), ), ); return $form; } if ($mode == file_storage::SHARING_MODE_RIGHTS) { if (!$this->init()) { throw new Exception("Storage error. Unable to get shares of SeaFile folder/lib.", file_storage::ERROR); } list($path, $repo_id) = $this->find_library($folder); $result = array(); $shares = $this->api->shared_item_list($repo_id, $path); foreach ($shares as $share) { if (!empty($share['group_info'])) { $name = $share['group_info']['name']; $name = $share['group_info']['id']; } else { $name = $share['user_info']['name']; $id = $share['user_info']['name']; } $result[] = array( 'mode' => 'shares', 'type' => $share['share_type'], 'right' => $share['permission'], 'user' => $name, 'id' => $id, ); } return $result; } if ($mode == file_storage::SHARING_MODE_UPDATE) { if (!$this->init()) { throw new Exception("Storage error. Unable to update shares of SeaFile folder/lib.", file_storage::ERROR); } list($path, $repo_id) = $this->find_library($folder); if ($args['mode'] == 'shares') { switch ($args['action']) { case 'submit': $result = $this->api->shared_item_add($repo_id, $path, $args['right'], $args['type'], $args['user']); break; case 'update': $result = $this->api->shared_item_update($repo_id, $path, $args['right'], $args['type'], $args['id'] ?: $args['user']); break; case 'delete': $result = $this->api->shared_item_delete($repo_id, $path, $args['type'], $args['user']); break; } } else { throw new Exception("Invalid input.", file_storage::ERROR); } if (empty($result)) { throw new Exception("Storage error. Failed to update share.", file_storage::ERROR); } return true; } } + /** + * User/group search (autocompletion) + * + * @param string $search Search string + * @param int $mode Search mode + * + * @return array Users/Groups list + * @throws Exception + */ + public function autocomplete($search, $mode) + { + if (!$this->init()) { + throw new Exception("Storage error. Failed to init Seafile storage connection.", file_storage::ERROR); + } + + $limit = (int) $this->rc->config->get('autocomplete_max', 15); + $result = array(); + $index = array(); + + if ($mode & file_storage::SEARCH_USER) { + $users = $this->api->user_search($search); + + if (!is_array($users)) { + throw new Exception("Storage error. Failed to search users.", file_storage::ERROR); + } + + foreach ($users as $user) { + $index[] = $user['name']; + $result[] = array( + 'name' => $user['name'], + 'id' => $user['email'], + 'type' => 'user', + ); + } + } + + if (count($result) < $limit && ($mode & file_storage::SEARCH_GROUP)) { + if ($groups = $this->api->group_list()) { + $search = mb_strtoupper($search); + foreach ($groups as $group) { + if (strpos(mb_strtoupper($group['name']), $search) !== false) { + $index[] = $group['name']; + $result[] = array( + 'name' => $group['name'], + 'id' => $group['id'], + 'type' => 'group', + ); + } + } + } + } + + if (count($result)) { + array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $result); + } + + if (count($result) > $limit) { + $result = array_slice($result, 0, $limit); + } + + return $result; + } + /** * Convert file/folder path into a global URI. * * @param string $path File/folder path * * @return string URI * @throws Exception */ public function path2uri($path) { list($file, $repo_id, $library) = $this->find_library($path); // Remove protocol prefix and path, we work with host only $host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']); return 'seafile://' . rawurlencode($library['owner']) . '@' . $host . '/' . file_utils::encode_path($path); } /** * Convert global URI into file/folder path. * * @param string $uri URI * * @return string File/folder path * @throws Exception */ public function uri2path($uri) { if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) { throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR); } $user = rawurldecode($matches[1]); $host = $matches[2]; $path = file_utils::decode_path($matches[3]); $c_host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']); list($file, $repo_id, $library) = $this->find_library($path, true); if (empty($library) || $host != $c_host || $user != $library['owner']) { throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR); } return $path; } /** * Get folders tree in the Seafile library */ protected function folders_tree($library) { if ($this->config['cache']) { $cache = $this->rc->get_cache('seafile_' . $this->title, $this->config['cache'], $this->config['cache_ttl'], true); if ($cache && $library['use-cache']) { $folders = $cache->get('folders.' . $library['id']); } } if (!isset($folders) || !is_array($folders)) { $folders = array(); // get folders in the repo (requires Seafile 4.4.1) if ($content = $this->api->directory_entries($library['id'], '', 'dir', true)) { foreach ($content as $item) { if ($item['type'] == 'dir' && strlen($item['name'])) { $parent = trim($item['parent_dir'], '/'); $name = (strlen($parent) > 0 ? "$parent/" : '') . $item['name']; $folders[$name] = array( 'mtime' => $item['mtime'], 'permission' => $item['permission'], ); } } } if ($cache && is_array($content)) { $cache->set('folders.' . $library['id'], $folders); } } return $folders; } /** * 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($this->api->mod_url($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; } /** * 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 7bf5ce8..e503e3f 100644 --- a/lib/drivers/webdav/webdav_file_storage.php +++ b/lib/drivers/webdav/webdav_file_storage.php @@ -1,1045 +1,1061 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ // try SabreDAV installed as described in README if (file_exists(__DIR__ . '/../../ext/SabreDAV/vendor/autoload.php')) { require __DIR__ . '/../../ext/SabreDAV/vendor/autoload.php'; } // fallback to System-installed package else { require 'Sabre/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($params['head'] ? 'HEAD' : 'GET', $file_name); if ($response['statusCode'] != 200) { throw new Exception("Storage error. File not found.", file_storage::ERROR); } // Sometimes Content-Length is an array here (T3757) $size = $response['headers']['content-length']; if (is_array($size)) { $size = $size[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'] : $response['headers']['content-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 && empty($params['head'])) { 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' => file_utils::date_format($mtime, $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($ctime, $this->config['date_format'], $this->config['timezone']), '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' => file_utils::date_format($mtime, $this->config['date_format'], $this->config['timezone']), 'ctime' => file_utils::date_format($ctime, $this->config['date_format'], $this->config['timezone']), '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($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('file_utils', 'sort_folder_comparator')); // In extended format we return array of arrays if (!empty($params['extended'])) { foreach ($result as $idx => $folder) { $item = array('folder' => $folder); $result[$idx] = $item; } } return $result; } /** * Check folder rights. * * @param string $folder_name Name of a folder with full path * * @return int Folder rights (sum of file_storage::ACL_*) */ public function folder_rights($folder_name) { // @TODO return file_storage::ACL_READ | file_storage::ACL_WRITE; } /** * 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 $path File/folder path * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($path, $child_locks = false) { $this->init_lock_db(); // convert path into global URI $uri = $this->path2uri($path); // get locks list $list = $this->lock_db->lock_list($uri, $child_locks); // convert back global URIs into paths foreach ($list as $idx => $lock) { $list[$idx]['uri'] = $this->uri2path($lock['uri']); } return $list; } /** * Locks a URI * * @param string $path File/folder path * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($path, $lock) { $this->init_lock_db(); // convert path into global URI $uri = $this->path2uri($path); 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 File/folder path * @param array $lock Lock data * * @throws Exception */ public function unlock($path, $lock) { $this->init_lock_db(); // convert path into global URI $uri = $this->path2uri($path); 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, ); } /** * Sharing interface * * @param string $folder_name Name of a folder with full path * @param int $mode Sharing action mode * @param array $args POST/GET parameters * * @return mixed Sharing response * @throws Exception */ public function sharing($folder, $mode, $args = array()) { // TODO + throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED); + } + + /** + * User/group search (autocompletion) + * + * @param string $search Search string + * @param int $mode Search mode + * + * @return array Users/Groups list + * @throws Exception + */ + public function autocomplete($search, $mode) + { + // TODO + throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED); } /** * Convert file/folder path into a global URI. * * @param string $path File/folder path * * @return string URI * @throws Exception */ public function path2uri($path) { $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']); return 'webdav://' . rawurlencode($this->config['username']) . '@' . $base . '/' . file_utils::encode_path($path); } /** * Convert global URI into file/folder path. * * @param string $uri URI * * @return string File/folder path * @throws Exception */ public function uri2path($uri) { if (!preg_match('|^webdav://([^@]+)@(.*)$|', $uri, $matches)) { throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR); } $user = rawurldecode($matches[1]); $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']); $uri = $matches[2]; if ($user != $this->config['username'] || strpos($uri, $base) !== 0) { throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR); } $uri = substr($matches[2], strlen($base) + 1); return file_utils::decode_path($uri); } /** * 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); } /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } } diff --git a/lib/file_storage.php b/lib/file_storage.php index 0ae02a6..e37a391 100644 --- a/lib/file_storage.php +++ b/lib/file_storage.php @@ -1,390 +1,405 @@ | +--------------------------------------------------------------------------+ | 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 = 1; const FILTER_WRITABLE = 2; // folder permissions const ACL_READ = 1; const ACL_WRITE = 2; // sharing interface modes const SHARING_MODE_FORM = 1; const SHARING_MODE_RIGHTS = 2; const SHARING_MODE_UPDATE = 3; + // search modes + const SEARCH_USER = 1; + const SEARCH_GROUP = 2; + /** * 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', 'extended', 'permissions') * * @return array List of folders * @throws Exception */ public function folder_list($params = array()); /** * Check folder rights. * * @param string $folder_name Name of a folder with full path * * @return int Folder rights (sum of file_storage::ACL_*) */ public function folder_rights($folder_name); /** * 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); /** * Sharing interface * * @param string $folder_name Name of a folder with full path * @param int $mode Sharing action mode * @param array $args POST/GET parameters * * @return mixed Sharing response * @throws Exception */ public function sharing($folder, $mode, $args = array()); + /** + * User/group search (autocompletion) + * + * @param string $search Search string + * @param int $mode Search mode + * + * @return array Users/Groups list + * @throws Exception + */ + public function autocomplete($search, $mode); + /** * Convert file/folder path into a global URI. * Note: We're using self::SEPARATOR as a hierarchy delimiter * * @param string $path File/folder path * * @return string URI * @throws Exception */ public function path2uri($path); /** * Convert global URI into file/folder path. * Note: We're using self::SEPARATOR as a hierarchy delimiter * * @param string $uri URI * * @return string File/folder path * @throws Exception */ public function uri2path($uri); }