diff --git a/lib/drivers/kolabfiles/kolabfiles_file_storage.php b/lib/drivers/kolabfiles/kolabfiles_file_storage.php new file mode 100644 index 0000000..53fdc2b --- /dev/null +++ b/lib/drivers/kolabfiles/kolabfiles_file_storage.php @@ -0,0 +1,1808 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +// use Sabre\HTTP\Client; + +class kolabfiles_file_storage implements file_storage +{ + /** + * @var rcube + */ + protected $rc; + + // protected $client = null; + + /** + * @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(); + } + + + /** + * 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->config = [ + 'baseuri' => 'https://kolab.local/api/v4/', + 'username' => 'admin@kolab.local', + 'password' => 'simple123', + ]; + // $this->client = new Client(array( + // 'baseUri' => rtrim($this->config['baseuri'], '/') . '/', + // 'userName' => $this->config['username'], + // 'password' => $this->config['password'], + // 'authType' => Client::AUTH_BASIC, + // )); + + $this->client = new \GuzzleHttp\Client( + [ + 'http_errors' => false, // No exceptions from Guzzle + 'base_uri' => rtrim($this->config['baseuri'], '/') . '/', + //FIXME basic auth is not available + // 'auth' => ['admin@kolab.local', 'simple123'], + // 'verify' => \config('meet.api_verify_tls'), + 'verify' => false, + // 'headers' => [ + // 'X-Auth-Token' => \config('meet.api_token'), + // ], + 'connect_timeout' => 10, + 'timeout' => 10, + // 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { + // $threshold = \config('logging.slow_log'); + // if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { + // $url = $stats->getEffectiveUri(); + // $method = $stats->getRequest()->getMethod(); + // \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); + // } + // }, + ] + ); + + // $this->client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, false); + // $this->client->addCurlSetting(CURLOPT_SSL_VERIFYHOST, false); + } + + protected function client() + { + $this->init(); + return $this->client; + } + + /** + * 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) + { + //FIXME + // $this->init(true); + $this->init(); + + // $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), + // 'token' => $_SESSION[$this->title . 'seafile_token'] ? $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']) : null, + + // ); + + // $this->config = array_merge($config, $this->config); + + // // initialize Web API + // $this->api = new seafile_api($this->config); + + // if ($skip_auth) { + // return true; + // } + + // if ($this->config['cache']) { + // $cache = $this->rc->get_cache('seafile_' . $this->title, + // $this->config['cache'], $this->config['cache_ttl'], true); + // } + + // // try session token + // if ($config['token']) { + // // With caching we know the last successful token use, so we can + // // skip ping call, which is a big win for case of parallel folders listing + // if ($cache) { + // $valid = ($ping = $cache->get('ping')) && $ping + 15 >= time(); + // } + + // if (empty($valid)) { + // $valid = $this->api->ping($config['token']); + + // if ($cache && $valid) { + // $cache->write('ping', time()); + // } + // } + // } + + // 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); + + // if ($cache) { + // $cache->remove('ping'); + // } + // } + // } + + // // 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) { + // throw new Exception("SeaFile storage unavailable", file_storage::ERROR); + // } + // 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()) + { + // $response = $this->actingAs($john)->get("fs?parent={$collection->id}"); + // mount point contains only folders + if (!is_string($folder_name) || $folder_name === '') { + return 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', 'path', 'level') + * + * @return array List of folders + * @throws Exception + */ + public function folder_list($params = array()) + { + // $this->init(); + //FIXME + // $libraries = $this->libraries(); + // $writable = ($params['type'] & file_storage::FILTER_WRITABLE) ? true : false; + // $prefix = (string) $params['path']; + // $prefix_len = strlen($prefix); + $folders = array(); + + // $response = $this->client->request('PUT', $file_name, $data); + // $response = $this->client->request('GET', 'fs'); + + // rcube::console("folder list"); + // FIXME why don't we see this on the commandline + rcube::write_log('kolabfiles', $response); + // $test = null; + // $test(); + + // $log_driver = rcube::get_instance()->config->get('log_driver'); + // throw new Exception($log_driver); + $response = $this->client()->request('GET', "fs"); + throw new Exception(var_export($response->getStatusCode(), true)); + throw new Exception(var_export($response->getBody(), true)); + rcube::write_log('console', $response); + + + // if ($prefix_len) { + // $path = explode('/', $prefix); + // $lib_search = array_shift($path); + // $params['path'] = implode('/', $path); + // } + + // foreach ($libraries as $library) { + // if ($library['virtual'] || $library['encrypted']) { + // continue; + // } + + // if ($prefix_len && $lib_search !== $library['name']) { + // continue; + // } + + // if (!strlen($params['path'])) { + // $folders[$library['name']] = array( + // 'mtime' => $library['mtime'], + // 'permission' => $library['permission'], + // ); + // } + + // if ($params['level'] == 1 && !$prefix_len) { + // // Only list of libraries has been requested + // continue; + // } + + // foreach ($this->folders_tree($library, $params) as $folder_name => $folder) { + // $folders[$library['name'] . '/' . $folder_name] = $folder; + // } + // } + + // if (empty($libraries)) { + // throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR); + // } + + // // 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->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 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) + { + //FIXME + // 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( + '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' => '', + ), + ), + 'download-link' => array( + 'title' => 'share.download-link', + 'label' => 'share.generate', + 'single' => true, + 'list_column' => 'link', + 'list_column_label' => 'share.link', + 'form' => array( + 'password' => array( + 'title' => 'share.password', + 'type' => 'password', + ), + 'expire' => array( + 'title' => 'share.expire', + 'placeholder' => 'share.expiredays', + 'type' => 'input', + ), + ), + 'extra_fields' => array( + 'id' => '', + ), + ), + 'upload-link' => array( + 'title' => 'share.upload-link', + 'label' => 'share.generate', + 'single' => true, + 'list_column' => 'link', + 'list_column_label' => 'share.link', + 'form' => array( + 'password' => array( + 'title' => 'share.password', + 'type' => 'password', + ), + ), + 'extra_fields' => array( + '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(); + + if ($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 = $this->user_label($share['user_info']['nickname'], $share['user_info']['name']); + $id = $share['user_info']['name']; + } + + $result[] = array( + 'mode' => 'shares', + 'type' => $share['share_type'], + 'right' => $share['permission'], + 'user' => $name, + 'id' => $id, + ); + } + } + + if ($links = $this->api->share_link_list($repo_id, $path)) { + foreach ($links as $link) { + $result[] = array( + 'mode' => 'download-link', + 'id' => $link['token'], + 'link' => $link['link'], + ); + } + } + + if ($links = $this->api->upload_link_list($repo_id, $path)) { + foreach ($links as $link) { + $result[] = array( + 'mode' => 'upload-link', + 'id' => $link['token'], + 'link' => $link['link'], + ); + } + } + + 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') { + $share_id = $args['id'] ?: $args['user']; + + switch ($args['action']) { + case 'submit': + $result = $this->api->shared_item_add($repo_id, $path, $args['right'], $args['type'], $share_id); + break; + + case 'update': + $result = $this->api->shared_item_update($repo_id, $path, $args['right'], $args['type'], $share_id); + break; + + case 'delete': + $result = $this->api->shared_item_delete($repo_id, $path, $args['type'], $share_id); + break; + } + } + else if ($args['mode'] == 'download-link') { + switch ($args['action']) { + case 'submit': + $result = $this->api->share_link_add($repo_id, $path, $args['password'], $args['expire']); + + if ($result) { + $result['id'] = $result['token']; + $result['mode'] = 'download-link'; + } + break; + + case 'delete': + $result = $this->api->share_link_delete($args['id']); + break; + } + } + else if ($args['mode'] == 'upload-link') { + switch ($args['action']) { + case 'submit': + $result = $this->api->upload_link_add($repo_id, $path, $args['password']); + + if ($result) { + $result['id'] = $result['token']; + $result['mode'] = 'upload-link'; + } + break; + + case 'delete': + $result = $this->api->upload_link_delete($args['id']); + 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 $result; + } + } + + /** + * 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' => $this->user_label($user['name'], $user['email']), + '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) + { + // Remove protocol prefix and path, we work with host only + $host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']); + + if (!is_string($path) || !strlen($path)) { + $user = $_SESSION[$this->title . 'seafile_user']; + return 'seafile://' . rawurlencode($user) . '@' . $host . '/'; + } + + list($file, $repo_id, $library) = $this->find_library($path); + + 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']); + + if (strlen($path)) { + 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, $params = array()) + // { + // $root = ''; + // if (is_string($params['path']) && strlen($params['path'])) { + // $root = trim($params['path'], '/'); + // } + + // if ($this->config['cache'] && empty($params['recursive'])) { + // $cache = $this->rc->get_cache('seafile_' . $this->title, + // $this->config['cache'], $this->config['cache_ttl'], true); + + // if ($cache) { + // $cache_key = 'folders.' . md5(sprintf('%s:%d:%s', $library['id'], $params['level'], $root)); + // $folders = $cache->get($cache_key); + + // if (is_string($folders) && preg_match('/^([0-9]+):[\{\[]/', $folders, $m)) { + // $cache_mtime = $m[1]; + // $folders = json_decode(substr($folders, strlen($cache_mtime)+1), true); + // } + // else { + // $folders = null; + // } + + // if (strlen($root)) { + // $info = $this->api->directory_info($library['id'], $root); + // if ($info && $info['mtime']) { + // try { + // $dt = new DateTime($info['mtime']); + // $mtime = $dt->format('U'); + // } + // catch (Exception $e) { + // // ignore + // rcube::raise_error($e, true, false); + // } + // } + // } + // else { + // $mtime = $library['mtime']; + // } + + // if (is_array($folders) && $mtime && $cache_mtime && intval($mtime) === intval($cache_mtime)) { + // return $folders; + // } + // } + // } + + // $folders = array(); + // $add_folder = function($item, &$result, $parent) { + // if ($item['type'] == 'dir' && strlen($item['name'])) { + // $name = (strlen($parent) > 0 ? "$parent/" : '') . $item['name']; + + // $result[$name] = array( + // 'mtime' => $item['mtime'], + // 'permission' => $item['permission'], + // ); + + // return $name; + // } + // }; + + // // Full folder hierarchy requested, we can get all in one request... + // if (empty($params['recursive']) && empty($params['level'])) { + // if ($content = $this->api->directory_entries($library['id'], $root, 'dir', true)) { + // foreach ($content as $item) { + // $add_folder($item, $folders, trim($item['parent_dir'], '/')); + // } + // } + // } + // // Only part of the folder tree has been requested... + // else if ($content = $this->api->directory_entries($library['id'], $root, 'dir', false)) { + // $params['recursive'] = true; + // $params['level'] -= 1; + + // // Recursively add sub-folders tree + // foreach ($content as $item) { + // $folder = $add_folder($item, $folders, $root); + + // // FIXME: id="0000000000000000000000000000000000000000" means the folder is empty? + // if ($folder !== null && $params['level'] > 1 && $item['id'] !== "0000000000000000000000000000000000000000") { + // $params['path'] = $folder; + // $tree = $this->folders_tree($library, $params); + // if (!empty($tree)) { + // $folders = array_merge($folders, $tree); + // } + // } + // } + // } + + // if ($cache_key && is_array($content) && $mtime && ($_folders = json_encode($folders))) { + // $cache->set($cache_key, intval($mtime) . ':' . $_folders); + // } + + // return $folders; + // } + + // /** + // * Get list of SeaFile libraries + // */ + // protected function libraries() + // { + // // get from memory + // 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 ($this->config['cache']) { + // $cache = $this->rc->get_cache('seafile_' . $this->title, + // $this->config['cache'], $this->config['cache_ttl'], true); + + // if ($cache) { + // $repos = $cache->get('repos'); + + // if (is_string($repos) && preg_match('/^([0-9]+):[\{\[]/', $repos, $m)) { + // $mtime = $m[1]; + // $repos = json_decode(substr($repos, strlen($mtime)+1), true); + // // We use the cached value for up to 15 seconds + // // It should be enough to improve parallel folders listing requests + // if (is_array($repos) && $mtime + 15 >= time()) { + // return $repos; + // } + // } + // } + // } + + // $mtime = time(); + + // if ($list = $this->api->library_list()) { + // $this->libraries = $list; + + // if ($cache) { + // $cache->write('repos', $mtime . ':' . json_encode($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 + // ); + // } + + // /** + // * 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; + // } + // } + + // /** + // * Create display-username-with-email string + // */ + // protected function user_label($name, $email) + // { + // if ($name && $name != $email) { + // $label = "{$name} ({$email})"; + // } + // else { + // $label = $email; + // } + + // return $label; + // } +}