diff --git a/lib/drivers/seafile/seafile_api.php b/lib/drivers/seafile/seafile_api.php index d371dac..0f14a21 100644 --- a/lib/drivers/seafile/seafile_api.php +++ b/lib/drivers/seafile/seafile_api.php @@ -1,1407 +1,1404 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Class implementing access via SeaFile Web API v2 */ class seafile_api { const STATUS_OK = 200; const CREATED = 201; const ACCEPTED = 202; const MOVED_PERMANENTLY = 301; const BAD_REQUEST = 400; const FORBIDDEN = 403; const NOT_FOUND = 404; const CONFLICT = 409; const TOO_MANY_REQUESTS = 429; const REPO_PASSWD_REQUIRED = 440; const REPO_PASSWD_MAGIC_REQUIRED = 441; const INTERNAL_SERVER_ERROR = 500; const OPERATION_FAILED = 520; const CONNECTION_ERROR = 550; private $status = null; /** * Specifies how long max. we'll wait and renew throttled request (in seconds) */ const WAIT_LIMIT = 30; /** * Configuration * * @var array */ protected $config = array(); /** * HTTP request handle * * @var HTTP_Request */ protected $request; /** * Web API URI prefix * * @var string */ protected $url; /** * Session token * * @var string */ protected $token; /** * API URL prefix (schema and host[:port]) * * @var string */ protected $url_prefix; public function __construct($config = array()) { $this->config = $config; $this->token = $config['token']; // set Web API URI $this->url = rtrim(trim($config['host']), '/') ?: 'localhost'; if (!preg_match('|^https?://|i', $this->url)) { $this->url = 'https://' . $this->url; } if (!preg_match('|/api2$|', $this->url)) { $this->url .= '/api2/'; } $this->url_prefix = preg_replace('|^(https?://[^/]+).*$|i', '\\1', $this->url); } /** * * @param array Configuration for this Request instance, that will be merged * with default configuration * * @return HTTP_Request2 Request object */ public static function http_request($config = array()) { - // load HTTP_Request2 - require_once 'HTTP/Request2.php'; - // remove unknown config, otherwise HTTP_Request will throw an error $config = array_intersect_key($config, array_flip(array( 'connect_timeout', 'timeout', 'use_brackets', 'protocol_version', 'buffer_size', 'store_body', 'follow_redirects', 'max_redirects', 'strict_redirects', 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase' ))); // force CURL adapter, this allows to handle correctly // compressed responses with simple SplObserver registered $config['adapter'] = 'HTTP_Request2_Adapter_Curl'; try { $request = new HTTP_Request2(); $request->setConfig($config); } catch (Exception $e) { rcube::raise_error($e, true, false); return; } return $request; } /** * Send HTTP request * * @param string $method Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT') * @param string $url Request API URL * @param array $get GET parameters * @param array $post POST parameters * @param array $upload Uploaded files data * @param string $version API version (to replace "api2" with "api/v$version" in the URL * * @return string|array Server response */ protected function request($method, $url, $get = null, $post = null, $upload = null, $version = null) { if (!preg_match('/^https?:\/\//', $url)) { $url = $this->url . $url; // Note: It didn't work for me without the last backslash $url = rtrim($url, '/') . '/'; } else { $url = $this->mod_url($url); } if ($version && $version != 2) { $url = str_replace('/api2/', "/api/v$version/", $url); } if (!$this->request) { $this->config['store_body'] = true; // some methods respond with 301 redirect, we'll not follow them // also because of https://github.com/haiwen/seahub/issues/288 $this->config['follow_redirects'] = false; $this->request = self::http_request($this->config); if (!$this->request) { $this->status = self::CONNECTION_ERROR; return; } } // cleanup try { $this->request->setBody(''); $this->request->setUrl($url); } catch (Exception $e) { rcube::raise_error($e, true, false); $this->status = self::CONNECTION_ERROR; return; } if ($this->config['debug']) { $log_line = "SeaFile $method: $url"; $json_opt = PHP_VERSION_ID >= 50400 ? JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE : 0; if (!empty($get)) { $log_line .= ", GET: " . @json_encode($get, $json_opt); } if (!empty($post)) { $log_line .= ", POST: " . preg_replace('/("password":)[^\},]+/', '\\1"*"', @json_encode($post, $json_opt)); } if (!empty($upload)) { $log_line .= ", Files: " . @json_encode(array_keys($upload), $json_opt); } rcube::write_log('console', $log_line); } $this->request->setMethod($method ?: HTTP_Request2::METHOD_GET); if (!empty($get)) { $_url = $this->request->getUrl(); $_url->setQueryVariables($get); $this->request->setUrl($_url); } // HTTP_Request2 does not support POST params on PUT requests if (!empty($post) && $method == 'PUT') { $body = http_build_query($post, '', '&'); $body = str_replace('%7E', '~', $body); // support RFC 3986 by not encoding '~' symbol $post = null; $this->request->setBody($body); $this->request->setHeader('content-type', 'application/x-www-form-urlencoded'); } if (!empty($post)) { $this->request->addPostParameter($post); } if (!empty($upload)) { foreach ($upload as $field_name => $file) { $this->request->addUpload($field_name, $file['data'], $file['name'], $file['type']); } } if ($this->token) { $this->request->setHeader('Authorization', "Token " . $this->token); } // some HTTP server configurations require this header $this->request->setHeader('Accept', "application/json,text/javascript,*/*"); // proxy User-Agent string $this->request->setHeader('User-Agent', $_SERVER['HTTP_USER_AGENT']); // send request to the SeaFile API server try { $response = $this->request->send(); $this->status = $response->getStatus(); $body = $response->getBody(); } catch (Exception $e) { rcube::raise_error($e, true, false); $this->status = self::CONNECTION_ERROR; } if ($this->config['debug']) { rcube::write_log('console', "SeaFile Response [$this->status]: " . trim($body)); } // request throttled, try again if ($this->status == self::TOO_MANY_REQUESTS) { if (preg_match('/([0-9]+) second/', $body, $m) && ($seconds = $m[1]) < self::WAIT_LIMIT) { sleep($seconds); return $this->request($method, $url, $get, $post, $upload); } } if ($this->status < 400) { $body = @json_decode($body, true); } // Sometimes we can get an error with response code 200 if ($this->status == 200 && ($method == 'PUT' || $method == 'POST') && !empty($body) && is_array($body) && !empty($body['failed']) && empty($body['success']) ) { // Note: $body['failed'] is an array where each element is an array with 'error_msg' item // TODO: Support partial success/error result $this->status = 520; } // decode response return $this->status >= 400 ? false : $body; } /** * Return error code of last operation */ public function is_error() { return $this->status >= 400 ? $this->status : false; } /** * Authenticate to SeaFile API and get auth token * * @param string $username User name (email) * @param string $password User password * * @return string|null Authentication token */ public function authenticate($username, $password) { // sanity checks if ($username === '' || !is_string($username) || $password === '' || !is_string($password)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', 'auth-token', null, array( 'username' => $username, 'password' => $password, )); if ($result['token']) { return $this->token = $result['token']; } } /** * Get account information * * @return array Account info (usage, total, email) */ public function account_info() { return $this->request('GET', "account/info"); } /** * Delete a directory * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool True on success, False on failure */ public function directory_delete($repo_id, $dir) { // sanity checks if ($dir === '' || $dir === '/' || !is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id/dir", array('p' => $dir)); return $this->is_error() === false; } /** * Rename a directory * * @param string $repo_id Library identifier * @param string $src_dir Directory name (with path) * @param string $dest_dir New directory name (with path) * * @return bool True on success, False on failure */ public function directory_rename($repo_id, $src_dir, $dest_dir) { // sanity checks if ($src_dir === '' || $src_dir === '/' || !is_string($src_dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dest_dir === '' || $dest_dir === '/' || !is_string($dest_dir) || $dest_dir === $src_dir) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/dir", array('p' => $src_dir), array( 'operation' => 'rename', 'newname' => $dest_dir, )); return $this->is_error() === false; } /** * Rename a directory * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool True on success, False on failure */ public function directory_create($repo_id, $dir) { // sanity checks if ($dir === '' || $dir === '/' || !is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/dir", array('p' => $dir), array( 'operation' => 'mkdir', )); return $this->is_error() === false; } /** * List directory entries (files and directories) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $type Entry type ('dir' or 'file') (requires Seafile 4.4.1) * @param bool $recursive Enable recursive call for 'dir' listing (requires Seafile 4.4.1) * * @return bool|array List of directories/files on success, False on failure */ public function directory_entries($repo_id, $dir, $type = null, $recursive = false) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // args: p=<$name> ('/' is a root, default), oid=? // sample result // [{ // "id": "0000000000000000000000000000000000000000", // "type": "file", // "name": "test1.c", // "size": 0 // },{ // "id": "e4fe14c8cda2206bb9606907cf4fca6b30221cf9", // "type": "dir", // "name": "test_dir" // }] $params = array('p' => $dir); if ($type) { $params['t'] = $type == 'dir' ? 'd' : 'f'; } if ($recursive && $type == 'dir') { $params['recursive'] = 1; } return $this->request('GET', "repos/$repo_id/dir", $params); } /** * Get directory information. * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool|array Directory properties on success, False on failure */ public function directory_info($repo_id, $dir) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $params = array('path' => $dir); return $this->request('GET', "repos/$repo_id/dir/detail", $params, null, null, '2.1'); } /** * Update a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param array $file File data (data, type, name) * * @return bool True on success, False on failure */ public function file_update($repo_id, $filename, $file) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } // first get the update link $result = $this->request('GET', "repos/$repo_id/update-link"); if ($this->is_error() || empty($result)) { return false; } $path = explode('/', $filename); $fn = array_pop($path); // then update file $result = $this->request('POST', $result, null, array( 'filename' => $fn, 'target_file' => $filename, ), array('file' => $file) ); return $this->is_error() === false; } /** * Upload a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param array $file File data (data, type, name) * * @return bool True on success, False on failure */ public function file_upload($repo_id, $filename, $file) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } // first get upload link $result = $this->request('GET', "repos/$repo_id/upload-link"); if ($this->is_error() || empty($result)) { return false; } $path = explode('/', $filename); $filename = array_pop($path); $dir = '/' . ltrim(implode('/', $path), '/'); $file['name'] = $filename; // then update file $result = $this->request('POST', $result, null, array( 'parent_dir' => $dir ), array('file' => $file) ); return $this->is_error() === false; } /** * Delete a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool True on success, False on failure */ public function file_delete($repo_id, $filename) { // sanity check if ($filename === '' || $filename === '/' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id/file", array('p' => $filename)); return $this->is_error() === false; } /** * Copy file(s) (no rename here) * * @param string $repo_id Library identifier * @param string|array $files List of files (without path) * @param string $src_dir Source directory * @param string $dest_dir Destination directory * @param string $dest_repo Destination library (optional) * * @return bool True on success, False on failure */ public function file_copy($repo_id, $files, $src_dir, $dest_dir, $dest_repo) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($src_dir === '' || !is_string($src_dir)) { $this->status = self::BAD_REQUEST; return false; } if ($dest_dir === '' || !is_string($dest_dir)) { $this->status = self::BAD_REQUEST; return false; } if ((!is_array($files) && !strlen($files)) || (is_array($files) && empty($files))) { $this->status = self::BAD_REQUEST; return false; } if (empty($dest_repo)) { $dest_repo = $repo_id; } $result = $this->request('POST', "repos/$repo_id/fileops/copy", array('p' => $src_dir), array( 'file_names' => implode(':', (array) $files), 'dst_dir' => $dest_dir, 'dst_repo' => $dest_repo, )); return $this->is_error() === false; } /** * Move a file (no rename here) * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param string $dst_dir Destination directory * @param string $dst_repo Destination library (optional) * * @return bool True on success, False on failure */ public function file_move($repo_id, $filename, $dst_dir, $dst_repo = null) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } if ($dst_dir === '' || !is_string($dst_dir)) { $this->status = self::BAD_REQUEST; return false; } if (empty($dst_repo)) { $dst_repo = $repo_id; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'move', 'dst_dir' => $dst_dir, 'dst_repo' => $dst_repo, )); return $this->is_error() === false; } /** * Rename a file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * @param string $new_name New file name (without path) * * @return bool True on success, False on failure */ public function file_rename($repo_id, $filename, $new_name) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } if ($new_name === '' || !is_string($new_name)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'rename', 'newname' => $new_name, )); return $this->is_error() === false; } /** * Create an empty file * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool True on success, False on failure */ public function file_create($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } $result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array( 'operation' => 'create', )); return $this->is_error() === false; } /** * Get file info * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool|array File info on success, False on failure */ public function file_info($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } // sample result: // "id": "013d3d38fed38b3e8e26b21bb3463eab6831194f", // "mtime": 1398148877, // "type": "file", // "name": "foo.py", // "size": 22 return $this->request('GET', "repos/$repo_id/file/detail", array('p' => $filename)); } /** * Get file content * * @param string $repo_id Library identifier * @param string $filename File name (with path) * * @return bool|string File download URI on success, False on failure */ public function file_get($repo_id, $filename) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($filename === '' || !is_string($filename)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('GET', "repos/$repo_id/file", array('p' => $filename)); } /** * List libraries (repositories) * * @return array|bool List of libraries on success, False on failure */ public function library_list() { $result = $this->request('GET', "repos"); // sample result // [{ // "permission": "rw", // "encrypted": false, // "mtime": 1400054900, // "owner": "user@mail.com", // "id": "f158d1dd-cc19-412c-b143-2ac83f352290", // "size": 0, // "name": "foo", // "type": "repo", // "virtual": false, // "desc": "new library", // "root": "0000000000000000000000000000000000000000" // }] return $result; } /** * Get library info * * @param string $repo_id Library identifier * * @return array|bool Library info on success, False on failure */ public function library_info($repo_id) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('GET', "repos/$repo_id"); } /** * Create library * * @param string $name Library name * @param string $description Library description * * @return bool|array Library info on success, False on failure */ public function library_create($name, $description = '') { if ($name === '' || !is_string($name)) { $this->status = self::BAD_REQUEST; return false; } return $this->request('POST', "repos", null, array( 'name' => $name, 'desc' => $description, )); } /** * Rename library * * @param string $repo_id Library identifier * @param string $new_name Library description * * @return bool True on success, False on failure */ public function library_rename($repo_id, $name, $description = '') { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($name === '' || !is_string($name)) { $this->status = self::BAD_REQUEST; return false; } // Note: probably by mistake the 'op' is a GET parameter // maybe changed in future to be consistent with other methods $this->request('POST', "repos/$repo_id", array('op' => 'rename'), array( 'repo_name' => $name, 'repo_desc' => $description, )); return $this->is_error() === false; } /** * Delete library * * @param string $repo_id Library identifier * * @return bool True on success, False on failure */ public function library_delete($repo_id) { if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "repos/$repo_id"); return $this->is_error() === false; } /** * Ping the API server * * @param string $token If set, auth token will be used * * @param bool True on success, False on failure */ public function ping($token = null) { // can be used to check if token is still valid if ($token) { $this->token = $token; $result = $this->request('GET', 'auth/ping', null, null); } // or if api works else { $result = $this->request('GET', 'ping', null, null); } return $this->is_error() === false; } /** * Share a directory (or library) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $right Permission ('r' or 'rw' or 'admin') * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * @param bool $update Update an existing entry * * @return bool True on success, False on failure */ public function shared_item_add($repo_id, $dir, $right, $mode, $who, $update = false) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($mode != 'user' && $mode != 'group') { $this->status = self::BAD_REQUEST; return false; } if ($right != 'r' && $right != 'rw' && $right != 'admin') { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $post = array( 'permission' => $right, 'share_type' => $mode, ); $post[$mode == 'group' ? 'group_id' : 'username'] = $who; $this->request($update ? 'POST' : 'PUT', "repos/$repo_id/dir/shared_items", array('p' => $dir), $post); return $this->is_error() === false; } /** * Update shared item permissions * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $right Permission ('r' or 'rw' or 'admin') * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * * @return bool True on success, False on failure */ public function shared_item_update($repo_id, $dir, $right, $mode, $who) { return $this->shared_item_add($repo_id, $dir, $right, $mode, $who, true); } /** * Un-Share a directory (or library) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $mode Mode ('user' or 'group') * @param string $who Username or Group ID * * @return bool True on success, False on failure */ public function shared_item_delete($repo_id, $dir, $mode, $who) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($mode != 'user' && $mode != 'group') { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $get = array( 'share_type' => $mode, 'p' => $dir ); $get[$mode == 'group' ? 'group_id' : 'username'] = $who; $this->request('DELETE', "repos/$repo_id/dir/shared_items", $get); return $this->is_error() === false; } /** * List directory permissions (shares) * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool|array List of user/group info on success, False on failure */ public function shared_item_list($repo_id, $dir) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // Example result: // [ // { // "group_info": { "id": 17, "name": "Group Name" }, // "is_admin": false, // "share_type": "group", // "permission": "rw" // }, // { // "user_info": { "nickname": "user", "name": "user@domain.com" }, // "share_type": "user", // "permission": "r" // } // ] return $this->request('GET', "repos/$repo_id/dir/shared_items", array('p' => $dir)); } /** * List share (download) links * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool|array List of shared links on success, False on failure */ public function share_link_list($repo_id, $dir) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // Example result: // [ // { // "username": "lian@lian.com", // "repo_id": "104f6537-b3a5-4d42-b8b5-8e47e494e4cf", // "ctime": "2017-04-01T02:35:29+00:00", // "expire_date": "", // "token": "0c4eb0cb104a43caaeef", // "view_cnt": 0, // "link": "https://cloud.seafile.com/d/0c4eb0cb104a43caaeef/", // "obj_name": "folder", // "path": "/folder/", // "is_dir": true, // "is_expired": false, // "repo_name": "for-test-web-api" // } // ] return $this->request('GET', "share-links", array('repo_id' => $repo_id, 'path' => $dir), null, null, '2.1'); } /** * Create a shared (download) link * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $password Password * @param string $expire Days to expire * * @return bool Link info on success, False on failure */ public function share_link_add($repo_id, $dir, $password = '', $expire = 0) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if (!empty($expire) && !is_numeric($expire)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $post = array( 'repo_id' => $repo_id, 'path' => $dir, ); if (strlen($password)) { $post['password'] = $password; } if ($expire > 0) { $post['expire_days'] = $expire; } $result = $this->request('POST', "share-links", array(), $post, null, '2.1'); // Sample response: // { // "token": "0c4eb0cb104a43caaeef", // "link": "https://cloud.seafile.com/d/db1a50e686/", // ... // } if (is_array($result) && !empty($result['link'])) { return $result; } return false; } /** * Delete a share (download) link * * @param string $token Link identifier (token) * * @return bool True on success, False on failure */ public function share_link_delete($token) { // sanity checks if ($token === '' || !is_string($token)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "share-links/{$token}", null, null, null, '2.1'); return $this->is_error() === false; } /** * List upload links * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * * @return bool|array List of upload links on success, False on failure */ public function upload_link_list($repo_id, $dir) { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } // Example result: // [ // { // "username": "lian@lian.com", // "repo_id": "104f6537-b3a5-4d42-b8b5-8e47e494e4cf", // "ctime": "2017-04-01T02:35:29+00:00", // "expire_date": "", // "token": "0c4eb0cb104a43caaeef", // "view_cnt": 0, // "link": "https://cloud.seafile.com/d/0c4eb0cb104a43caaeef/", // "obj_name": "folder", // "path": "/folder/", // "is_dir": true, // "is_expired": false, // "repo_name": "for-test-web-api" // } // ] return $this->request('GET', "upload-links", array('repo_id' => $repo_id, 'path' => $dir), null, null, '2.1'); } /** * Create an upload link * * @param string $repo_id Library identifier * @param string $dir Directory name (with path) * @param string $password Password * * @return bool Link info on success, False on failure */ public function upload_link_add($repo_id, $dir, $password = '') { // sanity checks if (!is_string($dir)) { $this->status = self::BAD_REQUEST; return false; } if ($repo_id === '' || !is_string($repo_id)) { $this->status = self::BAD_REQUEST; return false; } if ($dir === '') { $dir = '/'; } $post = array( 'repo_id' => $repo_id, 'path' => $dir, ); if (strlen($password)) { $post['password'] = $password; } $result = $this->request('POST', "upload-links", array(), $post, null, '2.1'); // Sample response: // { // "token": "0c4eb0cb104a43caaeef", // "link": "https://cloud.seafile.com/d/db1a50e686/", // ... // } if (is_array($result) && !empty($result['link'])) { return $result; } return false; } /** * Delete an upload link * * @param string $token Link identifier (token) * * @return bool True on success, False on failure */ public function upload_link_delete($token) { // sanity checks if ($token === '' || !is_string($token)) { $this->status = self::BAD_REQUEST; return false; } $this->request('DELETE', "upload-links/{$token}", null, null, null, '2.1'); return $this->is_error() === false; } /** * List user groups * * @return bool|array List of groups on success, False on failure */ public function group_list() { // Sample result: // [ // { // "ctime": 1398134171327948, // "creator": "user@example.com", // "msgnum": 0, // "mtime": 1398231100, // "id": 1, // "name": "lian" // } // ] }, $result = $this->request('GET', "groups"); if (is_array($result)) { $result = (array) $result['groups']; } return $result; } /** * List users * * @param string $search Search keyword * * @return bool|array List of users on success, False on failure */ public function user_search($search = null) { // Sample response: // [ // { // 'avatar_url': 'https://cloud.seafile.com/media/avatars/default.png', // 'contact_email': 'foo@foo.com', // 'email': 'foo@foo.com', // 'name': 'foo' // } // ] $result = $this->request('GET', "search-user", array('q' => $search)); if (is_array($result)) { $result = (array) $result['users']; } return $result; } /** * Parse and fix API request URLs */ public function mod_url($url) { // If Seafile is behind a proxy and different port, it will return // wrong URL for file uploads/downloads. We force the original URL prefix here if (stripos($url, $this->url_prefix) !== 0) { $url = $this->url_prefix . preg_replace('|^(https?://[^/]+)|i', '', $url); } return $url; } } diff --git a/lib/file_manticore_api.php b/lib/file_manticore_api.php index 1a50712..3797450 100644 --- a/lib/file_manticore_api.php +++ b/lib/file_manticore_api.php @@ -1,440 +1,438 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Helper class to connect to the Manticore API */ class file_manticore_api { /** * @var HTTP_Request2 */ private $request; /** * @var string */ private $base_url; /** * @var bool */ private $debug = false; const ERROR_INTERNAL = 100; const ERROR_CONNECTION = 500; const ACCEPT_HEADER = "application/json,text/javascript,*/*"; const ACCESS_WRITE = 'write'; const ACCESS_READ = 'read'; const ACCESS_DENY = 'deny'; /** * Class constructor. * * @param string $base_url Base URL of the Kolab API */ public function __construct($base_url) { - require_once 'HTTP/Request2.php'; - $config = rcube::get_instance()->config; $this->debug = rcube_utils::get_boolean($config->get('fileapi_manticore_debug')); $this->base_url = rtrim($base_url, '/') . '/'; $this->request = new HTTP_Request2(); self::configure($this->request); } /** * Configure HTTP_Request2 object * * @param HTTP_Request2 $request Request object */ public static function configure($request) { // Configure connection options $config = rcube::get_instance()->config; $http_config = (array) $config->get('http_request', $config->get('kolab_http_request')); // Deprecated config, all options are separated variables if (empty($http_config)) { $options = array( 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase', 'follow_redirects', ); foreach ($options as $optname) { if (($optvalue = $config->get($optname)) !== null || ($optvalue = $config->get('kolab_' . $optname)) !== null ) { $http_config[$optname] = $optvalue; } } } if (!empty($http_config)) { try { $request->setConfig($http_config); } catch (Exception $e) { rcube::raise_error("HTTP: " . $e->getMessage(), true, false); } } // proxy User-Agent $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']); // some HTTP server configurations require this header $request->setHeader('accept', self::ACCEPT_HEADER); $request->setHeader('Content-Type', 'application/json; charset=UTF-8'); } /** * Return API's base URL * * @return string Base URL */ public function base_url() { return $this->base_url; } /** * Return HTTP_Request2 object * * @return HTTP_Request2 Request object */ public function request() { return $this->request; } /** * Logs specified user into the API * * @param string $username User name * @param string $password User password * * @return string Session token (on success) */ public function login($username, $password) { $query = array( 'email' => $username, 'password' => $password, ); // remove current token if any $this->request->setHeader('Authorization'); // authenticate the user $response = $this->post('auth/local', $query); if ($token = $response->get('token')) { $this->set_session_token($token); } return $token; } /** * Sets request session token. * * @param string $token Session token. * @param bool $validate Enables token validatity check * * @return bool Token validity status */ public function set_session_token($token, $validate = false) { $this->request->setHeader('Authorization', "Bearer $token"); if ($validate) { $result = $this->get('api/users/me'); return $result->get_error_code() == 200; } return true; } /** * Delete document editing session * * @param array $id Session identifier * * @return bool True on success, False on failure */ public function document_delete($id) { $res = $this->delete('api/documents/' . $id); return $res->get_error_code() == 204; } /** * Create document editing session * * @param array $params Session parameters * * @return bool True on success, False on failure */ public function document_create($params) { $res = $this->post('api/documents', $params); // @FIXME: 422? return $res->get_error_code() == 201 || $res->get_error_code() == 422; } /** * Add document editor (update 'access' array) * * @param array $session_id Session identifier * @param array $identity User identifier * * @return bool True on success, False on failure */ public function editor_add($session_id, $identity, $permission) { $res = $this->get("api/documents/$session_id/access"); if ($res->get_error_code() != 200) { return false; } $access = $res->get(); // sanity check, this should never be empty if (empty($access)) { return false; } // add editor to the 'access' array foreach ($access as $entry) { if ($entry['identity'] == $identity) { return true; } } $access[] = array('identity' => $identity, 'permission' => $permission); $res = $this->put("api/documents/$session_id/access", $access); return $res->get_error_code() == 200; } /** * Remove document editor (update 'access' array) * * @param array $session_id Session identifier * @param array $identity User identifier * * @return bool True on success, False on failure */ public function editor_delete($session_id, $identity) { $res = $this->get("api/documents/$session_id/access"); if ($res->get_error_code() != 200) { return false; } $access = $res->get(); $found = true; // remove editor from the 'access' array foreach ((array) $access as $idx => $entry) { if ($entry['identity'] == $identity) { unset($access[$idx]); } } if (!$found) { return false; } $res = $this->put("api/documents/$session_id/access", $access); return $res->get_error_code() == 200; } /** * API's GET request. * * @param string $action Action name * @param array $get Request arguments * * @return file_ui_api_result Response */ public function get($action, $get = array()) { $url = $this->build_url($action, $get); if ($this->debug) { rcube::write_log('manticore', "GET: $url " . json_encode($get)); } $this->request->setMethod(HTTP_Request2::METHOD_GET); $this->request->setBody(''); return $this->get_response($url); } /** * API's POST request. * * @param string $action Action name * @param array $post POST arguments * * @return kolab_client_api_result Response */ public function post($action, $post = array()) { $url = $this->build_url($action); if ($this->debug) { rcube::write_log('manticore', "POST: $url " . json_encode($post)); } $this->request->setMethod(HTTP_Request2::METHOD_POST); $this->request->setBody(json_encode($post)); return $this->get_response($url); } /** * API's PUT request. * * @param string $action Action name * @param array $post POST arguments * * @return kolab_client_api_result Response */ public function put($action, $post = array()) { $url = $this->build_url($action); if ($this->debug) { rcube::write_log('manticore', "PUT: $url " . json_encode($post)); } $this->request->setMethod(HTTP_Request2::METHOD_PUT); $this->request->setBody(json_encode($post)); return $this->get_response($url); } /** * API's DELETE request. * * @param string $action Action name * @param array $get Request arguments * * @return file_ui_api_result Response */ public function delete($action, $get = array()) { $url = $this->build_url($action, $get); if ($this->debug) { rcube::write_log('manticore', "DELETE: $url " . json_encode($get)); } $this->request->setMethod(HTTP_Request2::METHOD_DELETE); $this->request->setBody(''); return $this->get_response($url); } /** * @param string $action Action GET parameter * @param array $args GET parameters (hash array: name => value) * * @return Net_URL2 URL object */ private function build_url($action, $args = array()) { $url = new Net_URL2($this->base_url . $action); $url->setQueryVariables((array) $args); return $url; } /** * HTTP Response handler. * * @param Net_URL2 $url URL object * * @return kolab_client_api_result Response object */ private function get_response($url) { try { $this->request->setUrl($url); $response = $this->request->send(); } catch (Exception $e) { return new file_ui_api_result(null, self::ERROR_CONNECTION, $e->getMessage()); } try { $body = $response->getBody(); } catch (Exception $e) { return new file_ui_api_result(null, self::ERROR_INTERNAL, $e->getMessage()); } $code = $response->getStatus(); if ($this->debug) { rcube::write_log('manticore', "Response [$code]: $body"); } if ($code < 300) { $result = $body ? json_decode($body, true) : array(); } else { if ($code != 401) { rcube::raise_error("Error $code on $url", true, false); } $error = $body; } return new file_ui_api_result($result, $code, $error); } } diff --git a/lib/file_ui_api.php b/lib/file_ui_api.php index 03ecfa6..c3f3c6b 100644 --- a/lib/file_ui_api.php +++ b/lib/file_ui_api.php @@ -1,289 +1,288 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Helper class to connect to the API */ class file_ui_api { /** * @var HTTP_Request2 */ private $request; /** * @var string */ private $base_url; const ERROR_INTERNAL = 100; const ERROR_CONNECTION = 200; const ACCEPT_HEADER = "application/json,text/javascript,*/*"; /** * Class constructor. * * @param string $base_url Base URL of the Kolab API */ public function __construct($base_url) { $this->base_url = $base_url; $this->init(); } /** * Initializes HTTP Request object. */ public function init() { - require_once 'HTTP/Request2.php'; $this->request = new HTTP_Request2(); self::configure($this->request); } /** * Configure HTTP_Request2 object * * @param HTTP_Request2 $request Request object */ public static function configure($request) { // Configure connection options $config = rcube::get_instance()->config; $http_config = (array) $config->get('http_request', $config->get('kolab_http_request')); // Deprecated config, all options are separated variables if (empty($http_config)) { $options = array( 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase', 'follow_redirects', ); foreach ($options as $optname) { if (($optvalue = $config->get($optname)) !== null || ($optvalue = $config->get('kolab_' . $optname)) !== null ) { $http_config[$optname] = $optvalue; } } } if (!empty($http_config)) { try { $request->setConfig($http_config); } catch (Exception $e) { //rcube::raise_error("HTTP: " . $e->getMessage(), true, false); } } // proxy User-Agent $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']); // some HTTP server configurations require this header $request->setHeader('accept', self::ACCEPT_HEADER); } /** * Return API's base URL * * @return string Base URL */ public function base_url() { return $this->base_url; } /** * Return HTTP_Request2 object * * @return HTTP_Request2 Request object */ public function request() { return $this->request; } /** * Logs specified user into the API * * @param string $username User name * @param string $password User password * @param array $get Additional GET parameters (e.g. 'version') * * @return file_ui_api_result Request response */ public function login($username, $password, $get = null) { $query = array( 'username' => $username, 'password' => $password, ); $response = $this->post('authenticate', $get, $query); return $response; } /** * Logs specified user out of the API * * @return bool True on success, False on failure */ public function logout() { $response = $this->get('quit'); return $response->get_error_code() ? false : true; } /** * Sets session token value. * * @param string $token Token string */ public function set_session_token($token) { $this->request->setHeader('X-Session-Token', $token); } /** * Gets capabilities of the API (according to logged in user). * * @return kolab_client_api_result Capabilities response */ public function get_capabilities() { return $this->get('capabilities'); } /** * API's GET request. * * @param string $action Action name * @param array $args Request arguments * * @return file_ui_api_result Response */ public function get($action, $args = array()) { $url = $this->build_url($action, $args); // Log::trace("Calling API GET: $url"); $this->request->setMethod(HTTP_Request2::METHOD_GET); return $this->get_response($url); } /** * API's POST request. * * @param string $action Action name * @param array $url_args URL arguments * @param array $post POST arguments * * @return kolab_client_api_result Response */ public function post($action, $url_args = array(), $post = array()) { $url = $this->build_url($action, $url_args); // Log::trace("Calling API POST: $url"); $this->request->setMethod(HTTP_Request2::METHOD_POST); $this->request->addPostParameter($post); return $this->get_response($url); } /** * @param string $action Action GET parameter * @param array $args GET parameters (hash array: name => value) * * @return Net_URL2 URL object */ private function build_url($action, $args) { $url = new Net_URL2($this->base_url); $args['method'] = $action; $url->setQueryVariables($args); return $url; } /** * HTTP Response handler. * * @param Net_URL2 $url URL object * * @return kolab_client_api_result Response object */ private function get_response($url) { try { $this->request->setUrl($url); $response = $this->request->send(); } catch (Exception $e) { return new file_ui_api_result(null, self::ERROR_CONNECTION, $e->getMessage()); } try { $body = $response->getBody(); } catch (Exception $e) { return new file_ui_api_result(null, self::ERROR_INTERNAL, $e->getMessage()); } $body = @json_decode($body, true); $err_code = null; $err_str = null; if (is_array($body) && (empty($body['status']) || $body['status'] != 'OK')) { $err_code = !empty($body['code']) ? $body['code'] : self::ERROR_INTERNAL; $err_str = !empty($body['reason']) ? $body['reason'] : 'Unknown error'; } else if (!is_array($body)) { $err_code = self::ERROR_INTERNAL; $err_str = 'Unable to decode response'; } if (!$err_code && array_key_exists('result', (array) $body)) { $body = $body['result']; } return new file_ui_api_result($body, $err_code, $err_str); } } diff --git a/lib/file_wopi.php b/lib/file_wopi.php index c752dc8..4c4a51a 100644 --- a/lib/file_wopi.php +++ b/lib/file_wopi.php @@ -1,356 +1,354 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Document editing sessions handling (WOPI) */ class file_wopi extends file_document { protected $cache; // Mimetypes supported by CODE, but not advertised by all possible names protected $mimetype_aliases = array( 'application/vnd.corel-draw' => 'image/x-coreldraw', ); // Mimetypes supported by other Chwala viewers or ones we don't want to be editable protected $mimetype_exceptions = array( 'text/plain', 'image/bmp', 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/gif', 'image/tiff', 'image/x-tiff', ); /** * Return viewer URI for specified file/session. This creates * a new collaborative editing session when needed. * * @param string $file File path * @param array &$file_info File metadata (e.g. type) * @param string &$session_id Optional session ID to join to * @param string $readonly Create readonly (one-time) session * * @return string WOPI URI for specified document * @throws Exception */ public function session_start($file, &$file_info, &$session_id = null, $readonly = false) { parent::session_start($file, $file_info, $session_id, $readonly); if ($session_id) { // Create Chwala session for use as WOPI access_token // This session will have access to this one document session only $keys = array('env', 'user_id', 'user', 'username', 'password', 'storage_host', 'storage_port', 'storage_ssl', 'user_roledns'); $data = array_intersect_key($_SESSION, array_flip($keys)); $data['document_session'] = $session_id; $this->token = $this->api->session->create($data); $this->log_login($session_id); } return $this->frame_uri($session_id, $file_info['type']); } /** * Generate URI of WOPI editing session (WOPIsrc) */ protected function frame_uri($id, $mimetype) { $capabilities = $this->capabilities(); if (empty($capabilities) || empty($mimetype)) { return; } $metadata = $capabilities[strtolower($mimetype)]; if (empty($metadata)) { return; } $office_url = rtrim($metadata['urlsrc'], ' /?'); // collabora $service_url = $this->api->api_url() . '/wopi/files/' . $id; // @TODO: Parsing and replacing placeholder values // https://wopi.readthedocs.io/en/latest/discovery.html#action-urls $args = array('WOPISrc' => $service_url); // We could also set: title, closebutton, revisionhistory // @TODO: do it in editor_post_params() when supported by the editor if ($lang = $this->api->env['language']) { $args['lang'] = str_replace('_', '-', $lang); } return $office_url . '?' . http_build_query($args, '', '&'); } /** * Retern extra viewer parameters to post to the viewer iframe * * @param array $info File info * * @return array POST parameters */ public function editor_post_params($info) { // Access token TTL (number of milliseconds since January 1, 1970 UTC) if ($ttl = $this->rc->config->get('session_lifetime', 0) * 60) { $now = new DateTime('now', new DateTimeZone('UTC')); $ttl = ($ttl + $now->format('U')) . '000'; } $params = array( 'access_token' => $this->token, 'access_token_ttl' => $ttl ?: 0, ); return $params; } /** * List supported mimetypes * * @param bool $editable Return only editable mimetypes * * @return array List of supported mimetypes */ public function supported_filetypes($editable = false) { $caps = $this->capabilities(); if ($editable) { $editable = array(); foreach ($caps as $mimetype => $c) { if ($c['name'] == 'edit') { $editable[] = $mimetype; } } return $editable; } return array_keys($caps); } /** * Uses WOPI discovery to get Office capabilities * https://wopi.readthedocs.io/en/latest/discovery.html */ protected function capabilities() { $cache_key = 'wopi.capabilities'; if ($result = $this->get_from_cache($cache_key)) { return $this->apply_aliases_and_exceptions($result); } $office_url = rtrim($this->rc->config->get('fileapi_wopi_office'), ' /'); $office_url .= '/hosting/discovery'; try { $request = $this->http_request(); $request->setMethod(HTTP_Request2::METHOD_GET); $request->setBody(''); $request->setUrl($office_url); $response = $request->send(); $body = $response->getBody(); $code = $response->getStatus(); if (empty($body) || $code != 200) { throw new Exception("Unexpected WOPI discovery response"); } } catch (Exception $e) { rcube::raise_error($e, true, false); // Don't bail out here, it would make the kolab_files UI broken return array(); } // parse XML output // // // // // // ... $node = new DOMDocument('1.0', 'UTF-8'); $node->loadXML($body); $result = array(); foreach ($node->getElementsByTagName('app') as $app) { if ($mimetype = $app->getAttribute('name')) { if ($action = $app->getElementsByTagName('action')->item(0)) { foreach ($action->attributes as $attr) { $result[$mimetype][$attr->name] = $attr->value; } } } } if (empty($result)) { rcube::raise_error("Failed to parse WOPI discovery response: $body", true, false); // Don't bail out here, it would make the kolab_files UI broken return array(); } $this->save_in_cache($cache_key, $result); return $this->apply_aliases_and_exceptions($result); } /** * Initializes HTTP request object */ protected function http_request() { - require_once 'HTTP/Request2.php'; - $request = new HTTP_Request2(); // Configure connection options $config = $this->rc->config; $http_config = (array) $config->get('http_request', $config->get('kolab_http_request')); // Deprecated config, all options are separated variables if (empty($http_config)) { $options = array( 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase', 'follow_redirects', ); foreach ($options as $optname) { if (($optvalue = $config->get($optname)) !== null || ($optvalue = $config->get('kolab_' . $optname)) !== null ) { $http_config[$optname] = $optvalue; } } } if (!empty($http_config)) { try { $request->setConfig($http_config); } catch (Exception $e) { rcube::raise_error("HTTP: " . $e->getMessage(), true, false); } } // proxy User-Agent $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']); // some HTTP server configurations require this header $request->setHeader('accept', "application/json,text/javascript,*/*"); return $request; } /** * Get cached data */ protected function get_from_cache($key) { if ($cache = $this->get_cache()) { return $cache->get($key); } } /** * Store data in cache */ protected function save_in_cache($key, $value) { if ($cache = $this->get_cache()) { $cache->set($key, $value); } } /** * Getter for the shared cache engine object */ protected function get_cache() { if ($this->cache === null) { $this->cache = $this->rc->get_cache_shared('fileapi') ?: false; } return $this->cache; } /** * Support more mimetypes in CODE capabilities */ protected function apply_aliases_and_exceptions($caps) { foreach ($this->mimetype_aliases as $type => $alias) { if (isset($caps[$type]) && !isset($caps[$alias])) { $caps[$alias] = $caps[$type]; } } foreach ($this->mimetype_exceptions as $type) { unset($caps[$type]); } return $caps; } /** * Write login data (name, ID, IP address) to the 'userlogins' log file. */ protected function log_login($session_id) { if (!$this->api->config->get('log_logins')) { return; } $rcube = rcube::get_instance(); $user_name = $rcube->get_user_name(); $user_id = $rcube->get_user_id(); $message = sprintf('CODE access for %s (ID: %d) from %s in session %s; %s', $user_name, $user_id, rcube_utils::remote_ip(), session_id(), $session_id); // log login rcube::write_log('userlogins', $message); } }