diff --git a/lib/api/file_create.php b/lib/api/file_create.php index e4450de..12cc814 100644 --- a/lib/api/file_create.php +++ b/lib/api/file_create.php @@ -1,109 +1,110 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_create extends file_api_common { /** * Request handler */ public function handle() { parent::handle(); if (!isset($this->args['file']) || $this->args['file'] === '') { throw new Exception("Missing file name", file_api_core::ERROR_CODE); } if (!isset($this->args['content'])) { if (!($this->api instanceof file_api_lib) || empty($this->args['path'])) { throw new Exception("Missing file content", file_api_core::ERROR_CODE); } } + $is_file = false; if (is_resource($this->args['content'])) { $chunk = stream_get_contents($this->args['content'], 1024000, 0); } - else if ($this->args['path']) { + else if ($this->args['path'] ?? null) { $chunk = $this->args['path']; $is_file = true; } else { $chunk = $this->args['content']; } $ctype = $this->args['content-type']; if ($ctype && !preg_match('/^[a-z_-]+\/[a-z._-]+$/', $ctype)) { $ctype = ''; } $request = $this instanceof file_api_file_update ? 'file_update' : 'file_create'; $file = array( 'content' => $this->args['content'], - 'path' => $this->args['path'], - 'type' => rcube_mime::file_content_type($chunk, $this->args['file'], $ctype, !$is_file), + 'path' => $this->args['path'] ?? null, + 'type' => rcube_mime::file_content_type($chunk, $this->args['file'] ?? null, $ctype, !$is_file), ); if (strpos($file['type'], 'empty') !== false && $ctype) { $file['type'] = $ctype; } else if (empty($file['type'])) { $file['type'] = 'application/octet-stream'; } // Get file content from a template if ($request == 'file_create' && empty($file['path']) && empty($file['content'])) { $this->use_file_template($file); } list($driver, $path) = $this->api->get_driver($this->args['file']); $driver->$request($path, $file); - if (rcube_utils::get_boolean((string) $this->args['info'])) { + if (rcube_utils::get_boolean((string) ($this->args['info'] ?? null))) { return $driver->file_info($path); } } /** * Use templates when creating empty files */ protected function use_file_template(&$file) { if ($ext = array_search($file['type'], file_utils::$ext_map)) { // find the template $ext = ".$ext"; if ($handle = opendir(__DIR__ . '/../templates')) { while (false !== ($entry = readdir($handle))) { if (substr($entry, -strlen($ext)) == $ext) { // set path to the template file $file['path'] = __DIR__ . '/../templates/' . $entry; break; } } closedir($handle); } } } } diff --git a/lib/api/file_info.php b/lib/api/file_info.php index 6627daa..6e62a3b 100644 --- a/lib/api/file_info.php +++ b/lib/api/file_info.php @@ -1,180 +1,180 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_info extends file_api_common { /** * Request handler */ public function handle() { parent::handle(); // check Manticore support. Note: we don't use config->get('fileapi_manticore') // here as it may be not properly set if backend driver wasn't initialized yet $capabilities = $this->api->capabilities(false); - $manticore = $capabilities['MANTICORE']; - $wopi = $capabilities['WOPI']; + $manticore = $capabilities['MANTICORE'] ?? null; + $wopi = $capabilities['WOPI'] ?? null; // support file_info by session ID if (!isset($this->args['file']) || $this->args['file'] === '') { if (($manticore || $wopi) && !empty($this->args['session'])) { if ($info = $this->file_document_file($this->args['session'])) { $this->args['file'] = $info['file']; } } else { throw new Exception("Missing file name", file_api_core::ERROR_CODE); } } if ($this->args['file'] !== null) { try { list($driver, $path) = $this->api->get_driver($this->args['file']); $info = $driver->file_info($path); $info['file'] = $this->args['file']; } catch (Exception $e) { // Invited user may have no access to the file, // ignore errors if session exists if (!$this->args['viewer'] || !$this->args['session']) { throw $e; } } } // Possible 'viewer' types are defined in files_api.js:file_type_supported() // 1 - Native browser support // 2 - Chwala viewer exists // 4 - Editor exists (manticore/wopi) if (rcube_utils::get_boolean((string) $this->args['viewer'])) { if ($this->args['file'] !== null) { $this->file_viewer_info($info); } if ((intval($this->args['viewer']) & 4)) { // @TODO: Chwala client should have a possibility to select // between wopi and manticore? if (!$wopi || !$this->file_wopi_handler($info)) { if ($manticore) { $this->file_manticore_handler($info); } } } } // check writable flag if ($this->args['file'] !== null) { $path = explode(file_storage::SEPARATOR, $path); array_pop($path); $path = implode(file_storage::SEPARATOR, $path); $acl = $driver->folder_rights($path); $info['writable'] = ($acl & file_storage::ACL_WRITE) != 0; } return $info; } /** * Merge file viewer data into file info */ protected function file_viewer_info(&$info) { $file = $this->args['file']; $viewer = $this->find_viewer($info['type']); if ($viewer) { $info['viewer'] = array(); if ($frame = $viewer->frame($file, $info['type'])) { $info['viewer']['frame'] = $frame; } else if ($href = $viewer->href($file, $info['type'])) { $info['viewer']['href'] = $href; } } } /** * Get file from manticore/wopi session */ protected function file_document_file($session_id) { $document = file_document::get_handler($this->api, $session_id); return $document->session_file($session_id, true); } /** * Merge manticore session data into file info */ protected function file_manticore_handler(&$info) { $manticore = new file_manticore($this->api); $file = $this->args['file']; $session = $this->args['session']; if (in_array_nocase($info['type'], $manticore->supported_filetypes(true))) { $info['viewer']['manticore'] = true; } else { return false; } if ($uri = $manticore->session_start($file, $info, $session)) { $info['viewer']['href'] = $uri; $info['viewer']['post'] = $manticore->editor_post_params($info); $info['session'] = $manticore->session_info($session, true); } return true; } /** * Merge WOPI session data into file info */ protected function file_wopi_handler(&$info) { $wopi = new file_wopi($this->api); $file = $this->args['file']; $session = $this->args['session']; if (in_array_nocase($info['type'], $wopi->supported_filetypes(true))) { $info['viewer']['wopi'] = true; } else { return false; } if ($uri = $wopi->session_start($file, $info, $session)) { $info['viewer']['href'] = $uri; $info['viewer']['post'] = $wopi->editor_post_params($info); $info['session'] = $wopi->session_info($session, true); } return true; } } diff --git a/lib/api/file_list.php b/lib/api/file_list.php index a9595f4..7d7a416 100644 --- a/lib/api/file_list.php +++ b/lib/api/file_list.php @@ -1,62 +1,62 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_list 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); } $params = array( - 'reverse' => rcube_utils::get_boolean((string) $this->args['reverse']), + 'reverse' => rcube_utils::get_boolean((string) ($this->args['reverse'] ?? "")), ); if (!empty($this->args['sort'])) { $params['sort'] = strtolower($this->args['sort']); } if (!empty($this->args['search'])) { $params['search'] = $this->args['search']; if (!is_array($params['search'])) { $params['search'] = array('name' => $params['search']); } } list($driver, $path) = $this->api->get_driver($this->args['folder']); // add mount point prefix to file paths if ($path != $this->args['folder']) { $params['prefix'] = substr($this->args['folder'], 0, -strlen($path)); } return $driver->file_list($path, $params); } } diff --git a/lib/api/quota.php b/lib/api/quota.php index ac7199a..af3d186 100644 --- a/lib/api/quota.php +++ b/lib/api/quota.php @@ -1,49 +1,49 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_quota extends file_api_common { /** * Request handler */ public function handle() { parent::handle(); - list($driver, $path) = $this->api->get_driver($this->args['folder']); + list($driver, $path) = $this->api->get_driver($this->args['folder'] ?? null); $quota = $driver->quota($path); - if (!$quota['total']) { + if (!($quota['total'] ?? false)) { $quota['percent'] = 0; } - else if ($quota['total']) { + else if ($quota['total'] ?? false) { if (!isset($quota['percent'])) { $quota['percent'] = min(100, round(($quota['used']/max(1, $quota['total']))*100)); } } return $quota; } } diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php index f5c6367..355ba5a 100644 --- a/lib/drivers/kolab/kolab_file_storage.php +++ b/lib/drivers/kolab/kolab_file_storage.php @@ -1,1681 +1,1682 @@ | +--------------------------------------------------------------------------+ | 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'], )); return false; } /** * 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)) { foreach($host as $key => $val) { $host = is_numeric($key) ? $val : $key; break; } } } 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); + $port = null; 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) { + if ($_SESSION['user_id'] ?? false || $user) { // overwrite config with user preferences $this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']); $this->rc->config->set_user_prefs((array)$this->rc->user->get_prefs()); $storage = $this->rc->get_storage(); $storage->set_charset($this->rc->config->get('default_charset', RCUBE_CHARSET)); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); } } /** * Configures environment * * @param array $config Configuration * @param string $title Source identifier */ public function configure($config, $title = null) { $this->config = array_merge($this->config, $config); // @TODO: this is currently not possible to have multiple sessions in Roundcube } /** * Returns current instance title * * @return string Instance title (mount point) */ public function title() { return ''; } /** * Storage driver capabilities * * @return array List of capabilities */ public function capabilities() { // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } $storage = $this->rc->get_storage(); $quota = $storage->get_capability('QUOTA'); return array( file_storage::CAPS_MAX_UPLOAD => $max_filesize, file_storage::CAPS_QUOTA => $quota, file_storage::CAPS_LOCKS => true, file_storage::CAPS_SUBSCRIPTIONS => true, file_storage::CAPS_ACL => true, file_storage::CAPS_FAST_FOLDER_LIST => 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_fast($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_fast($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_fast($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_fast($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'], + 'size' => (int) ($file['size'] ?? null), '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']; + $filename = ($params['prefix'] ?? null) . $folder_name . file_storage::SEPARATOR . ($file['name'] ?? null); $result[$filename] = array( 'name' => $file['name'], - 'size' => (int) $file['size'], + 'size' => (int) ($file['size'] ?? null), '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_fast($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_fast($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 = $this->folder_in($folder_name); $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 = $this->folder_in($folder_name); $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 = $this->folder_in($folder_name); $new_name = $this->folder_in($new_name); $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 = $this->folder_in($folder_name); $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 = $this->folder_in($folder_name); $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', 'level', 'path') * * @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(); $separator = $imap->get_hierarchy_delimiter(); $root = isset($params['path']) && strlen($params['path']) ? $this->folder_in($params['path']) . '/' : ''; $folders = $imap->list_folders_subscribed($root, '*', '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 && !strlen($root)) { $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($root, '*', 'file', $rights); $folders = array_diff($folders, $subscribed); } $folders = array_map(array($this, 'folder_out'), $folders); } // This could probably be optimized by doing a direct // IMAP LIST command with prepared second argument, but // it would make caching not optimal - if ($params['level'] > 0) { + if (($params['level'] ?? 0) > 0) { $offset = isset($params['path']) && strlen($params['path']) ? strlen($params['path']) + 1 : 0; foreach ($folders as $idx => $folder) { if (substr_count($folder, $separator, $offset) >= $params['level']) { unset($folders[$idx]); } } } // 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 = $this->folder_in($folder); $rights = file_storage::ACL_READ; // get list of known writable folders from cache $cache_key = 'mailboxes.permissions'; $permissions = (array) $storage->get_cache($cache_key); if (array_key_exists($folder, $permissions)) { return $permissions[$folder]; } // For better performance, assume personal folders are writeable if ($storage->folder_namespace($folder) == 'personal') { $rights |= file_storage::ACL_WRITE; } else { $myrights = $storage->my_rights($folder); if (in_array('t', (array) $myrights)) { $rights |= file_storage::ACL_WRITE; } $permissions[$folder] = $rights; $storage->update_cache($cache_key, $permissions); } return $rights; } /** * Returns a list of locks * * This method should return all the locks for a particular URI, including * locks that might be set on a parent URI. * * If child_locks is set to true, this method should also look for * any locks in the subtree of the URI for locks. * * @param string $path File/folder path * @param bool $child_locks Enables subtree checks * * @return array List of locks * @throws Exception */ public function lock_list($path, $child_locks = false) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); // get locks list $list = $this->lock_db->lock_list($uri, $child_locks); // convert back resource string into URIs foreach ($list as $idx => $lock) { $list[$idx]['uri'] = $this->uri2path($lock['uri']); } return $list; } /** * Locks a URI * * @param string $path File/folder path * @param array $lock Lock data * - depth: 0/'infinite' * - scope: 'shared'/'exclusive' * - owner: string * - token: string * - timeout: int * * @throws Exception */ public function lock($path, $lock) { $this->init_lock_db(); // convert URI to global resource string $uri = $this->path2uri($path); if (!$this->lock_db->lock($uri, $lock)) { throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR); } } /** * Removes a lock from a URI * * @param string $path File/folder path * @param array $lock Lock data * * @throws Exception */ public function unlock($path, $lock) { $this->init_lock_db(); // convert path to global resource string $uri = $this->path2uri($path); if (!$this->lock_db->unlock($uri, $lock)) { throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR); } } /** * Return disk quota information for specified folder. * * @param string $folder_name Name of a folder with full path * * @return array Quota * @throws Exception */ public function quota($folder) { $storage = $this->rc->get_storage(); $quota = $storage->get_quota(); $quota = $this->rc->plugins->exec_hook('quota', $quota); unset($quota['abort']); return $quota; } /** * Sharing interface * * @param string $folder_name Name of a folder with full path * @param int $mode Sharing action mode * @param array $args POST/GET parameters * * @return mixed Sharing response * @throws Exception */ public function sharing($folder, $mode, $args = array()) { $folder_name = $this->folder_in($folder); $storage = $this->rc->get_storage(); $folder_info = $storage->folder_info($folder_name); if (!is_array($folder_info['rights'])) { throw new Exception("Storage error. Failed to get folder permissions.", file_storage::ERROR); } if (!in_array('a', $folder_info['rights'])) { throw new Exception("No permissions to administer this folder.", file_storage::ERROR_FORBIDDEN); } if ($mode == file_storage::SHARING_MODE_FORM) { $form = array( 'shares' => array( 'title' => 'share.permissions', 'form' => array( 'user' => array( 'title' => 'share.usergroup', 'type' => 'input', 'autocomplete' => 'user,group', ), 'right' => array( 'title' => 'share.permission', 'type' => 'select', 'options' => array( 'r' => 'share.readonly', 'rw' => 'share.readwrite', 'a' => 'share.admin', ), ), ), 'extra_fields' => array( 'type' => 'user', 'id' => '', ), ), ); return $form; } if ($mode == file_storage::SHARING_MODE_RIGHTS) { $result = array(); $acl_list = $storage->get_acl($folder_name); $ac = new kolab_file_autocomplete($this); foreach ((array) $acl_list as $name => $acl) { if ($name == $_SESSION['username']) { continue; } if (in_array('a', $acl)) { $right = 'a'; } else if (in_array('i', $acl)) { $right = 'rw'; } else if (in_array('r', $acl)) { $right = 'r'; } else { continue; } $type = strpos($name, 'group:') === 0 ? 'group' : 'user'; $id = $name; $display = $ac->resolve_uid($id, $title); $result[] = array( 'mode' => 'shares', 'type' => $type, 'right' => $right, 'user' => $display, 'title' => $title, 'id' => $id, ); } return $result; } if ($mode == file_storage::SHARING_MODE_UPDATE) { if ($args['mode'] == 'shares') { $user = $args['id']; if (!$user) { $user = ($args['type'] == 'group' ? 'group:' : '') . preg_replace('/^group:/', '', $args['user']); } switch ($args['right']) { case 'r': $acl = 'lrs'; break; case 'rw': $acl = 'lrswite'; break; case 'a': $acl = 'lrswiteax'; break; } if (empty($user) || (empty($acl) && $args['action'] != 'delete')) { throw new Exception("Invalid input.", file_storage::ERROR); } switch ($args['action']) { case 'submit': case 'update': if ($result = $storage->set_acl($folder_name, $user, $acl)) { $ac = new kolab_file_autocomplete($this); $display = $ac->resolve_uid($user, $title); $result = array('display' => $display, 'title' => $title); } break; case 'delete': $result = $storage->delete_acl($folder_name, $user); break; } } else { throw new Exception("Invalid input.", file_storage::ERROR); } if (empty($result)) { throw new Exception("Storage error. Failed to update share.", file_storage::ERROR); } return $result; } } /** * User/group search (autocompletion) * * @param string $search Search string * @param int $mode Search mode * * @return array Users/Groups list * @throws Exception */ public function autocomplete($search, $mode) { $ac = new kolab_file_autocomplete($this); $result = $ac->search($search, $mode & file_storage::SEARCH_GROUP); if ($result === false) { throw new Exception("Failed to search users", file_storage::ERROR); } 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) { $storage = $this->rc->get_storage(); $namespace = $storage->get_namespace(); $separator = $storage->get_hierarchy_delimiter(); $_path = str_replace(file_storage::SEPARATOR, $separator, $path); $_path = $this->folder_in($_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]); if (strpos($path, '&') !== false) { $path = rcube_charset::convert($path, 'UTF7-IMAP', RCUBE_CHARSET); } // personal namespace if ($user == $owner) { // do nothing // Note: that might not work if personal namespace uses e.g. INBOX/ prefix. } // shared namespace else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) { $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, $fast_mode = 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, $fast_mode); } /** * 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 * @param bool $fast_mode Return limited list of file attributes * * @return array File data * @throws Exception */ protected function get_file_object(&$file_name, &$folder = null, $cache = false, $fast_mode = 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($this->folder_in($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, $fast_mode); $file = $files[0]; if ($cache) { $this->icache[$original_name] = $file; } return $file; } /** * 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 * @see self::get_file_object() */ protected function get_file_object_fast(&$file_name, &$folder = null, $cache = false) { return $this->get_file_object($file_name, $folder, $cache, true); } /** * 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 (!is_string($folder_name) || $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 = $this->folder_in($folder_name); $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 (isset($file['filename']) && !$file['name']) { + if (isset($file['filename']) && !($file['name'] ?? false)) { $file['name'] = $file['filename']; } 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'], + 'size' => $file['size'] ?? null, )); unset($file['name']); unset($file['size']); unset($file['type']); unset($file['path']); unset($file['fileid']); return $file; } /** * Initializes file_locks object */ protected function init_lock_db() { if (!$this->lock_db) { $this->lock_db = new file_locks; } } /** * Apply any conversion on folder name input */ protected function folder_in($folder_name) { $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP'); $plugin = $this->rc->plugins->exec_hook('folder_mod', array('folder' => $folder_name, 'dir' => 'in')); return $plugin['folder']; } /** * Apply any conversion on folder name output * * For example plugins can replace "/Outher Users/jane.doe/" * with "/Other Users/Doe, Jane (jane.doe)/" */ protected function folder_out($folder_name) { $plugin = $this->rc->plugins->exec_hook('folder_mod', array('folder' => $folder_name, 'dir' => 'out')); if (strpos($plugin['folder'], '&') !== false) { $plugin['folder'] = rcube_charset::convert($plugin['folder'], 'UTF7-IMAP', RCUBE_CHARSET); } return $plugin['folder']; } } diff --git a/lib/file_api.php b/lib/file_api.php index 9fdf849..95f9c5f 100644 --- a/lib/file_api.php +++ b/lib/file_api.php @@ -1,541 +1,541 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api extends file_api_core { public $session; public $config; public $browser; public $output_type = file_api_core::OUTPUT_JSON; /** * Class factory. */ public static function factory() { $class = 'file_api' . (!empty($_GET['wopi']) ? '_wopi' : ''); return new $class; } /** * Class constructor. */ public function __construct() { $rcube = rcube::get_instance(); register_shutdown_function(array($this, 'shutdown')); $this->config = $rcube->config; $this->session_init(); } /** * Process the request and dispatch it to the requested service */ public function run() { $this->request = strtolower($_GET['method']); // Check the session, authenticate the user - if (!$this->session_validate($this->request == 'authenticate', $_REQUEST['token'])) { + if (!$this->session_validate($this->request == 'authenticate', $_REQUEST['token'] ?? null)) { $this->session->destroy(session_id()); $this->session->regenerate_id(false); if ($username = $this->authenticate()) { // Init locale after the session started $this->locale_init(); $this->env['language'] = $this->language; $_SESSION['user'] = $username; $_SESSION['env'] = $this->env; // remember client API version - if (is_numeric($_GET['version'])) { + if (is_numeric($_GET['version'] ?? null)) { $_SESSION['version'] = $_GET['version']; } if ($this->request == 'authenticate') { $this->output_success(array( 'token' => session_id(), 'capabilities' => $this->capabilities(), )); } } else { throw new Exception("Invalid session", file_api_core::ERROR_UNAUTHORIZED); } } else { // Init locale after the session started $this->locale_init(); } // Call service method $result = $this->request_handler($this->request); // Send success response, errors should be handled by driver class // by throwing exceptions or sending output by itself $this->output_success($result); } /** * Session validation check and session start */ protected function session_validate($new_session = false, $token = null) { if (!$new_session) { $sess_id = rcube_utils::request_header('X-Session-Token') ?: $token; } if (empty($sess_id)) { $this->session->start(); return false; } session_id($sess_id); $this->session->start(); if (empty($_SESSION['user'])) { return false; } // Single-document session? if (!($this instanceof file_api_wopi) - && ($doc_id = $_SESSION['document_session']) + && ($doc_id = ($_SESSION['document_session'] ?? null)) && (strpos($this->request, 'document') !== 0 || $doc_id != $_GET['id']) ) { throw new Exception("Access denied", file_api_core::ERROR_UNAUTHORIZED); } if ($_SESSION['env']) { $this->env = $_SESSION['env']; } return true; } /** * Initializes session */ protected function session_init() { $rcube = rcube::get_instance(); $sess_name = $this->config->get('session_name'); $lifetime = $this->config->get('session_lifetime', 0) * 60; if ($lifetime) { ini_set('session.gc_maxlifetime', $lifetime * 2); } ini_set('session.name', $sess_name ? $sess_name : 'file_api_sessid'); ini_set('session.use_cookies', 0); ini_set('session.serialize_handler', 'php'); // Roundcube Framework >= 1.2 if (in_array('factory', get_class_methods('rcube_session'))) { $this->session = rcube_session::factory($this->config); } // Rouncube Framework < 1.2 else { /* @phpstan-ignore-next-line */ $this->session = new rcube_session($rcube->get_dbh(), $this->config); $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); $this->session->set_ip_check($this->config->get('ip_check')); } $this->session->register_gc_handler(array($rcube, 'gc')); // this is needed to correctly close session in shutdown function $rcube->session = $this->session; } /** * Script shutdown handler */ public function shutdown() { // write performance stats to logs/console if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) { // we have to disable per_user_logging to make sure stats end up in the main console log $this->config->set('per_user_logging', false); // make sure logged numbers use unified format setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); if (function_exists('memory_get_usage')) { $mem = round(memory_get_usage() / 1024 /1024, 1); } if (function_exists('memory_get_peak_usage')) { $mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1); } $path = !empty($this->path) ? '/' . implode($this->path, '/') : ''; $request = ($this instanceof file_api_wopi ? 'wopi/' : '') . $this->request; if ($path !== '' && substr_compare($this->request, $path, -1 * strlen($path), strlen($path), true) != 0) { $request .= $path; } $log = sprintf('%s: %s [%s]', $this->method ?: $_SERVER['REQUEST_METHOD'], trim($request) ?: '/', $mem); if (defined('FILE_API_START')) { rcube::print_timer(FILE_API_START, $log); } else { rcube::console($log); } } } /** * Authentication request handler (HTTP Auth) */ protected function authenticate() { if (isset($_POST['username'])) { $username = $_POST['username']; $password = $_POST['password']; } else if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { $tokens = array( $_SERVER['REMOTE_USER'], $_SERVER['REDIRECT_REMOTE_USER'], $_SERVER['HTTP_AUTHORIZATION'], rcube_utils::request_header('Authorization'), ); foreach ($tokens as $token) { if (!empty($token)) { if (stripos($token, 'Basic ') === 0) { $basicAuthData = base64_decode(substr($token, 6)); list($username, $password) = explode(':', $basicAuthData, 2); if ($username) { break; } } else if (stripos($token, 'Bearer ') === 0) { $username = base64_decode(substr($token, 7)); if ($username) { break; } } } } } if (!empty($username)) { $backend = $this->get_backend(); $result = $backend->authenticate($username, $password); if (empty($result)) { /* header('WWW-Authenticate: Basic realm="' . $this->app_name .'"'); header('HTTP/1.1 401 Unauthorized'); exit; */ throw new Exception("Invalid password or username", file_api_core::ERROR_UNAUTHORIZED); } } return $username; } /** * Storage/System method handler */ protected function request_handler($request) { // handle "global" requests that don't require api driver switch ($request) { case 'ping': return array(); case 'quit': $this->session->destroy(session_id()); return array(); case 'configure': foreach (array_keys($this->env) as $name) { if (isset($_GET[$name])) { $this->env[$name] = $_GET[$name]; } } $_SESSION['env'] = $this->env; return $this->env; case 'upload_progress': return $this->upload_progress(); case 'mimetypes': return $this->supported_mimetypes(); case 'capabilities': return $this->capabilities(); } // handle request if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) { $aliases = array( // request name aliases for backward compatibility 'lock' => 'lock_create', 'unlock' => 'lock_delete', 'folder_rename' => 'folder_move', ); // Redirect all document_* actions into 'document' action if (preg_match('/^(sessions|invitations|document_[a-z]+)$/', $request)) { $request = 'document'; } - $request = $aliases[$request] ?: $request; + $request = $aliases[$request] ?? $request; require_once __DIR__ . "/api/common.php"; include_once __DIR__ . "/api/$request.php"; $class_name = "file_api_$request"; if (class_exists($class_name, false)) { $handler = new $class_name($this); return $handler->handle(); } } throw new Exception("Unknown method", file_api_core::ERROR_INVALID); } /** * File upload progress handler */ protected function upload_progress() { if (function_exists('apc_fetch')) { $prefix = ini_get('apc.rfc1867_prefix'); $uploadid = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET); $status = apc_fetch($prefix . $uploadid); if (!empty($status)) { $status['percent'] = round($status['current']/$status['total']*100); if ($status['percent'] < 100) { $diff = max(1, time() - intval($status['start_time'])); // calculate time to end of uploading (in seconds) $status['eta'] = intval($diff * (100 - $status['percent']) / $status['percent']); // average speed (bytes per second) $status['rate'] = intval($status['current'] / $diff); } } $status['id'] = $uploadid; return $status; // id, done, total, current, percent, start_time, eta, rate } throw new Exception("Not supported", file_api_core::ERROR_CODE); } /** * Returns complete File URL * * @param string $file File name (with path) * * @return string File URL */ public function file_url($file) { return $this->api_url() . '?method=file_get' . '&file=' . urlencode($file) . '&token=' . urlencode(session_id()); } /** * Returns API URL * * @return string API URL */ public function api_url() { $api_url = $this->config->get('file_api_url', ''); if (!preg_match('|^https?://|', $api_url)) { $schema = rcube_utils::https_check() ? 'https' : 'http'; $port = $schema == 'http' ? 80 : 443; $url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']); if ($_SERVER['SERVER_PORT'] != $port && $_SERVER['SERVER_PORT'] != 80) { $url .= ':' . $_SERVER['SERVER_PORT']; } if ($api_url) { $api_url = $url . '/' . trim($api_url, '/ '); } else { $url .= preg_replace('/\/?\?.*$/', '', $_SERVER['REQUEST_URI']); $url = preg_replace('/\/api$/', '', $url); $api_url = $url . '/api'; } } return rtrim($api_url, '/ '); } /** * Returns web browser object * * @return rcube_browser Web browser object */ public function get_browser() { if ($this->browser === null) { $this->browser = new rcube_browser; } return $this->browser; } /** * Send success response * * @param mixed $data Data */ public function output_success($data) { if (!is_array($data)) { $data = array(); } $response = array('status' => 'OK', 'result' => $data); if (!empty($_REQUEST['req_id'])) { $response['req_id'] = $_REQUEST['req_id']; } $this->output_send($response); } /** * Send error response * * @param mixed $response Response data * @param int $code Error code */ public function output_error($response, $code = null) { if (is_string($response)) { $response = array('reason' => $response); } $response['status'] = 'ERROR'; if ($code) { $response['code'] = $code; } if (!empty($_REQUEST['req_id'])) { $response['req_id'] = $_REQUEST['req_id']; header("X-Chwala-Request-ID: " . $_REQUEST['req_id']); } if (empty($response['code'])) { $response['code'] = file_api_core::ERROR_CODE; } header("X-Chwala-Error: " . $response['code']); // When binary response is expected return real // HTTP error instaead of JSON response with code 200 if ($this->is_binary_request()) { header(sprintf("HTTP/1.0 %d %s", $response['code'], $response ?: "Server error")); exit; } $this->output_send($response); } /** * Send response * * @param mixed $data Data */ public function output_send($data = null) { // Send response if ($data !== null) { header("Content-Type: {$this->output_type}; charset=utf-8"); echo rcube_output::json_serialize($data); } exit; } /** * Find out if current request expects binary output */ protected function is_binary_request() { return $_SERVER['REQUEST_METHOD'] == 'GET' && ($this->request == 'file_get' || $this->request == 'document'); } /** * Returns API version supported by the client */ public function client_version() { return $_SESSION['version']; } /** * Create a human readable string for a number of bytes * * @param int Number of bytes * * @return string Byte string */ public function show_bytes($bytes) { if ($bytes >= 1073741824) { $gb = $bytes/1073741824; $str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . 'GB'; } else if ($bytes >= 1048576) { $mb = $bytes/1048576; $str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . 'MB'; } else if ($bytes >= 1024) { $str = sprintf("%d ", round($bytes/1024)) . 'KB'; } else { $str = sprintf('%d ', $bytes) . 'B'; } return $str; } } diff --git a/lib/file_api_core.php b/lib/file_api_core.php index 76ef511..40c258d 100644 --- a/lib/file_api_core.php +++ b/lib/file_api_core.php @@ -1,448 +1,448 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_core extends file_locale { const API_VERSION = 5; 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 ERROR_UNSUPPORTED = 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; protected $admin_drivers = array(); /** * 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 = 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'); // Disable webdav sources/drivers in iRony that point to the // same host to prevent infinite recursion $is_valid_source = function($source) { if ($source['driver'] == 'webdav') { $self_url = parse_url($_SERVER['SCRIPT_URI']); $item_url = parse_url($source['baseuri'] ?: $source['host']); $hosts = array($self_url['host'], $_SERVER['SERVER_NAME'], $_SERVER['SERVER_ADDR']); if (in_array($item_url['host'], $hosts)) { return false; } } return true; }; if (!empty($enabled)) { $drivers = $backend->driver_list(); if ($iRony) { $drivers = array_filter($drivers, $is_valid_source); } foreach ($drivers as $item) { $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($preconf)) { if ($iRony) { $preconf = array_filter($preconf, $is_valid_source); } foreach ($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)) { $rcube = rcube::get_instance(); if ($rcube->config->get('fileapi_backend_storage_disabled')) { throw new Exception("Failed to find a driver for specified folder/file.", self::ERROR_NOT_FOUND); } 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('VERSION' => self::API_VERSION); // 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 ($rcube->config->get('fileapi_backend_storage_disabled')) { $caps['NOROOT'] = true; } if (!$full) { return $caps; } - if ($caps['MANTICORE']) { + if ($caps['MANTICORE'] ?? false) { $manticore = new file_manticore($this); $caps['MANTICORE_EDITABLE'] = $manticore->supported_filetypes(true); } - if ($caps['WOPI']) { + if ($caps['WOPI'] ?? false) { $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|null 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; } }