diff --git a/lib/api/common.php b/lib/api/common.php index 69d2fc7..b60bc66 100644 --- a/lib/api/common.php +++ b/lib/api/common.php @@ -1,166 +1,182 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_common { protected $api; protected $rc; protected $args = array(); public function __construct($api) { $this->rc = rcube::get_instance(); $this->api = $api; } /** * Request handler */ public function handle() { // GET arguments $this->args = &$_GET; // POST arguments (JSON) if ($_SERVER['REQUEST_METHOD'] == 'POST') { $post = file_get_contents('php://input'); $this->args += (array) json_decode($post, true); unset($post); } // disable script execution time limit, so we can handle big files @set_time_limit(360); } /** * File uploads handler */ protected function upload() { $files = array(); if (is_array($_FILES['file']['tmp_name'])) { foreach ($_FILES['file']['tmp_name'] as $i => $filepath) { if ($err = $_FILES['file']['error'][$i]) { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $maxsize = ini_get('upload_max_filesize'); $maxsize = $this->show_bytes(parse_bytes($maxsize)); throw new Exception("Maximum file size ($maxsize) exceeded", file_api_core::ERROR_CODE); } throw new Exception("File upload failed", file_api_core::ERROR_CODE); } $files[] = array( 'path' => $filepath, 'name' => $_FILES['file']['name'][$i], 'size' => filesize($filepath), 'type' => rcube_mime::file_content_type($filepath, $_FILES['file']['name'][$i], $_FILES['file']['type']), ); } } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { // if filesize exceeds post_max_size then $_FILES array is empty, if ($maxsize = ini_get('post_max_size')) { $maxsize = $this->show_bytes(parse_bytes($maxsize)); throw new Exception("Maximum file size ($maxsize) exceeded", file_api_core::ERROR_CODE); } throw new Exception("File upload failed", file_api_core::ERROR_CODE); } return $files; } /** * Return built-in viewer opbject for specified mimetype * * @return object Viewer object */ protected function find_viewer($mimetype) { $dir = RCUBE_INSTALL_PATH . 'lib/viewers'; if ($handle = opendir($dir)) { while (false !== ($file = readdir($handle))) { if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) { include_once $dir . '/' . $file; $class = 'file_viewer_' . $matches[1]; $viewer = new $class($this->api); if ($viewer->supports($mimetype)) { return $viewer; } } } closedir($handle); } } /** * Parse driver metadata information */ protected function parse_metadata($metadata, $default = false) { if ($default) { unset($metadata['form']); $metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')'; } // localize form labels foreach ($metadata['form'] as $key => $val) { $label = $this->api->translate('form.' . $val); if (strpos($label, 'form.') !== 0) { $metadata['form'][$key] = $label; } } return $metadata; } /** * Get folder rights */ protected function folder_rights($folder) { list($driver, $path) = $this->api->get_driver($folder); $rights = $driver->folder_rights($path); $result = array(); $map = array( file_storage::ACL_READ => 'read', file_storage::ACL_WRITE => 'write', ); foreach ($map as $key => $value) { if ($rights & $key) { $result[] = $value; } } return $result; } + + /** + * Update manticore session on file/folder move + */ + protected function session_uri_update($from, $to, $is_folder = false) + { + // 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); + if (empty($capabilities['MANTICORE'])) { + return; + } + + $manticore = new file_manticore($this->api); + $manticore->session_uri_update($from, $to, $is_folder); + } } diff --git a/lib/api/file_move.php b/lib/api/file_move.php index 6b89bc7..ce2ba10 100644 --- a/lib/api/file_move.php +++ b/lib/api/file_move.php @@ -1,158 +1,163 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_move 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 (is_array($this->args['file'])) { if (empty($this->args['file'])) { throw new Exception("Missing file name", file_api_core::ERROR_CODE); } } else { if (!isset($this->args['new']) || $this->args['new'] === '') { throw new Exception("Missing new file name", file_api_core::ERROR_CODE); } $this->args['file'] = array($this->args['file'] => $this->args['new']); } $overwrite = rcube_utils::get_boolean((string) $this->args['overwrite']); $request = $this instanceof file_api_file_copy ? 'file_copy' : 'file_move'; $errors = array(); foreach ((array) $this->args['file'] as $file => $new_file) { if ($new_file === '') { throw new Exception("Missing new file name", file_api_core::ERROR_CODE); } if ($new_file === $file) { throw new Exception("Old and new file name is the same", file_api_core::ERROR_CODE); } list($driver, $path) = $this->api->get_driver($file); list($new_driver, $new_path) = $this->api->get_driver($new_file); try { // source and destination on the same driver... if ($driver == $new_driver) { $driver->{$request}($path, $new_path); } // cross-driver move/copy... else { // first check if destination file exists $info = null; try { $info = $new_driver->file_info($new_path); } catch (Exception $e) { } if (!empty($info)) { throw new Exception("File exists", file_storage::ERROR_FILE_EXISTS); } // copy/move between backends $this->file_copy($driver, $new_driver, $path, $new_path, $request == 'file_move'); } } catch (Exception $e) { if ($e->getCode() == file_storage::ERROR_FILE_EXISTS) { // delete existing file and do copy/move again if ($overwrite) { $new_driver->file_delete($new_path); if ($driver == $new_driver) { $driver->{$request}($path, $new_path); } else { $this->file_copy($driver, $new_driver, $path, $new_path, $request == 'file_move'); } } // collect file-exists errors, so the client can ask a user // what to do and skip or replace file(s) else { $errors[] = array( 'src' => $file, 'dst' => $new_file, ); } } else { throw $e; } } + + // Update manticore sessions + if ($request == 'file_move') { + $this->session_uri_update($file, $new_file, false); + } } if (!empty($errors)) { return array('already_exist' => $errors); } } /** * File copy/move between storage backends */ protected function file_copy($driver, $new_driver, $path, $new_path, $move = false) { // unable to put file on mount point if (strpos($new_path, file_storage::SEPARATOR) === false) { throw new Exception("Unable to copy/move file into specified location", file_api_core::ERROR_CODE); } // get the file from source location $fp = fopen('php://temp', 'w+'); if (!$fp) { throw new Exception("Internal server error", file_api_core::ERROR_CODE); } $driver->file_get($path, null, $fp); rewind($fp); $chunk = stream_get_contents($fp, 102400); $type = rcube_mime::file_content_type($chunk, $new_path, 'application/octet-stream', true); rewind($fp); // upload the file to new location $new_driver->file_create($new_path, array('content' => $fp, 'type' => $type)); fclose($fp); // now we can remove the original file if it was a move action if ($move) { $driver->file_delete($path); } } } diff --git a/lib/api/folder_move.php b/lib/api/folder_move.php index a06a05a..87bd546 100644 --- a/lib/api/folder_move.php +++ b/lib/api/folder_move.php @@ -1,183 +1,187 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_folder_move 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); } if (!isset($this->args['new']) || $this->args['new'] === '') { throw new Exception("Missing destination folder name", file_api_core::ERROR_CODE); } if ($this->args['new'] === $this->args['folder']) { return; } list($src_driver, $src_path, $cfg) = $this->api->get_driver($this->args['folder']); list($dst_driver, $dst_path) = $this->api->get_driver($this->args['new']); // source folder is a mount point (driver title)... if ($src_driver->title() === $this->args['folder']) { // ...rename if (strpos($this->args['new'], file_storage::SEPARATOR) === false) { // destination folder is an existing mount point if (!strlen($dst_path)) { throw new Exception("Destination folder already exists", file_api_core::ERROR_CODE); } return $this->driver_rename($src_driver, $this->args['new'], $cfg); } throw new Exception("Unsupported operation", file_api_core::ERROR_CODE); } // cross-driver move if ($src_driver != $dst_driver) { // destination folder is an existing mount point if (!strlen($dst_path)) { throw new Exception("Destination folder already exists", file_api_core::ERROR_CODE); } - return $this->folder_move_to_other_driver($src_driver, $src_path, $dst_driver, $dst_path); + $this->folder_move_to_other_driver($src_driver, $src_path, $dst_driver, $dst_path); + } + else { + $src_driver->folder_move($src_path, $dst_path); } - return $src_driver->folder_move($src_path, $dst_path); + // Update manticore + $this->session_uri_update($this->args['folder'], $this->args['new'], true); } /** * Move folder between two external storage locations */ protected function folder_move_to_other_driver($src_driver, $src_path, $dst_driver, $dst_path) { $src_folders = $src_driver->folder_list(); $dst_folders = $dst_driver->folder_list(); // first check if destination folder not exists if (in_array($dst_path, $dst_folders)) { throw new Exception("Destination folder already exists", file_api_core::ERROR_CODE); } // now recursively create/delete folders and copy their content $this->move_folder_with_content($src_driver, $src_path, $dst_driver, $dst_path, $src_folders); // now we can delete the folder $src_driver->folder_delete($src_path); } /** * Recursively moves folder and it's content to another location */ protected function move_folder_with_content($src_driver, $src_path, $dst_driver, $dst_path, $src_folders) { // create folder $dst_driver->folder_create($dst_path); foreach ($src_driver->file_list($src_path) as $filename => $file) { $this->file_copy($src_driver, $dst_driver, $filename, $dst_path . file_storage::SEPARATOR . $file['name']); } // sub-folders... foreach ($src_folders as $folder) { if (strpos($folder, $src_path . file_storage::SEPARATOR) === 0 && strpos($folder, file_storage::SEPARATOR, strlen($src_path) + 2) === false ) { $destination = $dst_path . file_storage::SEPARATOR . substr($folder, strlen($src_path) + 1); $this->move_folder_with_content($src_driver, $folder, $dst_driver, $destination, $src_folders); } } } /** * File move between storage backends */ protected function file_copy($src_driver, $dst_driver, $src_path, $dst_path) { // unable to put file on mount point if (strpos($dst_path, file_storage::SEPARATOR) === false) { throw new Exception("Unable to move file into specified location", file_api_core::ERROR_CODE); } // get the file from source location $fp = fopen('php://temp', 'w+'); if (!$fp) { throw new Exception("Internal server error", file_api_core::ERROR_CODE); } $src_driver->file_get($src_path, null, $fp); rewind($fp); $chunk = stream_get_contents($fp, 102400); $type = rcube_mime::file_content_type($chunk, $dst_path, 'application/octet-stream', true); rewind($fp); // upload the file to new location $dst_driver->file_create($dst_path, array('content' => $fp, 'type' => $type)); fclose($fp); } /** * External storage (mount point) rename */ protected function driver_rename($driver, $new_name, $config) { $backend = $this->api->get_backend(); $folders = $backend->folder_list(); // first check if destination folder not exists if (in_array($new_name, $folders)) { throw new Exception("Destination folder already exists", file_api_core::ERROR_CODE); } $title = $driver->title(); $config['title'] = $new_name; // store passwords if it was already stored if (empty($config['password']) || $config['password'] != '%u') { unset($config['password']); } $backend->driver_update($title, $config); // authenticate again to set session password if (empty($config['password']) || $config['password'] != '%u') { $auth = $driver->auth_info(); list($driver) = $this->api->get_driver($new_name); $driver->authenticate($auth['username'], $auth['password']); } } } diff --git a/lib/file_manticore.php b/lib/file_manticore.php index 18dfb2f..bb4af34 100644 --- a/lib/file_manticore.php +++ b/lib/file_manticore.php @@ -1,826 +1,860 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Document editing sessions handling */ class file_manticore { protected $api; protected $rc; protected $request; protected $user; protected $sessions_table = 'chwala_sessions'; protected $invitations_table = 'chwala_invitations'; protected $icache = array(); const STATUS_INVITED = 'invited'; const STATUS_REQUESTED = 'requested'; const STATUS_ACCEPTED = 'accepted'; const STATUS_DECLINED = 'declined'; const STATUS_DECLINED_OWNER = 'declined-owner'; // same as 'declined' but done by the session owner const STATUS_ACCEPTED_OWNER = 'accepted-owner'; // same as 'accepted' but done by the session owner /** * Class constructor * * @param file_api Chwala API app instance */ public function __construct($api) { $this->rc = rcube::get_instance(); $this->api = $api; $this->user = $_SESSION['user']; $db = $this->rc->get_dbh(); $this->sessions_table = $db->table_name($this->sessions_table); $this->invitations_table = $db->table_name($this->invitations_table); } /** * Return viewer URI for specified file/session. This creates * a new collaborative editing session when needed. * * @param string $file File path * @param string &$session_id Optional session ID to join to * * @return string Manticore URI * @throws Exception */ public function session_start($file, &$session_id = null) { if ($file !== null) { - list($driver, $path) = $this->api->get_driver($file); - $uri = $driver->path2uri($path); + $uri = $this->path2uri($file, $driver); } $backend = $this->api->get_backend(); if ($session_id) { $session = $this->session_info($session_id); if (empty($session)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } // check session ownership if ($session['owner'] != $this->user) { // check if the user was invited $invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $this->user)); $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER); if (empty($invitations) || !in_array($invitations[0]['status'], $states)) { throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE); } // automatically accept the invitation, if not done yet if ($invitations[0]['status'] == self::STATUS_INVITED) { $this->invitation_update($session_id, $this->user, self::STATUS_ACCEPTED); } } // authenticate to Manticore, we need auth token for frame_uri $req = $this->get_request(); // @TODO: make sure the session exists in Manticore? } else if (!empty($uri)) { // To prevent from creating new sessions for the same file+user // (e.g. when user uses F5 to refresh the page), we check first // if such a session exist and continue with it $db = $this->rc->get_dbh(); $res = $db->query("SELECT `id` FROM `{$this->sessions_table}`" . " WHERE `owner` = ? AND `uri` = ?", $this->user, $uri); if ($row = $db->fetch_assoc($res)) { $session_id = $row['id']; $res = true; } else if (!$db->is_error($res)) { $session_id = rcube_utils::bin2ascii(md5(time() . $uri, true)); $data = array(); $owner = $this->user; // we'll store user credentials if the file comes from // an external source that requires authentication if ($backend != $driver) { $auth = $driver->auth_info(); $auth['password'] = $this->rc->encrypt($auth['password']); $data['auth_info'] = $auth; } $res = $this->session_create($session_id, $uri, $owner, $data); } if (!$res) { throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE); } } else { throw new Exception("Failed creating document editing session (unknown file)", file_api_core::ERROR_CODE); } return $this->frame_uri($session_id); } /** * Get file path (not URI) from session. * * @param string $id Session ID * @param bool $join_mode Throw exception only if session does not exist * * @return string File path * @throws Exception */ public function session_file($id, $join_mode = false) { $session = $this->session_info($id); if (empty($session)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } $path = $this->uri2path($session['uri']); if (empty($path) && (!$join_mode || $session['owner'] == $this->user)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } // check permissions to the session if ($session['owner'] != $this->user) { $invitations = $this->invitations_find(array('session_id' => $id, 'user' => $this->user)); $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER); if (empty($invitations) || !in_array($invitations[0]['status'], $states)) { throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE); } } return $path; } /** * Get editing session info * * @param string $id Session identifier * @param bool $with_invitations Return invitations list * * @return array Session data */ public function session_info($id, $with_invitations = false) { $session = $this->icache["session:$id"]; if (!$session) { $db = $this->rc->get_dbh(); $result = $db->query("SELECT * FROM `{$this->sessions_table}`" . " WHERE `id` = ?", $id); if ($row = $db->fetch_assoc($result)) { $session = $this->session_info_parse($row); $this->icache["session:$id"] = $session; } } if ($session) { if ($session['owner'] == $this->user) { $session['is_owner'] = true; } if ($with_invitations && $session['is_owner']) { $session['invitations'] = $this->invitations_find(array('session_id' => $id)); } } return $session; } /** * Find editing sessions for specified path */ public function session_find($path, $invitations = true) { // create an URI for specified path - list($driver, $path) = $this->api->get_driver($path); - - $uri = trim($driver->path2uri($path), '/') . '/'; + $uri = trim($this->path2uri($path), '/') . '/'; // get existing sessions $sessions = array(); $filter = array('file', 'owner', 'owner_name', 'is_owner'); $db = $this->rc->get_dbh(); $result = $db->query("SELECT * FROM `{$this->sessions_table}`" . " WHERE `uri` LIKE '" . $db->escape($uri) . "%'"); while ($row = $db->fetch_assoc($result)) { if ($path = $this->uri2path($row['uri'])) { $sessions[$row['id']] = $this->session_info_parse($row, $path, $filter); } } // set 'is_invited' flag if ($invitations && !empty($sessions)) { $invitations = $this->invitations_find(array('user' => $this->user)); $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER); foreach ($invitations as $invitation) { if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) { $sessions[$invitation['session_id']]['is_invited'] = true; } } } return $sessions; } /** * Delete editing session (only owner can do that) * * @param string $id Session identifier * @param bool $local Remove session only from local database */ public function session_delete($id, $local = false) { $db = $this->rc->get_dbh(); $result = $db->query("DELETE FROM `{$this->sessions_table}`" . " WHERE `id` = ? AND `owner` = ?", $id, $this->user); $success = $db->affected_rows($result) > 0; // Send document delete to Manticore if ($success && !$local) { $req = $this->get_request(); $res = $req->document_delete($id); } return $success; } /** * Create editing session */ protected function session_create($id, $uri, $owner, $data) { // get user name $owner_name = $this->api->resolve_user($owner) ?: ''; // Do this before starting the session in Manticore, // it will immediately call api/document to get the file body $db = $this->rc->get_dbh(); $result = $db->query("INSERT INTO `{$this->sessions_table}`" . " (`id`, `uri`, `owner`, `owner_name`, `data`)" . " VALUES (?, ?, ?, ?, ?)", $id, $uri, $owner, $owner_name, json_encode($data)); $success = $db->affected_rows($result) > 0; // create the session in Manticore if ($success) { $req = $this->get_request(); $res = $req->document_create(array( 'id' => $id, 'title' => '', // @TODO: maybe set to a file path without extension? 'access' => array( array( 'identity' => $owner, 'permission' => file_manticore_api::ACCESS_WRITE, ), ), )); if (!$res) { $this->session_delete($id, true); return false; } } return $success; } /** * Find sessions, including: * 1. to which the user has access (is a creator or has been invited) * 2. to which the user is considered eligible to request authorization * to participate in the session by already having access to the file * * @param array $param List parameters * * @return array Sessions list */ public function sessions_list($param = array()) { $db = $this->rc->get_dbh(); $sessions = array(); // 1. Get sessions user has access to $result = $db->query("SELECT s.`id`, s.`uri`, s.`owner`, s.`owner_name`" . " FROM `{$this->sessions_table}` s" . " WHERE s.`owner` = ? OR s.`id` IN (" . "SELECT i.`session_id` FROM `{$this->invitations_table}` i" . " WHERE i.`user` = ?" . ")", $this->user, $this->user); if ($db->is_error($result)) { throw new Exception("Internal error.", file_api_core::ERROR_CODE); } while ($row = $db->fetch_assoc($result)) { if ($path = $this->uri2path($row['uri'], true)) { $sessions[$row['id']] = $this->session_info_parse($row, $path); // For performance reasons we don't want to fetch info of every file // on the list. As we support only ODT files here... $sessions[$row['id']]['type'] = 'application/vnd.oasis.opendocument.text'; } } - // 2. Get sessions user is eligible by access to the file + // 2. Get sessions user is eligible // - get list of all folder URIs and find sessions for files in these locations // @FIXME: in corner cases (user has many folders) this may produce a big query, // maybe fetching all sessions and then comparing with list of locations would be faster? $uris = $this->all_folder_locations(); $where = array_map(function($uri) use ($db) { return 's.`uri` LIKE ' . $db->quote(str_replace('%', '_', $uri) . '/%'); }, $uris); $result = $db->query("SELECT s.`id`, s.`uri`, s.`owner`, s.`owner_name`" . " FROM `{$this->sessions_table}` s WHERE " . join(' OR ', $where)); if ($db->is_error($result)) { throw new Exception("Internal error.", file_api_core::ERROR_CODE); } while ($row = $db->fetch_assoc($result)) { if (empty($sessions[$row['id']])) { // remove filename (and anything after it) so we have the folder URI // to check if it's on the folders list we have $uri = substr($row['uri'], 0, strrpos($row['uri'], '/')); if (in_array($uri, $uris) && ($path = $this->uri2path($row['uri'], true))) { $sessions[$row['id']] = $this->session_info_parse($row, $path); // For performance reasons we don't want to fetch info of every file // on the list. As we support only ODT files here... $sessions[$row['id']]['type'] = 'application/vnd.oasis.opendocument.text'; } } } // set 'is_invited' flag if (!empty($sessions)) { $invitations = $this->invitations_find(array('user' => $this->user)); $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER); foreach ($invitations as $invitation) { if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) { $sessions[$invitation['session_id']]['is_invited'] = true; } } } // Sorting $sort = !empty($params['sort']) ? $params['sort'] : 'name'; $index = array(); if (in_array($sort, array('name', 'file', 'owner'))) { foreach ($sessions as $key => $val) { if ($sort == 'name' || $sort == 'file') { $path = explode(file_storage::SEPARATOR, $val['file']); $index[$key] = $path[count($path) - 1]; continue; } $index[$key] = $val[$sort]; } array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $sessions); } if ($params['reverse']) { $sessions = array_reverse($sessions, true); } return $sessions; } /** * Find invitations for current user. This will return all * invitations related to the user including his sessions. * * @param array $filter Search filter (see self::invitations_find()) * * @return array Invitations list */ public function invitations_list($filter = array()) { $filter['user'] = $this->user; // list of invitations to the user or requested by him $result = $this->invitations_find($filter, true); unset($filter['user']); $filter['owner'] = $this->user; // other invitations that belong to the sessions owned by the user if ($other = $this->invitations_find($filter, true)) { $result = array_merge($result, $other); } return $result; } /** * Find invitations for specified filter * * @param array $filter Search filter (see self::invitations_find()) * - session_id: session identifier * - timestamp: "changed > ?" filter * - user: Invitation user identifier * - owner: Session owner identifier * @param bool $extended Return session file names * * @return array Invitations list */ public function invitations_find($filter, $extended = false) { $db = $this->rc->get_dbh(); $query = ''; $select = "i.*"; foreach ($filter as $column => $value) { if ($column == 'timestamp') { $where[] = "i.`changed` > " . $db->fromunixtime($value); } else if ($column == 'owner') { $join[] = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)"; $where[] = "s.`owner` = " . $db->quote($value); } else { $where[] = "i.`$column` = " . $db->quote($value); } } if ($extended) { $select .= ", s.`uri`, s.`owner`, s.`owner_name`"; $join[] = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)"; } if (!empty($join)) { $query .= ' JOIN ' . implode(' JOIN ', array_unique($join)); } if (!empty($where)) { $query .= ' WHERE ' . implode(' AND ', array_unique($where)); } $result = $db->query("SELECT $select FROM `{$this->invitations_table}` i" . "$query ORDER BY i.`changed`"); if ($db->is_error($result)) { throw new Exception("Internal error.", file_api_core::ERROR_CODE); } $invitations = array(); while ($row = $db->fetch_assoc($result)) { if ($extended) { try { // add unix-timestamp of the `changed` date to the result $dt = new DateTime($row['changed']); $row['timestamp'] = $dt->format('U'); } catch(Exception $e) { } // add filename to the result $filename = parse_url($row['uri'], PHP_URL_PATH); $filename = pathinfo($filename, PATHINFO_BASENAME); $filename = rawurldecode($filename); $row['filename'] = $filename; if ($path = $this->uri2path($row['uri'])) { $row['file'] = $path; } unset($row['uri']); } $invitations[] = $row; } return $invitations; } /** * Create an invitation * * @param string $session_id Document session identifier * @param string $user User identifier (use null for current user) * @param string $status Invitation status (invited, requested) * @param string $comment Invitation description/comment * @param string &$user_name Optional user name * * @throws Exception */ public function invitation_create($session_id, $user, $status = 'invited', $comment = '', &$user_name = '') { if (empty($user)) { $user = $this->user; } if ($status != self::STATUS_INVITED && $status != self::STATUS_REQUESTED) { throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE); } // get session information $session = $this->session_info($session_id); if (empty($session)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } // check session ownership, only owner can create 'new' invitations if ($status == self::STATUS_INVITED && $session['owner'] != $this->user) { throw new Exception("No permission to create an invitation.", file_api_core::ERROR_CODE); } if ($session['owner'] == $user) { throw new Exception("Not possible to create an invitation for the session creator.", file_api_core::ERROR_CODE); } // Update Manticore 'access' array if ($status == self::STATUS_INVITED) { $req = $this->get_request(); $res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE); if (!$res) { throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE); } } // get user name if (empty($user_name)) { $user_name = $this->api->resolve_user($user) ?: ''; } // insert invitation $db = $this->rc->get_dbh(); $result = $db->query("INSERT INTO `{$this->invitations_table}`" . " (`session_id`, `user`, `user_name`, `status`, `comment`, `changed`)" . " VALUES (?, ?, ?, ?, ?, " . $db->now() . ")", $session_id, $user, $user_name, $status, $comment ?: ''); if (!$db->affected_rows($result)) { throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE); } } /** * Delete an invitation (only session owner can do that) * * @param string $session_id Session identifier * @param string $user User identifier * * @throws Exception */ public function invitation_delete($session_id, $user) { $db = $this->rc->get_dbh(); $result = $db->query("DELETE FROM `{$this->invitations_table}`" . " WHERE `session_id` = ? AND `user` = ?" . " AND EXISTS (SELECT 1 FROM `{$this->sessions_table}` WHERE `id` = ? AND `owner` = ?)", $session_id, $user, $session_id, $this->user); if (!$db->affected_rows($result)) { throw new Exception("Failed to delete an invitation.", file_api_core::ERROR_CODE); } // Update Manticore 'access' array $req = $this->get_request(); $res = $req->editor_delete($session_id, $user); if (!$res) { throw new Exception("Failed to remove an invitation.", file_api_core::ERROR_CODE); } } /** * Update an invitation status * * @param string $session_id Session identifier * @param string $user User identifier (use null for current user) * @param string $status Invitation status (accepted, declined) * @param string $comment Invitation description/comment * * @throws Exception */ public function invitation_update($session_id, $user, $status, $comment = '') { if (empty($user)) { $user = $this->user; } if ($status != self::STATUS_ACCEPTED && $status != self::STATUS_DECLINED) { throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE); } // get session information $session = $this->session_info($session_id); if (empty($session)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } // check session ownership if ($user != $this->user && $session['owner'] != $this->user) { throw new Exception("No permission to update an invitation.", file_api_core::ERROR_CODE); } if ($session['owner'] == $this->user) { $status = $status . '-owner'; } $db = $this->rc->get_dbh(); $result = $db->query("UPDATE `{$this->invitations_table}`" . " SET `status` = ?, `comment` = ?, `changed` = " . $db->now() . " WHERE `session_id` = ? AND `user` = ?", $status, $comment ?: '', $session_id, $user); if (!$db->affected_rows($result)) { throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE); } // Update Manticore 'access' array if an owner accepted an invitation request if ($status == self::STATUS_ACCEPTED_OWNER) { $req = $this->get_request(); $res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE); if (!$res) { throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE); } } } + /** + * Update a session URI (e.g. on file/folder move) + * + * @param string $from Source file/folder path + * @param string $to Destination file/folder path + * @param bool $is_folder True if the path is a folder + */ + public function session_uri_update($from, $to, $is_folder = false) + { + $db = $this->rc->get_dbh(); + + // Resolve paths + $from = $this->path2uri($from); + $to = $this->path2uri($to); + + if ($is_folder) { + $set = "`uri` = REPLACE(`uri`, " . $db->quote($from . '/') . ", " . $db->quote($to .'/') . ")"; + $where = "`uri` LIKE " . $db->quote(str_replace('%', '_', $from) . '/%'); + } + else { + $set = "`uri` = " . $db->quote($to); + $where = "`uri` = " . $db->quote($from); + } + + $db->query("UPDATE `{$this->sessions_table}` SET $set WHERE $where"); + } + /** * Parse session info data */ protected function session_info_parse($record, $path = null, $filter = array()) { $session = array(); $fields = array('id', 'uri', 'owner', 'owner_name'); foreach ($fields as $field) { if (isset($record[$field])) { $session[$field] = $record[$field]; } } if ($path) { $session['file'] = $path; } // @TODO: is_invited?, last_modified? if ($session['owner'] == $this->user) { $session['is_owner'] = true; } if (!empty($filter)) { $session = array_intersect_key($session, array_flip($filter)); } return $session; } /** * Generate URI of Manticore editing session */ protected function frame_uri($id) { $base_url = rtrim($this->rc->config->get('fileapi_manticore'), ' /'); return $base_url . '/document/' . $id . '/' . $_SESSION['manticore_token']; } + /** + * Get file URI from path + */ + protected function path2uri($path, &$driver = null) + { + list($driver, $path) = $this->api->get_driver($path); + + return $driver->path2uri($path); + } + /** * Get file path from the URI */ protected function uri2path($uri, $use_fallback = false) { $backend = $this->api->get_backend(); try { return $backend->uri2path($uri); } catch (Exception $e) { // do nothing } foreach ($this->api->get_drivers(true) as $driver) { try { $path = $driver->uri2path($uri); $title = $driver->title(); if ($title) { $path = $title . file_storage::SEPARATOR . $path; } return $path; } catch (Exception $e) { // do nothing } } // likely user has no access to the file, but has been invited, // extract filename from the URI if ($use_fallback && $uri) { $path = parse_url($uri, PHP_URL_PATH); $path = explode('/', $path); $path = $path[count($path) - 1]; return $path; } } /** * Initialize Manticore API request handler */ protected function get_request() { if (!$this->request) { $uri = rcube_utils::resolve_url($this->rc->config->get('fileapi_manticore')); $this->request = new file_manticore_api($uri); // Use stored session token, check if it's still valid if ($_SESSION['manticore_token']) { $is_valid = $this->request->set_session_token($_SESSION['manticore_token'], true); if ($is_valid) { return $this->request; } } $backend = $this->api->get_backend(); $auth = $backend->auth_info(); $_SESSION['manticore_token'] = $this->request->login($auth['username'], $auth['password']); if (empty($_SESSION['manticore_token'])) { throw new Exception("Unable to login to Manticore server.", file_api_core::ERROR_CODE); } } return $this->request; } /** * Get URI of all user folders (with shared locations) */ protected function all_folder_locations() { $locations = array(); foreach (array_merge(array($this->api->get_backend()), $this->api->get_drivers(true)) as $driver) { // Performance optimization: We're interested here in shared folders, // Kolab is the only driver that currently supports them, ignore others if (get_class($driver) != 'kolab_file_storage') { continue; } try { foreach ($driver->folder_list() as $folder) { if ($uri = $driver->path2uri($folder)) { $locations[] = $uri; } } } catch (Exception $e) { // do nothing } } return $locations; } }