diff --git a/lib/api/sharing.php b/lib/api/sharing.php new file mode 100644 index 0000000..db7e8fe --- /dev/null +++ b/lib/api/sharing.php @@ -0,0 +1,84 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +class file_api_sharing extends file_api_common +{ + /** + * Request handler + */ + public function handle() + { + parent::handle(); + + if (!isset($this->args['folder']) || $this->args['folder'] === '') { + throw new Exception("Missing folder name", file_api_core::ERROR_CODE); + } + + list($driver, $path) = $this->api->get_driver($this->args['folder']); + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + return $driver->sharing($path, file_storage::SHARING_MODE_UPDATE); + } + else { + $form = $driver->sharing($path, file_storage::SHARING_MODE_FORM); + + if (empty($form)) { + throw new Exception("Sharing not supported", file_api_core::ERROR_UNSUPPORTED); + } + + $rights = $driver->sharing($path, file_storage::SHARING_MODE_RIGHTS); + + $this->localize_form_data($form); + + $result = array( + 'form' => $form, + 'rights' => (array) $rights, + ); + + return $result; + } + } + + /** + * Recurrent function to localize form data entries + */ + protected function localize_form_data(&$data, $key = null, $self = null) + { + if (!$self) { + $self = $this; + } + + if ($key == 'title' || $key == 'placeholder') { + $data = $self->api->translate($data); + } + else if ($key == 'options') { + foreach ($data as $idx => $val) { + $data[$idx] = $self->api->translate($val); + } + } + else if (is_array($data)) { + array_walk($data, array($self, 'localize_form_data'), $self); + } + } +} diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php index 38fcaad..6474e18 100644 --- a/lib/drivers/kolab/kolab_file_storage.php +++ b/lib/drivers/kolab/kolab_file_storage.php @@ -1,1454 +1,1469 @@ | +--------------------------------------------------------------------------+ | 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, ); } /** * 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()) + { + // TODO + } + + /** + * 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; } - /** - * 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; - } - /** * 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 c28a8e7..5c10bd4 100644 --- a/lib/drivers/seafile/seafile_api.php +++ b/lib/drivers/seafile/seafile_api.php @@ -1,1050 +1,1050 @@ | +--------------------------------------------------------------------------+ | 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/shared_items", array('p' => $dir)); + return $this->request('GET', "repos/$repo_id/dir/shared_items", array('p' => $dir)); } /** * 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 31ac9ef..db58f15 100644 --- a/lib/drivers/seafile/seafile_file_storage.php +++ b/lib/drivers/seafile/seafile_file_storage.php @@ -1,1358 +1,1429 @@ | +--------------------------------------------------------------------------+ | 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', + ), + 'right' => array( + 'title' => 'share.permission', + 'type' => 'select', + 'options' => array( + 'r' => 'share.readonly', + 'rw' => 'share.readwrite', + ), + ), + ), + ), + ); + + 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; + } + } + + /** + * 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; } - /** - * 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; - } - /** * 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 f3934e2..7bf5ce8 100644 --- a/lib/drivers/webdav/webdav_file_storage.php +++ b/lib/drivers/webdav/webdav_file_storage.php @@ -1,1030 +1,1045 @@ | +--------------------------------------------------------------------------+ | 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 + } + + /** + * 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); } - /** - * 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); - } - /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } } diff --git a/lib/file_api_core.php b/lib/file_api_core.php index 43a0b19..7cb5927 100644 --- a/lib/file_api_core.php +++ b/lib/file_api_core.php @@ -1,424 +1,424 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_core extends file_locale { const API_VERSION = 3; const ERROR_UNAUTHORIZED = 401; const ERROR_NOT_FOUND = 404; const ERROR_PRECONDITION_FAILED = 412; const ERROR_CODE = 500; const ERROR_INVALID = 501; const ERROR_NOT_IMPLEMENTED = 501; const OUTPUT_JSON = 'application/json'; const OUTPUT_HTML = 'text/html'; public $env = array( 'date_format' => 'Y-m-d H:i', 'language' => 'en_US', 'timezone' => 'UTC', ); protected $app_name = 'Kolab File API'; protected $drivers = array(); protected $icache = array(); protected $backend; /** * Returns API version */ public function client_version() { return self::API_VERSION; } /** * Initialise authentication/configuration backend class * * @return file_storage Main storage driver */ public function get_backend() { if ($this->backend) { return $this->backend; } $rcube = rcube::get_instance(); $driver = $rcube->config->get('fileapi_backend', 'kolab'); $this->backend = $this->load_driver_object($driver); // configure api $this->backend->configure($this->env); return $this->backend; } /** * Return supported/enabled external storage instances * * @param bool $as_objects Return drivers as objects not config data * @param array &$admin_drivers List of admin-configured drivers * * @return array List of storage drivers */ - public function get_drivers($as_objects = false, &$admin_drivers) + public function get_drivers($as_objects = false, &$admin_drivers = null) { $rcube = rcube::get_instance(); $backend = $this->get_backend(); $enabled = $rcube->config->get('fileapi_drivers'); $preconf = $rcube->config->get('fileapi_sources'); $result = array(); $all = array(); $iRony = defined('KOLAB_DAV_ROOT'); if (!empty($enabled)) { $drivers = $backend->driver_list(); foreach ($drivers as $item) { // Disable webdav sources/drivers in iRony that point to the // same host to prevent infinite recursion if ($iRony && $item['driver'] == 'webdav') { $self_url = parse_url($_SERVER['SCRIPT_URI']); $item_url = parse_url($item['host']); if ($self_url['host'] == $item_url['host']) { continue; } } $all[] = $item['title']; if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) { $result[] = $as_objects ? $this->get_driver_object($item) : $item; } } } $admin_drivers = array(); if (empty($result) && !empty($preconf)) { foreach ((array) $preconf as $title => $item) { if (!in_array($title, $all)) { $item['title'] = $title; $item['admin'] = true; $result[] = $as_objects ? $this->get_driver_object($item) : $item; $admin_drivers[] = $title; } } } return $result; } /** * Return driver for specified file/folder path * * @param string $path Folder/file path * * @return array Storage driver object, modified path, driver config */ public function get_driver($path) { $drivers = $this->get_drivers(); foreach ($drivers as $item) { $prefix = $item['title'] . file_storage::SEPARATOR; if ($path == $item['title'] || strpos($path, $prefix) === 0) { $selected = $item; break; } } if (empty($selected)) { return array($this->get_backend(), $path); } $path = substr($path, strlen($selected['title']) + 1); return array($this->get_driver_object($selected), $path, $selected); } /** * Initialize driver instance * * @param array $config Driver config * * @return file_storage Storage driver instance */ public function get_driver_object($config) { $key = $config['title']; if (empty($this->drivers[$key])) { $this->drivers[$key] = $driver = $this->load_driver_object($config['driver']); if ($config['username'] == '%u') { $backend = $this->get_backend(); $auth_info = $backend->auth_info(); $config['username'] = $auth_info['username']; $config['password'] = $auth_info['password']; } else if (!empty($config['password']) && empty($config['admin']) && !empty($key)) { $config['password'] = $this->decrypt($config['password']); } // configure api $driver->configure(array_merge($config, $this->env), $key); } return $this->drivers[$key]; } /** * Loads a driver */ public function load_driver_object($name) { $class = $name . '_file_storage'; if (!class_exists($class, false)) { $include_path = __DIR__ . "/drivers/$name" . PATH_SEPARATOR; $include_path .= ini_get('include_path'); set_include_path($include_path); } return new $class; } /** * Returns storage(s) capabilities * * @param bool $full Return all drivers' capabilities * * @return array Capabilities */ public function capabilities($full = true) { $rcube = rcube::get_instance(); $backend = $this->get_backend(); $caps = array(); // check support for upload progress if (($progress_sec = $rcube->config->get('upload_progress')) && ini_get('apc.rfc1867') && function_exists('apc_fetch') ) { $caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name'); $caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec; } // get capabilities of main storage module foreach ($backend->capabilities() as $name => $value) { // skip disabled capabilities if ($value !== false) { $caps[$name] = $value; } } // Manticore support if ($rcube->config->get('fileapi_manticore')) { $caps['MANTICORE'] = true; } // WOPI support if ($rcube->config->get('fileapi_wopi_office')) { $caps['WOPI'] = true; } if (!$full) { return $caps; } if ($caps['MANTICORE']) { $manticore = new file_manticore($this); $caps['MANTICORE_EDITABLE'] = $manticore->supported_filetypes(true); } if ($caps['WOPI']) { $wopi = new file_wopi($this); $caps['WOPI_EDITABLE'] = $wopi->supported_filetypes(true); } // get capabilities of other drivers $drivers = $this->get_drivers(true); foreach ($drivers as $driver) { if ($driver != $backend) { $title = $driver->title(); foreach ($driver->capabilities() as $name => $value) { // skip disabled capabilities if ($value !== false) { $caps['MOUNTPOINTS'][$title][$name] = $value; } } } } return $caps; } /** * Get user name from user identifier (email address) using LDAP lookup * * @param string $email User identifier * * @return string User name */ public function resolve_user($email) { $key = "user:$email"; // make sure Kolab backend is initialized so kolab_storage can be found $this->get_backend(); // @todo: Move this into drivers if ($this->icache[$key] === null && class_exists('kolab_storage') && ($ldap = kolab_storage::ldap()) ) { $user = $ldap->get_user_record($email, $_SESSION['imap_host']); $this->icache[$key] = $user ?: false; } if ($this->icache[$key]) { return $this->icache[$key]['displayname'] ?: $this->icache[$key]['name']; } } /** * Return mimetypes list supported by built-in viewers * * @return array List of mimetypes */ protected function supported_mimetypes() { $rcube = rcube::get_instance(); $mimetypes = array(); $mimetypes_c = array(); $dir = __DIR__ . '/viewers'; // make sure Kolab backend is initialized so kolab_auth can modify config $backend = $this->get_backend(); if ($handle = opendir($dir)) { while (false !== ($file = readdir($handle))) { if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) { include_once $dir . '/' . $file; $class = 'file_viewer_' . $matches[1]; $viewer = new $class($this); if ($supported = $viewer->supported_mimetypes()) { $mimetypes = array_merge($mimetypes, $supported); } } } closedir($handle); } // Here we return mimetypes supported for editing and creation of files // @TODO: maybe move this to viewers if ($rcube->config->get('fileapi_wopi_office')) { $mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt'); $mimetypes_c['application/vnd.oasis.opendocument.presentation'] = array('ext' => 'odp'); $mimetypes_c['application/vnd.oasis.opendocument.spreadsheet'] = array('ext' => 'ods'); } else if ($rcube->config->get('fileapi_manticore')) { $mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt'); } $mimetypes_c['text/plain'] = array('ext' => 'txt'); $mimetypes_c['text/html'] = array('ext' => 'html'); foreach (array_keys($mimetypes_c) as $type) { list ($app, $label) = explode('/', $type); $label = preg_replace('/[^a-z]/', '', $label); $mimetypes_c[$type]['label'] = $this->translate('type.' . $label); } return array( 'view' => $mimetypes, 'edit' => $mimetypes_c, ); } /** * Encrypts data with current user password * * @param string $str A string to encrypt * * @return string Encrypted string (and base64-encoded) */ public function encrypt($str) { $rcube = rcube::get_instance(); $key = $this->get_crypto_key(); return $rcube->encrypt($str, $key, true); } /** * Decrypts data encrypted with encrypt() method * * @param string $str Encrypted string (base64-encoded) * * @return string Decrypted string */ public function decrypt($str) { $rcube = rcube::get_instance(); $key = $this->get_crypto_key(); return $rcube->decrypt($str, $key, true); } /** * Set encryption password */ protected function get_crypto_key() { $key = 'chwala_crypto_key'; $rcube = rcube::get_instance(); $backend = $this->get_backend(); $user = $backend->auth_info(); $password = $user['password'] . $user['username']; // encryption password must be 24 characters, no less, no more if (($len = strlen($password)) > 24) { $password = substr($password, 0, 24); } else { $password = $password . substr($rcube->config->get('des_key'), 0, 24 - $len); } $rcube->config->set($key, $password); return $key; } } diff --git a/lib/file_storage.php b/lib/file_storage.php index 8e3e517..0ae02a6 100644 --- a/lib/file_storage.php +++ b/lib/file_storage.php @@ -1,372 +1,390 @@ | +--------------------------------------------------------------------------+ | 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; + + /** * 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()); + /** * 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); } diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php index f5b6313..673782c 100644 --- a/lib/locale/en_US.php +++ b/lib/locale/en_US.php @@ -1,93 +1,101 @@ Kolab Server.'; $LANG['about.warranty'] = 'Professional support is available from Kolab Systems.'; $LANG['about.support'] = 'It comes with absolutely no warranties and is typically run entirely self supported. You can find help & information on the community web site & wiki.'; $LANG['collection.audio'] = 'Audio'; $LANG['collection.video'] = 'Video'; $LANG['collection.image'] = 'Images'; $LANG['collection.document'] = 'Documents'; $LANG['file.copy'] = 'Copy'; $LANG['file.create'] = 'Create File'; $LANG['file.download'] = 'Download'; $LANG['file.edit'] = 'Edit'; $LANG['file.upload'] = 'Upload File'; $LANG['file.name'] = 'Name'; $LANG['file.move'] = 'Move'; $LANG['file.mtime'] = 'Modified'; $LANG['file.size'] = 'Size'; $LANG['file.open'] = 'Open'; $LANG['file.delete'] = 'Delete'; $LANG['file.rename'] = 'Rename'; $LANG['file.search'] = 'Search file'; $LANG['file.type'] = 'Type'; $LANG['file.save'] = 'Save'; $LANG['file.skip'] = 'Skip'; $LANG['file.skipall'] = 'Skip all'; $LANG['file.overwrite'] = 'Overwrite'; $LANG['file.overwriteall'] = 'Overwrite all'; $LANG['file.moveconfirm'] = 'This action is going to overwrite the destination file: $file.'; $LANG['file.progress'] = 'Uploaded $current of $total ($percent%)'; $LANG['folder.createtitle'] = 'Create Folder'; $LANG['folder.delete'] = 'Delete'; $LANG['folder.edit'] = 'Edit'; $LANG['folder.edittitle'] = 'Edit Folder'; $LANG['folder.under'] = 'inside the current folder'; $LANG['folder.driverselect'] = 'bind with the external storage'; $LANG['folder.driverwithpass'] = 'remember password'; $LANG['folder.driverwithpassdesc'] = 'Stored passwords will be encrypted. Enable this if you do not want to be asked for the password on every login or you want this storage to be available via WebDAV.'; $LANG['folder.name'] = 'Name:'; $LANG['folder.authenticate'] = 'Logon to $title'; $LANG['folder.parent'] = 'Subfolder of:'; $LANG['form.submit'] = 'Submit'; $LANG['form.cancel'] = 'Cancel'; $LANG['form.hostname'] = 'Hostname:'; $LANG['form.username'] = 'Username:'; $LANG['form.password'] = 'Password:'; $LANG['form.baseuri'] = 'Base URI:'; $LANG['login.username'] = 'Username'; $LANG['login.password'] = 'Password'; $LANG['login.login'] = 'Log in'; $LANG['reqtime'] = 'Request time: $1 sec.'; $LANG['maxupload'] = 'Maximum file size: $1'; $LANG['internalerror'] = 'Internal system error!'; $LANG['loginerror'] = 'Incorrect username or password!'; $LANG['authenticating'] = 'Authenticating...'; $LANG['loading'] = 'Loading...'; $LANG['saving'] = 'Saving...'; $LANG['deleting'] = 'Deleting...'; $LANG['copying'] = 'Copying...'; $LANG['moving'] = 'Moving...'; $LANG['logout'] = 'Logout'; $LANG['close'] = 'Close'; $LANG['servererror'] = 'Server Error!'; $LANG['session.expired'] = 'Session has expired. Login again, please'; $LANG['localstorage'] = 'local storage'; $LANG['search'] = 'Search'; $LANG['search.loading'] = 'Searching...'; $LANG['search.in_all_folders'] = 'in all folders'; $LANG['search.in_current_folder'] = 'in current folder'; +$LANG['share.permissions'] = 'Permissions'; +$LANG['share.readonly'] = 'read-only'; +$LANG['share.readwrite'] = 'read-write'; +$LANG['share.usergroup'] = 'User or Group'; +$LANG['share.permission'] = 'Permission'; +$LANG['share.uploadlinks'] = 'Upload Links'; +$LANG['share.downloadlinks'] = 'Download Links'; + $LANG['size.B'] = 'B'; $LANG['size.KB'] = 'KB'; $LANG['size.MB'] = 'MB'; $LANG['size.GB'] = 'GB'; $LANG['type.vndoasisopendocumenttext'] = 'OpenDocument Document (.odt)'; $LANG['type.vndoasisopendocumentpresentation'] = 'OpenDocument Presentation (.odp)'; $LANG['type.vndoasisopendocumentspreadsheet'] = 'OpenDocument Spreadsheet (.ods)'; $LANG['type.plain'] = 'Plain Text Document (.txt)'; $LANG['type.html'] = 'HTML Document (.html)'; $LANG['upload.size'] = 'Size:'; $LANG['upload.size.error'] = 'Maximum upload size ($size) exceeded!'; $LANG['upload.progress'] = 'Progress:'; $LANG['upload.rate'] = 'Rate:'; $LANG['upload.eta'] = 'ETA:';