roundcubemail-plugins-kolab/plugins/kolab_files/lib/kolab_files_engine.php
Aleksander Machniak 716a464a6d Added support for virtual collections, multi-folder search,
skip/overwrite questions on file move/copy, other improvements
2013-05-16 13:08:58 +02:00

762 lines
27 KiB
PHP

<?php
/**
* Kolab files storage engine
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* 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/>.
*/
class kolab_files_engine
{
private $plugin;
private $rc;
private $timeout = 60;
private $sort_cols = array('name', 'mtime', 'size');
/**
* Class constructor
*/
public function __construct($plugin, $url)
{
$this->url = $url;
$this->plugin = $plugin;
$this->rc = $plugin->rc;
}
/**
* User interface initialization
*/
public function ui()
{
$this->plugin->add_texts('localization/');
// set templates of Files UI and widgets
if ($this->rc->task == 'mail') {
if ($this->rc->action == 'compose') {
$template = 'compose_plugin';
}
else if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
$template = 'message_plugin';
// add "Save as" button into attachment menu
$this->plugin->add_button(array(
'id' => 'attachmenusaveas',
'name' => 'attachmenusaveas',
'type' => 'link',
'wrapper' => 'li',
'onclick' => 'return false',
'class' => 'icon active saveas',
'classact' => 'icon active saveas',
'innerclass' => 'icon active saveas',
'label' => 'kolab_files.saveto',
), 'attachmentmenu');
}
$this->plugin->add_label('save', 'cancel',
'saveall', 'fromcloud', 'attachsel', 'selectfiles', 'attaching',
'collection_audio', 'collection_video', 'collection_image', 'collection_document'
);
}
else if ($this->rc->task == 'files') {
$template = 'files';
}
// add taskbar button
if (empty($_REQUEST['framed'])) {
$this->plugin->add_button(array(
'command' => 'files',
'class' => 'button-files',
'classsel' => 'button-files button-selected',
'innerclass' => 'button-inner',
'label' => 'kolab_files.files',
), 'taskbar');
}
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/style.css');
if (!empty($template)) {
$this->plugin->include_script($this->url . '/js/files_api.js');
$this->plugin->include_script('kolab_files.js');
$this->rc->output->set_env('files_url', $this->url . '/api/');
$this->rc->output->set_env('files_token', $this->get_api_token());
// register template objects for dialogs (and main interface)
$this->rc->output->add_handlers(array(
'folder-create-form' => array($this, 'folder_create_form'),
'file-search-form' => array($this, 'file_search_form'),
'filelist' => array($this, 'file_list'),
));
if ($this->rc->task != 'files') {
// add dialog content at the end of page body
$this->rc->output->add_footer(
$this->rc->output->parse('kolab_files.' . $template, false, false));
}
}
}
/**
* Engine actions handler
*/
public function actions()
{
if ($this->rc->task == 'files' && $this->rc->action) {
$action = $this->rc->action;
}
else if ($this->rc->task != 'files' && $_POST['act']) {
$action = $_POST['act'];
}
else {
$action = 'index';
}
$method = 'action_' . str_replace('-', '_', $action);
if (method_exists($this, $method)) {
$this->plugin->add_texts('localization/');
$this->{$method}();
}
}
/**
* Template object for folder creation form
*/
public function folder_create_form($attrib)
{
$attrib['name'] = 'folder-create-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'folder-create-form';
}
$input_name = new html_inputfield(array('id' => 'folder-name', 'name' => 'name', 'size' => 30));
$select_parent = new html_select(array('id' => 'folder-parent', 'name' => 'parent'));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$table->add('title', html::label('folder-name', Q($this->plugin->gettext('foldername'))));
$table->add(null, $input_name->show());
$table->add('title', html::label('folder-parent', Q($this->plugin->gettext('folderinside'))));
$table->add(null, $select_parent->show());
$out = $table->show();
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('foldercreating', 'create', 'foldercreate', 'cancel');
$this->rc->output->add_gui_object('folder-create-form', $attrib['id']);
return $out;
}
/**
* Template object for file search form in "From cloud" dialog
*/
public function file_search_form($attrib)
{
$attrib['name'] = '_q';
if (empty($attrib['id'])) {
$attrib['id'] = 'filesearchbox';
}
if ($attrib['type'] == 'search' && !$this->rc->output->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
$input_q = new html_inputfield($attrib);
$out = $input_q->show();
// add some labels to client
$this->rc->output->add_label('searching');
$this->rc->output->add_gui_object('filesearchbox', $attrib['id']);
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag(array(
'action' => '?_task=files',
'name' => "filesearchform",
'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('files-search'); return false",
), $out);
}
return $out;
}
/**
* Template object for files list
*/
public function file_list($attrib)
{
// define list of cols to be displayed based on parameter or config
if (empty($attrib['columns'])) {
$list_cols = $this->rc->config->get('kolab_files_list_cols');
$dont_override = $this->rc->config->get('dont_override');
$a_show_cols = is_array($list_cols) ? $list_cols : array('name');
$this->rc->output->set_env('col_movable', !in_array('kolab_files_list_cols', (array)$dont_override));
}
else {
$a_show_cols = preg_split('/[\s,;]+/', strip_quotes($attrib['columns']));
}
// make sure 'name' and 'options' column is present
if (!in_array('name', $a_show_cols)) {
array_unshift($a_show_cols, 'name');
}
if (!in_array('options', $a_show_cols)) {
array_unshift($a_show_cols, 'options');
}
$attrib['columns'] = $a_show_cols;
// save some variables for use in ajax list
$_SESSION['kolab_files_list_attrib'] = $attrib;
// For list in dialog(s) remove all option-like columns
if ($this->rc->task != 'files') {
$a_show_cols = array_intersect($a_show_cols, $this->sort_cols);
}
// set default sort col/order to session
if (!isset($_SESSION['kolab_files_sort_col']))
$_SESSION['kolab_files_sort_col'] = $this->rc->config->get('kolab_files_sort_col') ?: 'name';
if (!isset($_SESSION['kolab_files_sort_order']))
$_SESSION['kolab_files_sort_order'] = strtoupper($this->rc->config->get('kolab_files_sort_order') ?: 'asc');
// set client env
$this->rc->output->add_gui_object('filelist', $attrib['id']);
$this->rc->output->set_env('sort_col', $_SESSION['kolab_files_sort_col']);
$this->rc->output->set_env('sort_order', $_SESSION['kolab_files_sort_order']);
$this->rc->output->set_env('coltypes', $a_show_cols);
$this->rc->output->set_env('search_threads', $this->rc->config->get('kolab_files_search_threads'));
$this->rc->output->include_script('list.js');
// attach css rules for mimetype icons
$this->plugin->include_stylesheet($this->url . '/skins/default/images/mimetypes/style.css');
$thead = '';
foreach ($this->file_list_head($attrib, $a_show_cols) as $cell) {
$thead .= html::tag('td', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
}
return html::tag('table', $attrib,
html::tag('thead', null, html::tag('tr', null, $thead)) . html::tag('tbody', null, ''),
array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
}
/**
* Creates <THEAD> for message list table
*/
protected function file_list_head($attrib, $a_show_cols)
{
$skin_path = $_SESSION['skin_path'];
// $image_tag = html::img(array('src' => "%s%s", 'alt' => "%s"));
// check to see if we have some settings for sorting
$sort_col = $_SESSION['kolab_files_sort_col'];
$sort_order = $_SESSION['kolab_files_sort_order'];
$dont_override = (array)$this->rc->config->get('dont_override');
$disabled_sort = in_array('message_sort_col', $dont_override);
$disabled_order = in_array('message_sort_order', $dont_override);
$this->rc->output->set_env('disabled_sort_col', $disabled_sort);
$this->rc->output->set_env('disabled_sort_order', $disabled_order);
// define sortable columns
if ($disabled_sort)
$a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array();
else
$a_sort_cols = $this->sort_cols;
if (!empty($attrib['optionsmenuicon'])) {
$onclick = 'return ' . JS_OBJECT_NAME . ".command('menu-open', 'filelistmenu')";
if ($attrib['optionsmenuicon'] === true || $attrib['optionsmenuicon'] == 'true')
$list_menu = html::div(array('onclick' => $onclick, 'class' => 'listmenu',
'id' => 'listmenulink', 'title' => $this->rc->gettext('listoptions')));
else
$list_menu = html::a(array('href' => '#', 'onclick' => $onclick),
html::img(array('src' => $skin_path . $attrib['optionsmenuicon'],
'id' => 'listmenulink', 'title' => $this->rc->gettext('listoptions'))));
}
else {
$list_menu = '';
}
$cells = array();
foreach ($a_show_cols as $col) {
// get column name
switch ($col) {
/*
case 'status':
$col_name = '<span class="' . $col .'">&nbsp;</span>';
break;
*/
case 'options':
$col_name = $list_menu;
break;
default:
$col_name = Q($this->plugin->gettext($col));
}
// make sort links
if (in_array($col, $a_sort_cols))
$col_name = html::a(array('href'=>"#sort", 'onclick' => 'return '.JS_OBJECT_NAME.".command('files-sort','".$col."',this)", 'title' => rcube_label('sortby')), $col_name);
else if ($col_name[0] != '<')
$col_name = '<span class="' . $col .'">' . $col_name . '</span>';
$sort_class = $col == $sort_col && !$disabled_order ? " sorted$sort_order" : '';
$class_name = $col.$sort_class;
// put it all together
$cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
}
return $cells;
}
/**
* Update files list object
*/
protected function file_list_update($prefs)
{
$attrib = $_SESSION['kolab_files_list_attrib'];
if (!empty($prefs['kolab_files_list_cols'])) {
$attrib['columns'] = $prefs['kolab_files_list_cols'];
$_SESSION['kolab_files_list_attrib'] = $attrib;
}
$a_show_cols = $attrib['columns'];
$head = '';
foreach ($this->file_list_head($attrib, $a_show_cols) as $cell) {
$head .= html::tag('td', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
}
$head = html::tag('tr', null, $head);
$this->rc->output->set_env('coltypes', $a_show_cols);
$this->rc->output->command('files_list_update', $head);
}
/**
* Get API token for current user session, authenticate if needed
*/
public function get_api_token()
{
$token = $_SESSION['kolab_files_token'];
$time = $_SESSION['kolab_files_time'];
if ($token && time() - $this->timeout < $time) {
return $token;
}
if (!($request = $this->get_request())) {
return $token;
}
try {
$url = $request->getUrl();
// Send ping request
if ($token) {
$url->setQueryVariables(array('method' => 'ping'));
$request->setUrl($url);
$response = $request->send();
$status = $response->getStatus();
if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
if ($body['status'] == 'OK') {
$_SESSION['kolab_files_time'] = time();
return $token;
}
}
}
// Go with authenticate request
$url->setQueryVariables(array('method' => 'authenticate'));
$request->setUrl($url);
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
$response = $request->send();
$status = $response->getStatus();
if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
$token = $body['result']['token'];
if ($token) {
$_SESSION['kolab_files_token'] = $token;
$_SESSION['kolab_files_time'] = time();
}
}
else {
throw new Exception(sprintf("Authenticate error (Status: %d)", $status));
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
return $token;
}
/**
* Initialize HTTP_Request object
*/
protected function get_request()
{
$url = $this->url . '/api/';
if (!$this->request) {
require_once 'HTTP/Request2.php';
try {
$request = new HTTP_Request2();
$request->setConfig(array(
'store_body' => true,
'follow_redirects' => true,
'ssl_verify_peer' => $this->rc->config->get('kolab_ssl_verify_peer', true),
));
$this->request = $request;
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
}
if ($this->request) {
// cleanup
try {
$this->request->setBody('');
$this->request->setUrl($url);
$this->request->setMethod(HTTP_Request2::METHOD_GET);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
}
return $this->request;
}
protected function action_index()
{
$this->plugin->add_label(
'folderdeleting', 'folderdeleteconfirm', 'foldercreating', 'uploading', 'attaching',
'filedeleting', 'filedeletenotice', 'filedeleteconfirm',
'filemoving', 'filemovenotice', 'filecopying', 'filecopynotice',
'collection_audio', 'collection_video', 'collection_image', 'collection_document',
'fileskip', 'fileskipall', 'fileoverwrite', 'fileoverwriteall', 'filemoveconfirm'
);
$this->rc->output->set_pagetitle($this->plugin->gettext('files'));
$this->rc->output->send('kolab_files.files');
}
/**
* Handler for preferences save action
*/
protected function action_prefs()
{
$dont_override = (array)$this->rc->config->get('dont_override');
$prefs = array();
$opts = array(
'kolab_files_sort_col' => true,
'kolab_files_sort_order' => true,
'kolab_files_list_cols' => false,
);
foreach ($opts as $o => $sess) {
if (isset($_POST[$o]) && !in_array($o, $dont_override)) {
$prefs[$o] = rcube_utils::get_input_value($o, rcube_utils::INPUT_POST);
if ($sess) {
$_SESSION[$o] = $prefs[$o];
}
if ($o == 'kolab_files_list_cols') {
$update_list = true;
}
}
}
// save preference values
if (!empty($prefs)) {
$this->rc->user->save_prefs($prefs);
}
if (!empty($update_list)) {
$this->file_list_update($prefs);
}
$this->rc->output->send();
}
/**
* Handler for "save all attachments into cloud" action
*/
protected function action_save_file()
{
// $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
$dest = rcube_utils::get_input_value('dest', rcube_utils::INPUT_POST);
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$name = rcube_utils::get_input_value('name', rcube_utils::INPUT_POST);
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$message = new rcube_message($uid);
$request = $this->get_request();
$url = $request->getUrl();
$files = array();
$errors = array();
$attachments = array();
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setHeader('X-Session-Token', $this->get_api_token());
$url->setQueryVariables(array('method' => 'file_upload', 'folder' => $dest));
$request->setUrl($url);
foreach ($message->attachments as $attach_prop) {
if (empty($id) || $id == $attach_prop->mime_id) {
$filename = strlen($name) ? $name : rcmail_attachment_name($attach_prop, true);
$attachments[$filename] = $attach_prop;
}
}
// @TODO: handle error
// @TODO: implement file upload using file URI instead of body upload
foreach ($attachments as $attach_name => $attach_prop) {
$path = tempnam($temp_dir, 'rcmAttmnt');
// save attachment to file
if ($fp = fopen($path, 'w+')) {
$message->get_part_content($attach_prop->mime_id, $fp, true);
}
else {
$errors[] = true;
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to save attachment into file $path"),
true, false);
continue;
}
fclose($fp);
// send request to the API
try {
$request->setBody('');
$request->addUpload('file[]', $path, $attach_name, $attach_prop->mimetype);
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$files[] = $attach_name;
}
else {
throw new Exception($body['reason']);
}
}
catch (Exception $e) {
unlink($path);
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
// clean up
unlink($path);
$request->setBody('');
}
if ($count = count($files)) {
$msg = $this->plugin->gettext(array('name' => 'saveallnotice', 'vars' => array('n' => $count)));
$this->rc->output->show_message($msg, 'confirmation');
}
if ($count = count($errors)) {
$msg = $this->plugin->gettext(array('name' => 'saveallerror', 'vars' => array('n' => $count)));
$this->rc->output->show_message($msg, 'error');
}
// @TODO: update quota indicator, make this optional in case files aren't stored in IMAP
$this->rc->output->send();
}
/**
* Handler for "add attachments from the cloud" action
*/
protected function action_attach_file()
{
$files = rcube_utils::get_input_value('files', rcube_utils::INPUT_POST);
$uploadid = rcube_utils::get_input_value('uploadid', rcube_utils::INPUT_POST);
$COMPOSE_ID = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$COMPOSE = null;
$errors = array();
if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
if (!$COMPOSE) {
die("Invalid session var!");
}
// attachment upload action
if (!is_array($COMPOSE['attachments'])) {
$COMPOSE['attachments'] = array();
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$request = $this->get_request();
$url = $request->getUrl();
// Use observer object to store HTTP response into a file
require_once $this->plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_files_observer.php';
$observer = new kolab_files_observer();
$request->setHeader('X-Session-Token', $this->get_api_token());
// download files from the API and attach them
foreach ($files as $file) {
// decode filename
$file = urldecode($file);
// get file information
try {
$url->setQueryVariables(array('method' => 'file_info', 'file' => $file));
$request->setUrl($url);
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$file_params = $body['result'];
}
else {
throw new Exception($body['reason']);
}
}
catch (Exception $e) {
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
// set location of downloaded file
$path = tempnam($temp_dir, 'rcmAttmnt');
$observer->set_file($path);
// download file
try {
$url->setQueryVariables(array('method' => 'file_get', 'file' => $file));
$request->setUrl($url);
$request->attach($observer);
$response = $request->send();
$status = $response->getStatus();
$response->getBody(); // returns nothing
$request->detach($observer);
if ($status != 200 || !file_exists($path)) {
throw new Exception("Unable to save file");
}
}
catch (Exception $e) {
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
$attachment = array(
'path' => $path,
'size' => $file_params['size'],
'name' => $file_params['name'],
'mimetype' => $file_params['type'],
'group' => $COMPOSE_ID,
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$COMPOSE['attachments'][$id] = $attachment;
if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => $this->rc->gettext('delete')
));
}
else {
$button = Q($this->rc->gettext('delete'));
}
$content = html::a(array(
'href' => "#delete",
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'class' => 'delete',
), $button);
$content .= Q($attachment['name']);
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
else if ($attachment['error']) {
$errors[] = $attachment['error'];
}
else {
$errors[] = $this->plugin->gettext('attacherror');
}
}
if (!empty($errors)) {
$this->rc->output->command('display_message', $this->plugin->gettext('attacherror'), 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
// send html page with JS calls as response
$this->rc->output->command('auto_save_start', false);
$this->rc->output->send();
}
}