Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2533717
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
145 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php
index de2f43b..700d649 100644
--- a/lib/drivers/kolab/kolab_file_storage.php
+++ b/lib/drivers/kolab/kolab_file_storage.php
@@ -1,1334 +1,1343 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $folders;
/**
* @var array
*/
protected $config = array();
/**
* @var string
*/
protected $title;
/**
* 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']))) {
return true;
}
$this->rc->plugins->exec_hook('login_failed', array(
'host' => $auth['host'],
'user' => $auth['user'],
));
}
/**
* Get password and name of authenticated user
*
* @return array Authenticated user data
*/
public function auth_info()
{
return array(
'username' => $this->config['username'] ?: $_SESSION['username'],
'password' => $this->config['password'] ?: $this->rc->decrypt($_SESSION['password']),
);
}
/**
* Storage host selection
*/
private function select_host($username)
{
// Get IMAP host
$host = $this->rc->config->get('default_host');
if (is_array($host)) {
list($user, $domain) = explode('@', $username);
// try to select host by mail domain
if (!empty($domain)) {
foreach ($host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is not found
if (is_array($host)) {
list($key, $val) = each($host);
$host = is_numeric($key) ? $val : $key;
}
}
return rcube_utils::parse_host($host);
}
/**
* Authenticates a user in IMAP
*/
private function login($username, $password, $host)
{
if (empty($username)) {
return false;
}
$login_lc = $this->rc->config->get('login_lc');
$default_port = $this->rc->config->get('default_port', 143);
// parse $host
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port'])) {
$port = $a_host['port'];
}
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
}
}
if (!$port) {
$port = $default_port;
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered?
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
// authenticate user in IMAP
$storage = $this->rc->get_storage();
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
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
$storage->clear_cache('mailboxes', true);
return true;
}
protected function init($user = null)
{
if ($_SESSION['user_id'] || $user) {
// overwrite config with user preferences
$this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']);
$this->rc->config->set_user_prefs((array)$this->rc->user->get_prefs());
$storage = $this->rc->get_storage();
$storage->set_charset($this->rc->config->get('default_charset', RCUBE_CHARSET));
setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8');
}
}
/**
* Configures environment
*
* @param array $config Configuration
* @param string $title Source identifier
*/
public function configure($config, $title = null)
{
$this->config = array_merge($this->config, $config);
// @TODO: this is currently not possible to have multiple sessions in Roundcube
}
/**
* Returns current instance title
*
* @return string Instance title (mount point)
*/
public function title()
{
return '';
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
$storage = $this->rc->get_storage();
$quota = $storage->get_capability('QUOTA');
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => $quota,
file_storage::CAPS_LOCKS => true,
file_storage::CAPS_SUBSCRIPTIONS => true,
);
}
/**
* Save configuration of external driver (mount point)
*
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_create($driver)
{
$drivers = $this->driver_list();
if ($drivers[$driver['title']]) {
throw new Exception("Driver exists", file_storage::ERROR);
}
$config = kolab_storage_config::get_instance();
$status = $config->save($driver, 'file_driver');
if (!$status) {
throw new Exception("Driver create failed", file_storage::ERROR);
}
$this->driver_list = null;
}
/**
* Delete configuration of external driver (mount point)
*
* @param string $name Driver instance name
*
* @throws Exception
*/
public function driver_delete($name)
{
$drivers = $this->driver_list();
if ($driver = $drivers[$name]) {
$config = kolab_storage_config::get_instance();
$status = $config->delete($driver['uid']);
if (!$status) {
throw new Exception("Driver delete failed", file_storage::ERROR);
}
$this->driver_list = null;
return;
}
throw new Exception("Driver not found", file_storage::ERROR);
}
/**
* Return list of registered drivers (mount points)
*
* @return array List of drivers data
* @throws Exception
*/
public function driver_list()
{
// use internal cache, this is specifically for iRony
// which may call this code path many times in one request
if ($this->driver_list !== null) {
return $this->driver_list;
}
// get current relations state
$config = kolab_storage_config::get_instance();
$default = true;
$filter = array(
array('type', '=', 'file_driver'),
);
$drivers = $config->get_objects($filter, $default, 100);
$result = array();
foreach ($drivers as $driver) {
$result[$driver['title']] = $driver;
}
return $this->driver_list = $result;
}
/**
* Update configuration of external driver (mount point)
*
* @param string $title Driver instance title
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_update($title, $driver)
{
$drivers = $this->driver_list();
if (!$drivers[$title]) {
throw new Exception("Driver not found", file_storage::ERROR);
}
$config = kolab_storage_config::get_instance();
$status = $config->save($driver, 'file_driver');
if (!$status) {
throw new Exception("Driver update failed", file_storage::ERROR);
}
$this->driver_list = null;
}
/**
* Returns metadata of the driver
*
* @return array Driver meta data (image, name, form)
*/
public function driver_metadata()
{
$image_content = file_get_contents(__DIR__ . '/kolab.png');
$metadata = array(
'image' => 'data:image/png;base64,' . base64_encode($image_content),
'name' => 'Kolab Groupware',
'ref' => 'http://kolab.org',
'description' => 'Kolab Groupware server',
'form' => array(
'host' => 'hostname',
'username' => 'username',
'password' => 'password',
),
);
return $metadata;
}
/**
* Validate metadata (config) of the driver
*
* @param array $metadata Driver metadata
*
* @return array Driver meta data to be stored in configuration
* @throws Exception
*/
public function driver_validate($metadata)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
$exists = $this->get_file_object($file_name, $folder);
if (!empty($exists)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR);
}
$object = $this->to_file_object(array(
'name' => $file_name,
'type' => $file['type'],
'path' => $file['path'],
'content' => $file['content'],
));
// save the file object in IMAP
$saved = $folder->save($object, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
$file_object = $this->get_file_object($file_name, $folder);
if (empty($file_object)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$key = key($file_object['_attachments']);
$file_object['_attachments'] = array(
0 => array(
'name' => $file_name,
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
),
$key => false,
);
// save the file object in IMAP
$saved = $folder->save($file_object, 'file', $file_object['_msguid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$deleted = $folder->delete($file);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting object from Kolab server"),
true, false);
throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
$file = $this->get_file_object($file_name, $folder);
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']) {
$folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], true);
}
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
return array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'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
$folder = $this->get_folder_object($folder_name);
$files = $folder->select($filter);
$result = array();
// convert to kolab_storage files list data format
foreach ($files as $idx => $file) {
$file = $this->from_file_object($file);
if (!isset($file['name'])) {
continue;
}
$filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
$result[$filename] = array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
unset($files[$idx]);
}
// @TODO: pagination, search (by filename, mimetype)
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
$file = $this->from_file_object($file);
// Save to temp file
// @TODO: use IMAP CATENATE extension
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmAttmnt');
$fh = fopen($file_path, 'w');
if (!$fh) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
if ($file['size']) {
$folder->get_attachment($file['uid'], $file['fileid'], null, false, $fh, true);
}
fclose($fh);
if (!file_exists($file_path)) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
// Update object
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file_path,
'mimetype' => $file['type'],
'size' => $file['size'],
));
$fields = array('created', 'changed', '_attachments', 'notes', 'sensitivity', 'categories', 'x-custom');
$file = array_intersect_key($file, array_combine($fields, $fields));
$saved = $new_folder->save($file, 'file');
@unlink($file_path);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
// Move the file
if ($folder->name != $new_folder->name) {
$saved = $folder->move($file['uid'], $new_folder->name);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error moving object on Kolab server"),
true, false);
throw new Exception("Storage error. File move failed.", file_storage::ERROR);
}
$folder = $new_folder;
}
if ($file_name === $new_name) {
return;
}
// Update object (changing the name)
$cid = key($file['_attachments']);
$file['_attachments'][$cid]['name'] = $new_name;
$file['_attachments'][0] = $file['_attachments'][$cid];
$file['_attachments'][$cid] = false;
$saved = $folder->save($file, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_create($folder_name, 'file', true);
if (!$success) {
throw new Exception("Storage error. Unable to create the folder", file_storage::ERROR);
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_delete($folder_name);
if (!$success) {
throw new Exception("Storage error. Unable to delete the folder.", file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_rename($folder_name, $new_name);
if (!$success) {
throw new Exception("Storage error. Unable to rename the folder", file_storage::ERROR);
}
}
/**
* Subscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_subscribe($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
if (!$storage->subscribe($folder_name)) {
throw new Exception("Storage error. Unable to subscribe the folder", file_storage::ERROR);
}
}
/**
* Unsubscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_unsubscribe($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
if (!$storage->unsubscribe($folder_name)) {
throw new Exception("Storage error. Unable to unsubsribe the folder", file_storage::ERROR);
}
}
/**
* Returns list of folders.
*
* @param array $params List parameters ('type', 'search')
*
* @return array List of folders
* @throws Exception
*/
public function folder_list($params = array())
{
$unsubscribed = $params['type'] & file_storage::FILTER_UNSUBSCRIBED;
$rights = ($params['type'] & file_storage::FILTER_WRITABLE) ? 'w' : null;
$imap = $this->rc->get_storage();
$folders = $imap->list_folders_subscribed('', '*', 'file', $rights);
if (!is_array($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
// create/subscribe 'Files' folder in case there's no folder of type 'file'
if (empty($folders) && !$unsubscribed) {
$imap = $this->rc->get_storage();
$default = 'Files';
// the folder may exist but be unsubscribed
if (!$imap->folder_exists($default)) {
if (kolab_storage::folder_create($default, 'file', true)) {
$folders[] = $default;
}
}
else if (kolab_storage::folder_type($default) == 'file') {
if ($imap->subscribe($default)) {
$folders[] = $default;
}
}
}
else {
if ($unsubscribed) {
$subscribed = $folders;
$folders = $imap->list_folders('', '*', 'file', $rights);
$folders = array_diff($folders, $subscribed);
}
// convert folder names to UTF-8
$callback = function($folder) {
if (strpos($folder, '&') !== false) {
return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET);
}
return $folder;
};
$folders = array_map($callback, $folders);
}
// searching
if (isset($params['search'])) {
$search = mb_strtoupper($params['search']);
$prefix = null;
$ns = $imap->get_namespace('other');
if (!empty($ns)) {
$prefix = rcube_charset::convert($ns[0][0], 'UTF7-IMAP', RCUBE_CHARSET);
}
$folders = array_filter($folders, function($folder) use ($search, $prefix) {
$path = explode('/', $folder);
// search in folder name not the full path
if (strpos(mb_strtoupper($path[count($path)-1]), $search) !== false) {
return true;
}
// if it is an other user folder, we'll match the user name
// and return all folders of the matching user
else if (strpos($folder, $prefix) === 0 && strpos(mb_strtoupper($path[1]), $search) !== false) {
return true;
}
return false;
});
}
$folders = array_values($folders);
return $folders;
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
- * @param string $uri URI
+ * @param string $path File/folder path
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
- public function lock_list($uri, $child_locks = false)
+ public function lock_list($path, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ $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->resource2uri($lock['uri']);
+ $list[$idx]['uri'] = $this->uri2path($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
- * @param string $uri 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($uri, $lock)
+ public function lock($path, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ $uri = $this->path2uri($path);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
- * @param string $path URI
+ * @param string $path File/folder path
* @param array $lock Lock data
*
* @throws Exception
*/
- public function unlock($uri, $lock)
+ public function unlock($path, $lock)
{
$this->init_lock_db();
- // convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ // 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;
}
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
*
* @return array File data
* @throws Exception
*/
protected function get_file_object(&$file_name, &$folder = null)
{
// extract file path and file name
$path = explode(file_storage::SEPARATOR, $file_name);
$file_name = array_pop($path);
$folder_name = implode(file_storage::SEPARATOR, $path);
if ($folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
// get folder object
$folder = $this->get_folder_object($folder_name);
$files = $folder->select(array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
));
return $files[0];
}
/**
* Get folder object.
*
* @param string $folder_name Name of a folder with full path
*
* @return kolab_storage_folder Folder object
* @throws Exception
*/
protected function get_folder_object($folder_name)
{
if ($folder_name === null || $folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
if (empty($this->folders[$folder_name])) {
$storage = $this->rc->get_storage();
$separator = $storage->get_hierarchy_delimiter();
$folder_name = str_replace(file_storage::SEPARATOR, $separator, $folder_name);
$imap_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$folder = kolab_storage::get_folder($imap_name, 'file');
if (!$folder || !$folder->valid) {
$error = $folder->get_error();
if ($error === kolab_storage::ERROR_IMAP_CONN || $error === kolab_storage::ERROR_CACHE_DB) {
throw new Exception("The storage is temporarily unavailable.", file_storage::ERROR_UNAVAILABLE);
}
else if ($error === kolab_storage::ERROR_NO_PERMISSION) {
throw new Exception("Storage error. Access not permitted", file_storage::ERROR_FORBIDDEN);
}
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
$this->folders[$folder_name] = $folder;
}
return $this->folders[$folder_name];
}
/**
* Simplify internal structure of the file object
*/
protected function from_file_object($file)
{
if (empty($file['_attachments'])) {
return $file;
}
$attachment = array_shift($file['_attachments']);
$file['name'] = $attachment['name'];
$file['size'] = $attachment['size'];
$file['type'] = $attachment['mimetype'];
$file['fileid'] = $attachment['id'];
unset($file['_attachments']);
return $file;
}
/**
* Convert to kolab_format internal structure of the file object
*/
protected function to_file_object($file)
{
// @TODO if path is empty and fileid exists it is an update
// get attachment body and save it in path
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
'size' => $file['size'],
));
unset($file['name']);
unset($file['size']);
unset($file['type']);
unset($file['path']);
unset($file['fileid']);
return $file;
}
- protected function uri2resource($uri)
+ /**
+ * 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();
- $uri = str_replace(file_storage::SEPARATOR, $separator, $uri);
+ $path = str_replace(file_storage::SEPARATOR, $separator, $path);
$owner = $this->rc->get_user_name();
// find the owner and remove namespace prefix
foreach ($namespace as $type => $ns) {
foreach ($ns as $root) {
- if (is_array($root) && $root[0] && strpos($uri, $root[0]) === 0) {
- $uri = substr($uri, strlen($root[0]));
+ 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, $uri) = explode($separator, $uri, 2);
+ list($user, $path) = explode($separator, $path, 2);
if (strpos($user, '@') === false) {
$domain = strstr($owner, '@');
if (!empty($domain)) {
$user .= $domain;
}
}
$owner = $user;
break;
}
break 2;
}
}
}
- // convert to imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
-
- return 'imap://' . urlencode($owner) . '@' . $storage->options['host'] . '/' . $uri;
+ return 'imap://' . rawurlencode($owner) . '@' . $storage->options['host']
+ . '/' . file_utils::encode_path($path);
}
- protected function resource2uri($resource)
+ /**
+ * 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://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
+ 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 = urldecode($matches[1]);
- $uri = $matches[3];
-
- // convert from imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
+ $user = rawurldecode($matches[1]);
+ $path = file_utils::decode_path($matches[3]);
// personal namespace
if ($user == $owner) {
// do nothing
// Note: that might not work if personal namespace uses e.g. INBOX/ prefix.
}
// shared namespace
else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) {
- $uri = $matches[1] . $uri;
+ $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
- $uri = $namespace[0][0] . $local . $separator . $uri;
+ $path = $namespace[0][0] . $local . $separator . $path;
}
- $uri = str_replace($separator, file_storage::SEPARATOR, $uri);
-
- return $uri;
+ return str_replace($separator, file_storage::SEPARATOR, $path);
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/drivers/seafile/seafile_file_storage.php b/lib/drivers/seafile/seafile_file_storage.php
index 649a91f..6f3265d 100644
--- a/lib/drivers/seafile/seafile_file_storage.php
+++ b/lib/drivers/seafile/seafile_file_storage.php
@@ -1,1237 +1,1255 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class seafile_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $config = array();
/**
* @var seafile_api
*/
protected $api;
/**
* List of SeaFile libraries
*
* @var array
*/
protected $libraries;
/**
* Instance title (mount point)
*
* @var string
*/
protected $title;
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param bool True on success, False on failure
*/
public function authenticate($username, $password)
{
$this->init(true);
$token = $this->api->authenticate($username, $password);
if ($token) {
$_SESSION[$this->title . 'seafile_user'] = $username;
$_SESSION[$this->title . 'seafile_token'] = $this->rc->encrypt($token);
$_SESSION[$this->title . 'seafile_pass'] = $this->rc->encrypt($password);
return true;
}
$this->api = false;
return false;
}
/**
* Get password and name of authenticated user
*
* @return array Authenticated user data
*/
public function auth_info()
{
return array(
'username' => $_SESSION[$this->title . 'seafile_user'],
'password' => $this->rc->decrypt($_SESSION[$this->title . 'seafile_pass']),
);
}
/**
* Initialize SeaFile Web API connection
*/
protected function init($skip_auth = false)
{
if ($this->api !== null) {
return $this->api !== false;
}
// read configuration
$config = array(
'host' => $this->rc->config->get('fileapi_seafile_host', 'localhost'),
'ssl_verify_peer' => $this->rc->config->get('fileapi_seafile_ssl_verify_peer', true),
'ssl_verify_host' => $this->rc->config->get('fileapi_seafile_ssl_verify_host', true),
'cache' => $this->rc->config->get('fileapi_seafile_cache'),
'cache_ttl' => $this->rc->config->get('fileapi_seafile_cache_ttl', '14d'),
'debug' => $this->rc->config->get('fileapi_seafile_debug', false),
);
$this->config = array_merge($config, $this->config);
// initialize Web API
$this->api = new seafile_api($this->config);
if ($skip_auth) {
return true;
}
// try session token
if ($_SESSION[$this->title . 'seafile_token']
&& ($token = $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']))
) {
$valid = $this->api->ping($token);
}
if (!$valid) {
// already authenticated in session
if ($_SESSION[$this->title . 'seafile_user']) {
$user = $_SESSION[$this->title . 'seafile_user'];
$pass = $this->rc->decrypt($_SESSION[$this->title . 'seafile_pass']);
}
// try user/pass of the main driver
else {
$user = $this->config['username'];
$pass = $this->config['password'];
}
if ($user) {
$valid = $this->authenticate($user, $pass);
}
}
// throw special exception, so we can ask user for the credentials
if (!$valid && empty($_SESSION[$this->title . 'seafile_user'])) {
throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH);
}
else if (!$valid && $this->api->is_error() == seafile_api::TOO_MANY_REQUESTS) {
throw new Exception("SeaFile storage temporarily unavailable (too many requests)", file_storage::ERROR);
}
return $valid;
}
/**
* Configures environment
*
* @param array $config Configuration
* @param string $title Source identifier
*/
public function configure($config, $title = null)
{
$this->config = array_merge($this->config, $config);
$this->title = $title;
}
/**
* Returns current instance title
*
* @return string Instance title (mount point)
*/
public function title()
{
return $this->title;
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => true,
file_storage::CAPS_LOCKS => true,
);
}
/**
* Save configuration of external driver (mount point)
*
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_create($driver)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Delete configuration of external driver (mount point)
*
* @param string $title Driver instance name
*
* @throws Exception
*/
public function driver_delete($title)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Return list of registered drivers (mount points)
*
* @return array List of drivers data
* @throws Exception
*/
public function driver_list()
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Update configuration of external driver (mount point)
*
* @param string $title Driver instance name
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_update($title, $driver)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Returns metadata of the driver
*
* @return array Driver meta data (image, name, form)
*/
public function driver_metadata()
{
$image_content = file_get_contents(__DIR__ . '/seafile.png');
$metadata = array(
'image' => 'data:image/png;base64,' . base64_encode($image_content),
'name' => 'SeaFile',
'ref' => 'http://seafile.com',
'description' => 'Storage implementing SeaFile API access',
'form' => array(
'host' => 'hostname',
'username' => 'username',
'password' => 'password',
),
);
// these are returned when authentication on folders list fails
if ($this->config['username']) {
$metadata['form_values'] = array(
'host' => $this->config['host'],
'username' => $this->config['username'],
);
}
return $metadata;
}
/**
* Validate metadata (config) of the driver
*
* @param array $metadata Driver metadata
*
* @return array Driver meta data to be stored in configuration
* @throws Exception
*/
public function driver_validate($metadata)
{
if (!is_string($metadata['username']) || !strlen($metadata['username'])) {
throw new Exception("Missing user name.", file_storage::ERROR);
}
if (!is_string($metadata['password']) || !strlen($metadata['password'])) {
throw new Exception("Missing user password.", file_storage::ERROR);
}
if (!is_string($metadata['host']) || !strlen($metadata['host'])) {
throw new Exception("Missing host name.", file_storage::ERROR);
}
$this->config['host'] = $metadata['host'];
if (!$this->authenticate($metadata['username'], $metadata['password'])) {
throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH);
}
return array(
'host' => $metadata['host'],
'username' => $metadata['username'],
'password' => $metadata['password'],
);
}
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
list($fn, $repo_id) = $this->find_library($file_name);
if (empty($repo_id)) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
if ($file['path']) {
$file['data'] = $file['path'];
}
else if (is_resource($file['content'])) {
$file['data'] = $file['content'];
}
else {
$fp = fopen('php://temp', 'wb');
fwrite($fp, $file['content'], strlen($file['content']));
$file['data'] = $fp;
unset($file['content']);
}
$created = $this->api->file_upload($repo_id, $fn, $file);
if ($fp) {
fclose($fp);
}
if (!$created) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving file to SeaFile server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
list($fn, $repo_id) = $this->find_library($file_name);
if (empty($repo_id)) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
if ($file['path']) {
$file['data'] = $file['path'];
}
else if (is_resource($file['content'])) {
$file['data'] = $file['content'];
}
else {
$fp = fopen('php://temp', 'wb');
fwrite($fp, $file['content'], strlen($file['content']));
$file['data'] = $fp;
unset($file['content']);
}
$saved = $this->api->file_update($repo_id, $fn, $file);
if ($fp) {
fclose($fp);
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving file to SeaFile server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
list($file_name, $repo_id) = $this->find_library($file_name);
if ($repo_id && $file_name != '/') {
$deleted = $this->api->file_delete($repo_id, $file_name);
}
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting object from SeaFile server"),
true, false);
throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
list($fn, $repo_id) = $this->find_library($file_name);
$file = $this->api->file_info($repo_id, $fn);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
// get file location on SeaFile server for download
if ($file['size']) {
$link = $this->api->file_get($repo_id, $fn);
}
// write to file pointer, send no headers
if ($fp) {
if ($file['size']) {
$this->save_file_content($link, $fp);
}
return;
}
if (!empty($params['force-download'])) {
$disposition = 'attachment';
header("Content-Type: application/octet-stream");
// @TODO
// if ($browser->ie)
// header("Content-Type: application/force-download");
}
else {
$mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']);
$disposition = 'inline';
header("Content-Transfer-Encoding: binary");
header("Content-Type: $mimetype");
}
$filename = addcslashes($file['name'], '"');
// Workaround for nasty IE bug (#1488844)
// If Content-Disposition header contains string "attachment" e.g. in filename
// IE handles data as attachment not inline
/*
@TODO
if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
$filename = str_ireplace('attachment', 'attach', $filename);
}
*/
header("Content-Length: " . $file['size']);
header("Content-Disposition: $disposition; filename=\"$filename\"");
// just send redirect to SeaFile server
if ($file['size']) {
// In view-mode we can't redirect to SeaFile server because:
// - it responds with Content-Disposition: attachment, which causes that
// e.g. previewing images is not possible
// - pdf/odf viewers can't follow redirects for some reason (#4590)
if (empty($params['force-download'])) {
if ($fp = fopen('php://output', 'wb')) {
$this->save_file_content($link, $fp);
fclose($fp);
die;
}
}
header("Location: $link");
}
die;
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
list($file, $repo_id) = $this->find_library($file_name);
$file = $this->api->file_info($repo_id, $file);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
return array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
}
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
* @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array())
{
list($folder, $repo_id) = $this->find_library($folder_name);
// prepare search filter
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
if ($idx == 'name') {
$params['search'][$idx] = mb_strtoupper($value);
}
else if ($idx == 'class') {
$params['search'][$idx] = file_utils::class2mimetypes($value);
}
}
}
// get directory entries
$entries = $this->api->directory_entries($repo_id, $folder);
$result = array();
foreach ((array) $entries as $idx => $file) {
if ($file['type'] != 'file') {
continue;
}
$file = $this->from_file_object($file);
// search filter
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
if ($idx == 'name') {
if (strpos(mb_strtoupper($file['name']), $value) === false) {
continue 2;
}
}
else if ($idx == 'class') {
foreach ($value as $v) {
if (stripos($file['type'], $v) !== false) {
continue 2;
}
}
continue 2;
}
}
}
$filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
$result[$filename] = array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
unset($entries[$idx]);
}
// @TODO: pagination, search (by filename, mimetype)
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
list($src_name, $repo_id) = $this->find_library($file_name);
list($dst_name, $dst_repo_id) = $this->find_library($new_name);
if ($repo_id && $dst_repo_id) {
$path_src = explode('/', $src_name);
$path_dst = explode('/', $dst_name);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = '/' . ltrim(implode('/', $path_src), '/');
$dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
$success = $this->api->file_copy($repo_id, $f_src, $src_dir, $dst_dir, $dst_repo_id);
// now rename the file if needed
if ($success && $f_src != $f_dst) {
$success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst);
}
}
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error copying file on SeaFile server"),
true, false);
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
list($src_name, $repo_id) = $this->find_library($file_name);
list($dst_name, $dst_repo_id) = $this->find_library($new_name);
if ($repo_id && $dst_repo_id) {
$path_src = explode('/', $src_name);
$path_dst = explode('/', $dst_name);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = '/' . ltrim(implode('/', $path_src), '/');
$dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
if ($src_dir == $dst_dir && $repo_id == $dst_repo_id) {
$success = true;
}
else {
$success = $this->api->file_move($repo_id, $src_name, $dst_dir, $dst_repo_id);
}
// now rename the file if needed
if ($success && $f_src != $f_dst) {
$success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst);
}
}
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error moving file on SeaFile server"),
true, false);
throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
list($folder, $repo_id) = $this->find_library($folder_name, true);
if (empty($repo_id)) {
$success = $this->api->library_create($folder_name);
}
else if ($folder != '/') {
$success = $this->api->directory_create($repo_id, $folder);
}
if (!$success) {
throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
}
// clear the cache
if (empty($repo_id)) {
$this->libraries = null;
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
list($folder, $repo_id) = $this->find_library($folder_name, true);
if ($repo_id && $folder == '/') {
$success = $this->api->library_delete($repo_id);
}
else if ($repo_id) {
$success = $this->api->directory_delete($repo_id, $folder);
}
if (!$success) {
throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
list($folder, $repo_id, $library) = $this->find_library($folder_name, true);
list($dest_folder, $dest_repo_id) = $this->find_library($new_name, true);
// folders rename/move is possible only in the same library and folder
// @TODO: support folder move between libraries and folders
// @TODO: support converting library into a folder and vice-versa
// library rename
if ($repo_id && !$dest_repo_id && $folder == '/' && strpos($new_name, '/') === false) {
$success = $this->api->library_rename($repo_id, $new_name, $library['desc']);
}
// folder rename
else if ($folder != '/' && $dest_folder != '/' && $repo_id && $repo_id == $dest_repo_id) {
$path_src = explode('/', $folder);
$path_dst = explode('/', $dest_folder);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = implode('/', $path_src);
$dst_dir = implode('/', $path_dst);
if ($src_dir == $dst_dir) {
$success = $this->api->directory_rename($repo_id, $folder, $f_dst);
}
}
if (!$success) {
throw new Exception("Storage error. Unable to rename/move folder", file_storage::ERROR);
}
}
/**
* Subscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_subscribe($folder_name)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Unsubscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_unsubscribe($folder_name)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Returns list of folders.
*
* @param array $params List parameters ('type', 'search')
*
* @return array List of folders
* @throws Exception
*/
public function folder_list($params = array())
{
$libraries = $this->libraries();
$folders = array();
if ($this->config['cache']) {
$cache = $this->rc->get_cache('seafile_' . $this->title,
$this->config['cache'], $this->config['cache_ttl'], true);
if ($cache) {
$cached = $cache->get('folders');
}
}
foreach ($libraries as $library) {
if ($library['virtual'] || $library['encrypted']) {
continue;
}
$folders[$library['name']] = $library['mtime'];
if ($folder_tree = $this->folders_tree($library, '', $library, $cached)) {
$folders = array_merge($folders, $folder_tree);
}
}
if (empty($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
if ($cache) {
$cache->set('folders', $folders);
}
// sort folders
$folders = array_keys($folders);
usort($folders, array('file_utils', 'sort_folder_comparator'));
return $folders;
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
- * @param string $uri URI
+ * @param string $path File/folder path
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
- public function lock_list($uri, $child_locks = false)
+ public function lock_list($path, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ $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->resource2uri($lock['uri']);
+ $list[$idx]['uri'] = $this->uri2path($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
- * @param string $uri 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($uri, $lock)
+ public function lock($path, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ $uri = $this->uri2resource($path);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
- * @param string $path URI
+ * @param string $path File/folder path
* @param array $lock Lock data
*
* @throws Exception
*/
- public function unlock($uri, $lock)
+ public function unlock($path, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ $uri = $this->path2uri($path);
if (!$this->lock_db->unlock($uri, $lock)) {
throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
}
}
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder)
{
if (!$this->init()) {
throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
}
$account_info = $this->api->account_info();
if (empty($account_info)) {
throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
}
$quota = array(
// expected values in kB
'total' => intval($account_info['total'] / 1024),
'used' => intval($account_info['usage'] / 1024),
);
return $quota;
}
/**
* Recursively builds folders list
*/
protected function folders_tree($library, $path, $folder, $cached)
{
$folders = array();
$fname = strlen($path) ? $path . $folder['name'] : '/';
$root = $library['name'] . ($fname != '/' ? $fname : '');
// nothing changed, use cached folders tree of this folder
if ($cached && $cached[$root] && $cached[$root] == $folder['mtime']) {
foreach ($cached as $folder_name => $mtime) {
if (strpos($folder_name, $root . '/') === 0) {
$folders[$folder_name] = $mtime;
}
}
}
// get folder content (files and sub-folders)
// there's no API method to get only folders
else if ($content = $this->api->directory_entries($library['id'], $fname)) {
if ($fname != '/') {
$fname .= '/';
}
foreach ($content as $item) {
if ($item['type'] == 'dir' && strlen($item['name'])) {
$folders[$root . '/' . $item['name']] = $item['mtime'];
// get subfolders recursively
$folders_tree = $this->folders_tree($library, $fname, $item, $cached);
if (!empty($folders_tree)) {
$folders = array_merge($folders, $folders_tree);
}
}
}
}
return $folders;
}
/**
* Get list of SeaFile libraries
*/
protected function libraries()
{
// get from memory, @TODO: cache in rcube_cache?
if ($this->libraries !== null) {
return $this->libraries;
}
if (!$this->init()) {
throw new Exception("Storage error. Unable to get list of SeaFile libraries.", file_storage::ERROR);
}
if ($list = $this->api->library_list()) {
$this->libraries = $list;
}
else {
$this->libraries = array();
}
return $this->libraries;
}
/**
* Find library ID from folder name
*/
protected function find_library($folder_name, $no_exception = false)
{
$libraries = $this->libraries();
foreach ($libraries as $lib) {
$path = $lib['name'] . '/';
if ($folder_name == $lib['name'] || strpos($folder_name, $path) === 0) {
if (empty($library) || strlen($library['name']) < strlen($lib['name'])) {
$library = $lib;
}
}
}
if (empty($library)) {
if (!$no_exception) {
throw new Exception("Storage error. Library not found.", file_storage::ERROR);
}
}
else {
$folder = substr($folder_name, strlen($library['name']) + 1);
}
return array(
'/' . ($folder ? $folder : ''),
$library['id'],
$library
);
}
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
*
* @return array File data
* @throws Exception
*/
protected function get_file_object(&$file_name, &$folder = null)
{
// extract file path and file name
$path = explode(file_storage::SEPARATOR, $file_name);
$file_name = array_pop($path);
$folder_name = implode(file_storage::SEPARATOR, $path);
if ($folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
// get folder object
$folder = $this->get_folder_object($folder_name);
$files = $folder->select(array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
));
return $files[0];
}
/**
* Simplify internal structure of the file object
*/
protected function from_file_object($file)
{
if ($file['type'] != 'file') {
return null;
}
// file modification time
if ($file['mtime']) {
try {
$file['changed'] = new DateTime('@' . $file['mtime']);
}
catch (Exception $e) { }
}
// find file mimetype from extension
$file['type'] = file_utils::ext_to_type($file['name']);
unset($file['id']);
unset($file['mtime']);
return $file;
}
/**
* Save remote file into file pointer
*/
protected function save_file_content($location, $fp)
{
if (!$fp || !$location) {
return false;
}
$config = array_merge($this->config, array('store_bodies' => true));
$request = seafile_api::http_request($config);
if (!$request) {
return false;
}
$observer = new seafile_request_observer();
$observer->set_fp($fp);
try {
$request->setUrl($location);
$request->attach($observer);
$response = $request->send();
$status = $response->getStatus();
$response->getBody(); // returns nothing
$request->detach($observer);
if ($status != 200) {
throw new Exception("Unable to save file. Status $status.");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return false;
}
return true;
}
- protected function uri2resource($uri)
+ /**
+ * Convert file/folder path into a global URI.
+ *
+ * @param string $path File/folder path
+ *
+ * @return string URI
+ * @throws Exception
+ */
+ public function path2uri($path)
{
- list($file, $repo_id, $library) = $this->find_library($uri);
-
- // convert to imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
+ list($file, $repo_id, $library) = $this->find_library($path);
- return 'seafile://' . urlencode($library['owner']) . '@' . $this->config['host'] . '/' . $uri;
+ return 'seafile://' . rawurlencode($library['owner']) . '@' . $this->config['host']
+ . '/' . file_utils::encode_path($path);
}
- protected function resource2uri($resource)
+ /**
+ * Convert global URI into file/folder path.
+ *
+ * @param string $uri URI
+ *
+ * @return string File/folder path
+ * @throws Exception
+ */
+ public function uri2path($uri)
{
- if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
+ if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) {
throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
}
- $user = urldecode($matches[1]);
- $uri = $matches[3];
+ $user = rawurldecode($matches[1]);
+ $host = $matches[2];
+ $path = file_utils::decode_path($matches[3]);
+
+ list($file, $repo_id, $library) = $this->find_library($path, true);
- // convert from imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
+ if (empty($library) || $host != $this->config['host'] || $user != $library['owner']) {
+ throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
+ }
- return $uri;
+ return $path;
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/drivers/webdav/webdav_file_storage.php b/lib/drivers/webdav/webdav_file_storage.php
index edb82c0..be2a4de 100644
--- a/lib/drivers/webdav/webdav_file_storage.php
+++ b/lib/drivers/webdav/webdav_file_storage.php
@@ -1,978 +1,997 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
require 'SabreDAV/vendor/autoload.php';
use Sabre\DAV\Client;
class webdav_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $config = array();
/**
* @var string
*/
protected $title;
/**
* @var Sabre\DAV\Client
*/
protected $client;
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param bool True on success, False on failure
*/
public function authenticate($username, $password)
{
$settings = array(
'baseUri' => $this->config['baseuri'],
'userName' => $username,
'password' => $password,
'authType' => Client::AUTH_BASIC,
);
$client = new Client($settings);
try {
$client->propfind('', array());
}
catch (Exception $e) {
return false;
}
if ($this->title) {
$_SESSION[$this->title . '_webdav_user'] = $username;
$_SESSION[$this->title . '_webdav_pass'] = $this->rc->encrypt($password);
$this->client = $client;
}
return true;
}
/**
* Get password and name of authenticated user
*
* @return array Authenticated user data
*/
public function auth_info()
{
return array(
'username' => $this->config['username'],
'password' => $this->config['password'],
);
}
/**
* Configures environment
*
* @param array $config Configuration
* @param string $title Source identifier
*/
public function configure($config, $title = null)
{
if (!empty($config['host'])) {
$config['baseuri'] = $config['host'];
}
$this->config = array_merge($this->config, $config);
$this->title = $title;
}
/**
* Initializes WebDAV client
*/
protected function init()
{
if ($this->client !== null) {
return true;
}
// Load configuration for main driver
$config['baseuri'] = $this->rc->config->get('fileapi_webdav_baseuri');
if (!empty($config['baseuri'])) {
$config['username'] = $_SESSION['username'];
$config['password'] = $this->rc->decrypt($_SESSION['password']);
}
$this->config = array_merge($config, $this->config);
// Use session username if not set in configuration
if (!isset($this->config['username'])) {
$this->config['username'] = $_SESSION[$this->title . '_webdav_user'];
}
if (!isset($this->config['password'])) {
$this->config['password'] = $this->rc->decrypt($_SESSION[$this->title . '_webdav_pass']);
}
if (empty($this->config['baseuri'])) {
throw new Exception("Missing base URI of WebDAV server", file_storage::ERROR_NOAUTH);
}
$this->client = new Client(array(
'baseUri' => $this->config['baseuri'],
'userName' => $this->config['username'],
'password' => $this->config['password'],
'authType' => Client::AUTH_BASIC,
));
}
/**
* Returns current instance title
*
* @return string Instance title (mount point)
*/
public function title()
{
return $this->title;
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => true,
file_storage::CAPS_LOCKS => true, //TODO: Implement WebDAV locks
);
}
/**
* Save configuration of external driver (mount point)
*
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_create($driver)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Delete configuration of external driver (mount point)
*
* @param string $name Driver instance name
*
* @throws Exception
*/
public function driver_delete($name)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Return list of registered drivers (mount points)
*
* @return array List of drivers data
* @throws Exception
*/
public function driver_list()
{
return array();
//TODO: Stub. Not implemented.
}
/**
* Update configuration of external driver (mount point)
*
* @param string $title Driver instance title
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_update($title, $driver)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Returns metadata of the driver
*
* @return array Driver meta data (image, name, form)
*/
public function driver_metadata()
{
$image_content = file_get_contents(__DIR__ . '/webdav.png');
$metadata = array(
'image' => 'data:image/png;base64,' . base64_encode($image_content),
'name' => 'WebDAV',
'ref' => 'http://www.webdav.org/',
'description' => 'WebDAV client',
'form' => array(
'baseuri' => 'baseuri',
'username' => 'username',
'password' => 'password',
),
);
// these are returned when authentication on folders list fails
if ($this->config['username']) {
$metadata['form_values'] = array(
'baseuri' => $this->config['baseuri'],
'username' => $this->config['username'],
);
}
return $metadata;
}
/**
* Validate metadata (config) of the driver
*
* @param array $metadata Driver metadata
*
* @return array Driver meta data to be stored in configuration
* @throws Exception
*/
public function driver_validate($metadata)
{
if (!is_string($metadata['username']) || !strlen($metadata['username'])) {
throw new Exception("Missing user name.", file_storage::ERROR);
}
if (!is_string($metadata['password']) || !strlen($metadata['password'])) {
throw new Exception("Missing user password.", file_storage::ERROR);
}
if (!is_string($metadata['baseuri']) || !strlen($metadata['baseuri'])) {
throw new Exception("Missing base URL.", file_storage::ERROR);
}
// Ensure baseUri ends with a slash
$base_uri = $metadata['baseuri'];
if (substr($base_uri, -1) != '/') {
$base_uri .= '/';
}
$this->config['baseuri'] = $base_uri;
if (!$this->authenticate($metadata['username'], $metadata['password'])) {
throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH);
}
return array(
'host' => $base_uri,
'port' => 0,
'username' => $metadata['username'],
'password' => $metadata['password'],
);
}
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
$this->init();
if ($file['path']) {
$data = fopen($file['path'], 'r');
}
else {
// Resource or data
$data = $file['content'];
}
$file_name = $this->encode_path($file_name);
$response = $this->client->request('PUT', $file_name, $data);
if ($response['statusCode'] != 201) {
throw new Exception("Storage error. " . $response['body'], file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
$this->init();
if ($file['path']) {
$data = fopen($file['path'], 'r');
}
else {
//Resource or data
$data = $file['content'];
}
$file_name = $this->encode_path($file_name);
$response = $this->client->request('PUT', $file_name, $data);
if ($response['statusCode'] != 204) {
throw new Exception("Storage error. " . $response['body'], file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
$this->init();
$file_name = $this->encode_path($file_name);
$response = $this->client->request('DELETE', $file_name);
if ($response['statusCode'] != 204) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
$this->init();
// TODO: Write directly to $fp
$file_name = $this->encode_path($file_name);
$response = $this->client->request('GET', $file_name);
if ($response['statusCode'] != 200) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$size = $response['headers']['content-length'][0];
// write to file pointer, send no headers
if ($fp) {
if ($size) {
fwrite($fp, $response['body']);
}
return;
}
if (!empty($params['force-download'])) {
$disposition = 'attachment';
header("Content-Type: application/octet-stream");
// @TODO
// if ($browser->ie)
// header("Content-Type: application/force-download");
}
else {
$mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']);
$disposition = 'inline';
header("Content-Transfer-Encoding: binary");
header("Content-Type: $mimetype");
}
$filename = addcslashes(end(explode('/', $file_name)), '"');
// Workaround for nasty IE bug (#1488844)
// If Content-Disposition header contains string "attachment" e.g. in filename
// IE handles data as attachment not inline
/*
@TODO
if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
$filename = str_ireplace('attachment', 'attach', $filename);
}
*/
header("Content-Length: " . $size);
header("Content-Disposition: $disposition; filename=\"$filename\"");
if ($size)
echo $response['body'];
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
$this->init();
try {
$props = $this->client->propfind($this->encode_path($file_name), array(
'{DAV:}resourcetype',
'{DAV:}getcontentlength',
'{DAV:}getcontenttype',
'{DAV:}getlastmodified',
'{DAV:}creationdate'
), 0);
}
catch (Exception $e) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$mtime = new DateTime($props['{DAV:}getlastmodified']);
$ctime = new DateTime($props['{DAV:}creationdate']);
return array (
'name' => end(explode('/', $file_name)),
'size' => (int) $props['{DAV:}getcontentlength'],
'type' => (string) $props['{DAV:}getcontenttype'],
'mtime' => $mtime ? $mtime->format($this->config['date_format']) : '',
'ctime' => $ctime ? $ctime->format($this->config['date_format']) : '',
'modified' => $mtime ? $mtime->format('U') : 0,
'created' => $ctime ? $ctime->format('U') : 0,
);
}
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
* @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array())
{
$this->init();
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
switch ($idx) {
case 'name':
$params['search']['name'] = mb_strtoupper($value);
break;
case 'class':
$params['search']['class'] = file_utils::class2mimetypes($params['search']['class']);
break;
}
}
}
try {
$items = $this->client->propfind($this->encode_path($folder_name), array(
'{DAV:}resourcetype',
'{DAV:}getcontentlength',
'{DAV:}getcontenttype',
'{DAV:}getlastmodified',
'{DAV:}creationdate'
), 1);
}
catch (Exception $e) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
$result = array();
foreach ($items as $file => $props) {
//Skip directories
$is_dir = in_array('{DAV:}collection', $props['{DAV:}resourcetype']->resourceType);
if ($is_dir) {
continue;
}
$mtime = new DateTime($props['{DAV:}getlastmodified']);
$ctime = new DateTime($props['{DAV:}creationdate']);
$ctype = (string) $props['{DAV:}getcontenttype'];
$path = $this->get_full_url($file);
$path = $this->decode_path($path);
$fname = end(explode('/', $path));
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
switch ($idx) {
case 'name':
if (stripos(mb_strtoupper($fname), $value) === false) {
continue 3; // skip the file
}
break;
case 'class':
foreach ($value as $type) {
if (stripos($ctype, $type) !== false) {
continue 3;
}
}
continue 3; // skip the file
break;
}
}
}
$result[$path] = array(
'name' => $fname,
'size' => (int) $props['{DAV:}getcontentlength'],
'type' => $ctype,
'mtime' => $mtime ? $mtime->format($this->config['date_format']) : '',
'ctime' => $ctime ? $ctime->format($this->config['date_format']) : '',
'modified' => $mtime ? $mtime->format('U') : 0,
'created' => $ctime ? $ctime->format('U') : 0,
);
}
// @TODO: pagination
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
$this->init();
$request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
$file_name = $this->encode_path($file_name);
$response = $this->client->request('COPY', $file_name, null, $request);
if ($response['statusCode'] != 201) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
$this->init();
$request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
$file_name = $this->encode_path($file_name);
$response = $this->client->request('MOVE', $file_name, null, $request);
if ($response['statusCode'] != 201) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
$this->init();
$folder_name = $this->encode_path($folder_name);
$response = $this->client->request('MKCOL', $folder_name);
if ($response['statusCode'] != 201) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
$this->init();
$folder_name = $this->encode_path($folder_name);
$response = $this->client->request('DELETE', $folder_name);
if ($response['statusCode'] != 204) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
$this->init();
$request = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
$folder_name = $this->encode_path($folder_name);
$response = $this->client->request('MOVE', $folder_name, null, $request);
if ($response['statusCode'] != 201) {
throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
}
}
/**
* Subscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_subscribe($folder_name)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Unsubscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_unsubscribe($folder_name)
{
throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
}
/**
* Returns list of folders.
*
* @param array $params List parameters ('type', 'search')
*
* @return array List of folders
* @throws Exception
*/
public function folder_list($params = array())
{
$this->init();
try {
$items = $this->client->propfind('', array(
'{DAV:}resourcetype',
), 'infinity');
// TODO: Replace infinity by recursion
// Many servers just do not support 'Depth: infinity' for security reasons
// E.g. SabreDAV has this optional and disabled by default
}
catch (Exception $e) {
throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH);
}
$result = array();
foreach ($items as $file => $props) {
// Skip files
$is_dir = in_array('{DAV:}collection', $props['{DAV:}resourcetype']->resourceType);
if (!$is_dir) {
continue;
}
$path = $this->get_relative_url($file);
$path = $this->decode_path($path);
if ($path !== '') {
$result[] = $path;
}
}
// ensure sorted folders
usort($result, array('file_utils', 'sort_folder_comparator'));
return $result;
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
- * @param string $uri URI
+ * @param string $path File/folder path
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
- public function lock_list($uri, $child_locks = false)
+ public function lock_list($path, $child_locks = false)
{
$this->init_lock_db();
- // convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ // convert path into global URI
+ $uri = $this->path2uri($path);
// get locks list
$list = $this->lock_db->lock_list($uri, $child_locks);
- // convert back resource string into URIs
+ // convert back global URIs into paths
foreach ($list as $idx => $lock) {
- $list[$idx]['uri'] = $this->resource2uri($lock['uri']);
+ $list[$idx]['uri'] = $this->uri2path($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
- * @param string $uri 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($uri, $lock)
+ public function lock($path, $lock)
{
$this->init_lock_db();
- // convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ // convert path into global URI
+ $uri = $this->path2uri($path);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
- * @param string $path URI
+ * @param string $path File/folder path
* @param array $lock Lock data
*
* @throws Exception
*/
- public function unlock($uri, $lock)
+ public function unlock($path, $lock)
{
$this->init_lock_db();
- // convert URI to global resource string
- $uri = $this->uri2resource($uri);
+ // convert path into global URI
+ $uri = $this->path2uri($path);
if (!$this->lock_db->unlock($uri, $lock)) {
throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
}
}
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder)
{
$this->init();
$props = $this->client->propfind($this->encode_path($folder), array(
'{DAV:}quota-available-bytes',
'{DAV:}quota-used-bytes',
), 0);
$used = $props['{DAV:}quota-used-bytes'];
$available = $props['{DAV:}quota-available-bytes'];
return array(
// expected values in kB
'total' => ($used + $available) / 1024,
'used' => $used / 1024,
);
}
/**
* Gets the relative URL of a resource
*
* @param string $url WebDAV URL
* @return string Path relative to root (title/.)
*/
protected function get_relative_url($url)
{
$url = $this->client->getAbsoluteUrl($url);
return trim(str_replace($this->config['baseuri'], '', $url), '/');
}
/**
* Gets the full URL of a resource
*
* @param string $url WebDAV URL
* @return string Path relative to chwala root
*/
protected function get_full_url($url)
{
if (!empty($this->title)) {
return $this->title . '/' . $this->get_relative_url($url);
}
return $this->get_relative_url($url);
}
/**
* Encode folder/file names in the path
* so it can be used as URL
*
* @param string $path File/folder path
*
* @return string Encoded URL
*/
protected function encode_path($path)
{
$path = explode('/', $path);
$path = array_map('rawurlencode', $path);
return implode('/', $path);
}
/**
* Decode folder/file URL path
*
* @param string $path File/folder path
*
* @return string Decoded path
*/
protected function decode_path($path)
{
$path = explode('/', $path);
$path = array_map('rawurldecode', $path);
return implode('/', $path);
}
- protected function uri2resource($uri)
+ /**
+ * Convert file/folder path into a global URI.
+ *
+ * @param string $path File/folder path
+ *
+ * @return string URI
+ * @throws Exception
+ */
+ public function path2uri($path)
{
- // convert to imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
$base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']);
- return 'webdav://' . urlencode($base) . '/' . $uri;
+ return 'webdav://' . rawurlencode($this->config['username']) . '@' . $base
+ . '/' . file_utils::encode_path($path);
}
- protected function resource2uri($resource)
+ /**
+ * Convert global URI into file/folder path.
+ *
+ * @param string $uri URI
+ *
+ * @return string File/folder path
+ * @throws Exception
+ */
+ public function uri2path($uri)
{
- if (!preg_match('|^webdav://(.*)$|', $resource, $matches)) {
+ if (!preg_match('|^webdav://([^@]+)@(.*)$|', $uri, $matches)) {
throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
}
- $uri = explode('/', $matches[1], 2);
- $uri = end($uri);
+ $user = rawurldecode($matches[1]);
+ $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']);
+ $uri = $matches[2];
+
+ if ($user != $this->config['username'] || strpos($uri, $base) !== 0) {
+ throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
+ }
- // convert from imap charset (to be safe to store in DB)
- $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
+ $uri = substr($matches[2], strlen($base) + 1);
- return $uri;
+ return file_utils::decode_path($uri);
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/file_locks.php b/lib/file_locks.php
index 56651a8..ed76973 100644
--- a/lib/file_locks.php
+++ b/lib/file_locks.php
@@ -1,267 +1,267 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* The Lock manager allows you to handle all file-locks centrally.
* It stores all its data in a sql database. Derived from SabreDAV's
* PDO Lock manager.
*/
-class file_locks {
-
+class file_locks
+{
const SHARED = 1;
const EXCLUSIVE = 2;
const INFINITE = -1;
/**
* The database connection object
*
* @var rcube_db
*/
private $db;
/**
* The tablename this backend uses.
*
* @var string
*/
protected $table;
/**
* Internal cache
*
* @var array
*/
protected $icache = array();
/**
* Constructor
*
* @param string $table Table name
*/
public function __construct($table = 'chwala_locks')
{
$rcube = rcube::get_instance();
$this->db = $rcube->get_dbh();
$this->table = $this->db->table_name($table);
if ($rcube->session) {
$rcube->session->register_gc_handler(array($this, 'gc'));
}
else {
// run garbage collector with probability based on
// session settings if session does not exist.
$probability = (int) ini_get('session.gc_probability');
$divisor = (int) ini_get('session.gc_divisor');
if ($divisor > 0 && $probability > 0) {
$random = mt_rand(1, $divisor);
if ($random <= $probability) {
$this->gc();
}
}
}
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
*/
public function lock_list($uri, $child_locks = false)
{
if ($this->icache['uri'] == $uri && $this->icache['child'] == $child_locks) {
return $this->icache['list'];
}
$query = "SELECT * FROM `{$this->table}` WHERE (`uri` = ?";
$params = array($uri);
if ($child_locks) {
$query .= " OR `uri` LIKE ?";
$params[] = $uri . '/%';
}
$path = '';
$key = $uri;
$list = array();
// in case uri contains protocol/host specification e.g. imap://user@host/
// handle prefix separately
if (preg_match('~^([a-z]+://[^/]+/)~i', $uri, $matches)) {
$path = $matches[1];
$uri = substr($uri, strlen($matches[1]));
}
// We need to check locks for every part in the path
$path_parts = explode('/', $uri);
// We already covered the last part of the uri
array_pop($path_parts);
if (!empty($path_parts)) {
$root_path = $path . implode('/', $path_parts);
// this path is already cached, extract locks from cached result
// we do this because it is a common scenario to request
// for lock on every file/folder in specified location
if ($this->icache['root_path'] == $root_path) {
$length = strlen($root_path);
foreach ($this->icache['list'] as $lock) {
if ($lock['depth'] != 0 && strlen($lock['token']) <= $length) {
$list[] = $lock;
}
}
}
else {
foreach ($path_parts as $part) {
$path .= $part;
$params[] = $path;
$path .= '/';
}
$query .= " OR (`uri` IN (" . implode(',', array_pad(array(), count($path_parts), '?')) . ") AND `depth` <> 0)";
}
}
// finally, skip expired locks
$query .= ") AND `expires` > " . $this->db->now();
// run the query and parse result
$result = $this->db->query($query, $params);
while ($row = $this->db->fetch_assoc($result)) {
$created = strtotime($row['expires']) - $row['timeout'];
$list[] = array(
'uri' => $row['uri'],
'owner' => $row['owner'],
'token' => $row['token'],
'timeout' => (int) $row['timeout'],
'created' => (int) $created,
'scope' => $row['scope'] == self::EXCLUSIVE ? file_storage::LOCK_EXCLUSIVE : file_storage::LOCK_SHARED,
'depth' => $row['depth'] == self::INFINITE ? file_storage::LOCK_INFINITE : (int) $row['depth'],
);
}
// remember last result in memory, sometimes we need it (or part of it) again
$this->icache['list'] = $list;
$this->icache['uri'] = $key;
$this->icache['root_path'] = $root_path;
$this->icache['child_locks'] = $child_locks;
return $list;
}
/**
* Locks a uri
*
* @param string $uri URI
* @param array $lock Lock data
*
* @return bool
*/
public function lock($uri, $lock)
{
// We're making the lock timeout max. 30 minutes
$timeout = min($lock['timeout'], 30*60);
$data = array(
$this->db->quote_identifier('uri') => $uri,
$this->db->quote_identifier('owner') => $lock['owner'],
$this->db->quote_identifier('scope') => $lock['scope'] == file_storage::LOCK_EXCLUSIVE ? self::EXCLUSIVE : self::SHARED,
$this->db->quote_identifier('depth') => $lock['depth'] == file_storage::LOCK_INFINITE ? self::INFINITE : 0,
$this->db->quote_identifier('timeout') => $timeout,
);
// check if lock exists
$locks = $this->lock_list($uri, false);
$exists = false;
foreach ($locks as $l) {
if ($l['token'] == $lock['token']) {
$exists = true;
break;
}
}
if ($exists) {
foreach (array_keys($data) as $key) {
$update_cols[] = "$key = ?";
}
$result = $this->db->query("UPDATE `{$this->table}`"
. " SET " . implode(', ', $update_cols)
. ", `expires` = " . $this->db->now($timeout)
. " WHERE `token` = ?",
array_merge(array_values($data), array($lock['token']))
);
}
else {
$data[$this->db->quote_identifier('token')] = $lock['token'];
$result = $this->db->query("INSERT INTO `{$this->table}`"
. " (".join(', ', array_keys($data)) . ", `expires`)"
. " VALUES (" . str_repeat('?, ', count($data)) . $this->db->now($timeout) . ")",
array_values($data)
);
}
return $this->db->affected_rows();
}
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @return bool
*/
public function unlock($uri, $lock)
{
$stmt = $this->db->query("DELETE FROM `{$this->table}`"
. " WHERE `uri` = ? AND `token` = ?",
$uri, $lock['token']);
return $this->db->affected_rows($stmt);
}
/**
* Remove expired locks
*/
public function gc()
{
$this->db->query("DELETE FROM `{$this->table}` WHERE `expires` < " . $this->db->now());
}
}
diff --git a/lib/file_storage.php b/lib/file_storage.php
index ef30f13..6cb8e07 100644
--- a/lib/file_storage.php
+++ b/lib/file_storage.php
@@ -1,338 +1,358 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
interface file_storage
{
// capabilities
const CAPS_ACL = 'ACL';
const CAPS_MAX_UPLOAD = 'MAX_UPLOAD';
const CAPS_PROGRESS_NAME = 'PROGRESS_NAME';
const CAPS_PROGRESS_TIME = 'PROGRESS_TIME';
const CAPS_QUOTA = 'QUOTA';
const CAPS_LOCKS = 'LOCKS';
const CAPS_SUBSCRIPTIONS = 'SUBSCRIPTIONS';
// config
const SEPARATOR = '/';
// error codes
const ERROR_LOCKED = 423;
const ERROR = 500;
const ERROR_UNAVAILABLE = 503;
const ERROR_FORBIDDEN = 530;
const ERROR_FILE_EXISTS = 550;
const ERROR_UNSUPPORTED = 570;
const ERROR_NOAUTH = 580;
// locks
const LOCK_SHARED = 'shared';
const LOCK_EXCLUSIVE = 'exclusive';
const LOCK_INFINITE = 'infinite';
// list filters
const FILTER_UNSUBSCRIBED = 1;
const FILTER_WRITABLE = 2;
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @return bool True on success, False on failure
*/
public function authenticate($username, $password);
/**
* Get password and name of authenticated user
*
* @return array Authenticated user data
*/
public function auth_info();
/**
* Configures environment
*
* @param array $config Configuration
* @param string $title Driver instance identifier
*/
public function configure($config, $title = null);
/**
* Returns current instance title
*
* @return string Instance title (mount point)
*/
public function title();
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities();
/**
* Save configuration of external driver (mount point)
*
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_create($driver);
/**
* Delete configuration of external driver (mount point)
*
* @param string $title Driver instance title
*
* @throws Exception
*/
public function driver_delete($title);
/**
* Return list of registered drivers (mount points)
*
* @return array List of drivers data
* @throws Exception
*/
public function driver_list();
/**
* Returns metadata of the driver
*
* @return array Driver meta data (image, name, form)
*/
public function driver_metadata();
/**
* Validate metadata (config) of the driver
*
* @param array $metadata Driver metadata
*
* @return array Driver meta data to be stored in configuration
* @throws Exception
*/
public function driver_validate($metadata);
/**
* Update configuration of external driver (mount point)
*
* @param string $title Driver instance title
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_update($title, $driver);
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path/content, type), where
* content might be a string or resource
*
* @throws Exception
*/
public function file_create($file_name, $file);
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path/content, type)
*
* @throws Exception
*/
public function file_update($file_name, $file);
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name);
/**
* Returns file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null);
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name);
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name);
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name);
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
* @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array());
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_create($folder_name);
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_delete($folder_name);
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception
*/
public function folder_move($folder_name, $new_name);
/**
* Subscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_subscribe($folder_name);
/**
* Unsubscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_unsubscribe($folder_name);
/**
* Returns list of folders.
*
* @param array $params List parameters ('type', 'search')
*
* @return array List of folders
* @throws Exception
*/
public function folder_list($params = array());
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false);
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock);
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock);
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder);
+
+ /**
+ * Convert file/folder path into a global URI.
+ *
+ * @param string $path File/folder path
+ *
+ * @return string URI
+ * @throws Exception
+ */
+ public function path2uri($path);
+
+ /**
+ * Convert global URI into file/folder path.
+ *
+ * @param string $uri URI
+ *
+ * @return string File/folder path
+ * @throws Exception
+ */
+ public function uri2path($uri);
}
diff --git a/lib/file_utils.php b/lib/file_utils.php
index b376c03..32891a4 100644
--- a/lib/file_utils.php
+++ b/lib/file_utils.php
@@ -1,211 +1,241 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_utils
{
static $class_map = array(
'document' => array(
// text
'text/',
'application/rtf',
'application/x-rtf',
'application/xml',
// office
'application/wordperfect',
'application/excel',
'application/msword',
'application/msexcel',
'application/mspowerpoint',
'application/vnd.ms-word',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument',
'application/vnd.oasis.opendocument',
'application/vnd.sun.xml.calc',
'application/vnd.sun.xml.writer',
'application/vnd.stardivision.calc',
'application/vnd.stardivision.writer',
// pdf
'application/pdf',
'application/x-pdf',
'application/acrobat',
'application/vnd.pdf',
),
'audio' => array(
'audio/',
),
'video' => array(
'video/',
),
'image' => array(
'image/',
'application/dxf',
'application/acad',
),
'empty' => array(
'application/x-empty',
),
);
// list of known file extensions, more in Roundcube config
static $ext_map = array(
'doc' => 'application/msword',
'eml' => 'message/rfc822',
'gz' => 'application/gzip',
'htm' => 'text/html',
'html' => 'text/html',
'mp3' => 'audio/mpeg',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odt' => 'application/vnd.oasis.opendocument.text',
'ogg' => 'application/ogg',
'pdf' => 'application/pdf',
'ppt' => 'application/vnd.ms-powerpoint',
'rar' => 'application/x-rar-compressed',
'tgz' => 'application/gzip',
'txt' => 'text/plain',
'zip' => 'application/zip',
);
/**
* Return list of mimetype prefixes for specified file class
*
* @param string $class Class name
*
* @return array List of mimetype prefixes
*/
static function class2mimetypes($class)
{
return isset(self::$class_map[$class]) ? self::$class_map[$class] : self::$class_map['empty'];
}
/**
* Finds class of specified mimetype
*
* @param string $mimetype File mimetype
*
* @return string Class name
*/
static function mimetype2class($mimetype)
{
$mimetype = strtolower($mimetype);
foreach (self::$class_map as $class => $prefixes) {
foreach ($prefixes as $prefix) {
if (strpos($mimetype, $prefix) === 0) {
return $class;
}
}
}
}
/**
* Apply some fixes on file mimetype string
*
* @param string $mimetype File type
*
* @return string File type
*/
static function real_mimetype($mimetype)
{
if (preg_match('/^text\/(.+)/i', $mimetype, $m)) {
// fix pdf mimetype
if (preg_match('/^(pdf|x-pdf)$/i', $m[1])) {
$mimetype = 'application/pdf';
}
}
return $mimetype;
}
/**
* Find mimetype from file name (extension)
*
* @param string $filename File name
* @param string $fallback Follback mimetype
*
* @return string File mimetype
*/
static function ext_to_type($filename, $fallback = 'application/octet-stream')
{
static $mime_ext = array();
$config = rcube::get_instance()->config;
$ext = substr($filename, strrpos($filename, '.') + 1);
if (empty($mime_ext)) {
$mime_ext = self::$ext_map;
foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
$mime_ext = array_merge($mime_ext, (array) @include($fpath));
}
}
if (is_array($mime_ext) && $ext) {
$mimetype = $mime_ext[strtolower($ext)];
}
return $mimetype ?: $fallback;
}
/**
* Returns script URI
*
* @return string Script URI
*/
static function script_uri()
{
if (!empty($_SERVER['SCRIPT_URI'])) {
return $_SERVER['SCRIPT_URI'];
}
$uri = $_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://';
$uri .= $_SERVER['HTTP_HOST'];
$uri .= preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
return $uri;
}
/**
* Callback for uasort() that implements correct
* locale-aware case-sensitive sorting
*/
public static function sort_folder_comparator($str1, $str2)
{
$path1 = explode(file_storage::SEPARATOR, $str1);
$path2 = explode(file_storage::SEPARATOR, $str2);
foreach ($path1 as $idx => $folder1) {
$folder2 = $path2[$idx];
if ($folder1 === $folder2) {
continue;
}
return strcoll($folder1, $folder2);
}
return 0;
}
+
+ /**
+ * Encode folder path for use in an URI
+ *
+ * @param string $path Folder path
+ *
+ * @return string Encoded path
+ */
+ public static function encode_path($path)
+ {
+ $items = explode(file_storage::SEPARATOR, $path);
+ $items = array_map('rawurlencode', $items);
+
+ return implode(file_storage::SEPARATOR, $items);
+ }
+
+ /**
+ * Decode an URI into folder path
+ *
+ * @param string $path Encoded folder path
+ *
+ * @return string Decoded path
+ */
+ public static function decode_path($path)
+ {
+ $items = explode(file_storage::SEPARATOR, $path);
+ $items = array_map('rawurldecode', $items);
+
+ return implode(file_storage::SEPARATOR, $items);
+ }
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Feb 5, 9:47 PM (4 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427998
Default Alt Text
(145 KB)
Attached To
Mode
R26 chwala
Attached
Detach File
Event Timeline
Log In to Comment