Support images in HTML signatures (#1488676)
This enables image button and file browser in html editor for signatures
| | |
| | | CHANGELOG Roundcube Webmail |
| | | =========================== |
| | | |
| | | - Support images in HTML signatures (#1488676) |
| | | - Display full quota information in popup (#1485769, #1486604) |
| | | - Mail compose: Selecting contact inserts recipient to previously focused input - to/cc/bcc accordingly (#1489684) |
| | | - Add option to set default message list mode - default_list_mode (#1487312) |
| | |
| | | // 4 - one identity with possibility to edit only signature |
| | | $config['identities_level'] = 0; |
| | | |
| | | // Maximum size of uploaded image in kilobytes |
| | | // Images (in html signatures) are stored in database as data URIs |
| | | $config['identity_image_size'] = 64; |
| | | |
| | | // Mimetypes supported by the browser. |
| | | // attachments of these types will open in a preview window |
| | | // either a comma-separated list or an array: 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/pdf' |
| | |
| | | |
| | | /** |
| | | * Initializes file uploading interface. |
| | | * |
| | | * @param $int Optional maximum file size in bytes |
| | | */ |
| | | public function upload_init() |
| | | public function upload_init($max_size = null) |
| | | { |
| | | // Enable upload progress bar |
| | | if ($seconds = $this->config->get('upload_progress')) { |
| | |
| | | $max_filesize = $max_postsize; |
| | | } |
| | | |
| | | if ($max_size && $max_size < $max_filesize) { |
| | | $max_filesize = $max_size; |
| | | } |
| | | |
| | | $this->output->set_env('max_filesize', $max_filesize); |
| | | $max_filesize = $this->show_bytes($max_filesize); |
| | | $this->output->set_env('filesizeerror', $this->gettext(array( |
| | |
| | | } |
| | | |
| | | /** |
| | | * Outputs uploaded file content (with image thumbnails support |
| | | * |
| | | * @param array $file Upload file data |
| | | */ |
| | | public function display_uploaded_file($file) |
| | | { |
| | | if (empty($file)) { |
| | | return; |
| | | } |
| | | |
| | | $file = $this->plugins->exec_hook('attachment_display', $file); |
| | | |
| | | if ($file['status']) { |
| | | if (empty($file['size'])) { |
| | | $file['size'] = $file['data'] ? strlen($file['data']) : @filesize($file['path']); |
| | | } |
| | | |
| | | // generate image thumbnail for file browser in HTML editor |
| | | if (!empty($_GET['_thumbnail'])) { |
| | | $temp_dir = $this->config->get('temp_dir'); |
| | | $thumbnail_size = 80; |
| | | list(,$ext) = explode('/', $file['mimetype']); |
| | | $mimetype = $file['mimetype']; |
| | | $file_ident = $file['id'] . ':' . $file['mimetype'] . ':' . $file['size']; |
| | | $cache_basename = $temp_dir . '/' . md5($file_ident . ':' . $this->user->ID . ':' . $thumbnail_size); |
| | | $cache_file = $cache_basename . '.' . $ext; |
| | | |
| | | // render thumbnail image if not done yet |
| | | if (!is_file($cache_file)) { |
| | | if (!$file['path']) { |
| | | $orig_name = $filename = $cache_basename . '.orig.' . $ext; |
| | | file_put_contents($orig_name, $file['data']); |
| | | } |
| | | else { |
| | | $filename = $file['path']; |
| | | } |
| | | |
| | | $image = new rcube_image($filename); |
| | | if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) { |
| | | $mimetype = 'image/' . $imgtype; |
| | | |
| | | if ($orig_name) { |
| | | unlink($orig_name); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (is_file($cache_file)) { |
| | | // cache for 1h |
| | | $this->output->future_expire_header(3600); |
| | | header('Content-Type: ' . $mimetype); |
| | | header('Content-Length: ' . filesize($cache_file)); |
| | | |
| | | readfile($cache_file); |
| | | exit; |
| | | } |
| | | } |
| | | |
| | | header('Content-Type: ' . $file['mimetype']); |
| | | header('Content-Length: ' . $file['size']); |
| | | |
| | | if ($file['data']) { |
| | | echo $file['data']; |
| | | } |
| | | else if ($file['path']) { |
| | | readfile($file['path']); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Initializes client-side autocompletion. |
| | | */ |
| | | public function autocomplete_init() |
| | |
| | | if (upload_id) |
| | | this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id}); |
| | | |
| | | if (!this.env.attachments) |
| | | this.env.attachments = {}; |
| | | |
| | | if (upload_id && this.env.attachments[upload_id]) |
| | | delete this.env.attachments[upload_id]; |
| | | |
| | | this.env.attachments[name] = att; |
| | | |
| | | if (!this.gui_objects.attachmentlist) |
| | | return false; |
| | | |
| | |
| | | // set tabindex attribute |
| | | var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0'; |
| | | li.find('a').attr('tabindex', tabindex); |
| | | |
| | | if (upload_id && this.env.attachments[upload_id]) |
| | | delete this.env.attachments[upload_id]; |
| | | |
| | | this.env.attachments[name] = att; |
| | | |
| | | return true; |
| | | }; |
| | |
| | | |
| | | $(form).attr({ |
| | | target: frame_name, |
| | | action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }), |
| | | action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}), |
| | | method: 'POST'}) |
| | | .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data') |
| | | .submit(); |
| | |
| | | // minimal editor |
| | | if (config.mode == 'identity') { |
| | | $.extend(conf, { |
| | | plugins: 'autolink charmap code colorpicker hr link paste tabfocus textcolor', |
| | | plugins: 'autolink charmap code colorpicker hr image link paste tabfocus textcolor', |
| | | toolbar: 'bold italic underline alignleft aligncenter alignright alignjustify' |
| | | + ' | outdent indent charmap hr link unlink code forecolor' |
| | | + ' | fontselect fontsizeselect' |
| | | + ' | outdent indent charmap hr link unlink image code forecolor' |
| | | + ' | fontselect fontsizeselect', |
| | | file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); }, |
| | | file_browser_callback_types: 'image' |
| | | }); |
| | | } |
| | | // full-featured editor |
| | |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // @todo: upload progress indicator |
| | | }; |
| | | |
| | | // close file browser window |
| | |
| | | } |
| | | |
| | | if (rx.test(file.mimetype)) { |
| | | var href = rcmail.env.comm_path+'&_id='+rcmail.env.compose_id+'&_action=display-attachment&_file='+file_id, |
| | | var path = rcmail.env.comm_path + '&_from=' + rcmail.env.action, |
| | | action = rcmail.env.compose_id ? '&_id=' + rcmail.env.compose_id + '&_action=display-attachment' : '&_action=upload-display', |
| | | href = path + action + '&_file=' + file_id, |
| | | img = $('<img>').attr({title: file.name, src: img_src ? img_src : href + '&_thumbnail=1'}); |
| | | |
| | | return $('<li>').attr({tabindex: 0}) |
| | |
| | | this.hack_file_input = function(elem, clone_form) |
| | | { |
| | | var link = $(elem), |
| | | file = $('<input>'), |
| | | file = $('<input>').attr('name', '_files[]'), |
| | | form = $('<form>').attr({method: 'post', enctype: 'multipart/form-data'}), |
| | | offset = link.offset(); |
| | | |
| | |
| | | $id = $regs[1]; |
| | | } |
| | | |
| | | if ($attachment = $COMPOSE['attachments'][$id]) { |
| | | $attachment = $RCMAIL->plugins->exec_hook('attachment_display', $attachment); |
| | | } |
| | | |
| | | if ($attachment['status']) { |
| | | if (empty($attachment['size'])) { |
| | | $attachment['size'] = $attachment['data'] ? strlen($attachment['data']) : @filesize($attachment['path']); |
| | | } |
| | | |
| | | // generate image thumbnail for file browser in HTML editor |
| | | if (!empty($_GET['_thumbnail'])) { |
| | | $temp_dir = $RCMAIL->config->get('temp_dir'); |
| | | $thumbnail_size = 80; |
| | | list(,$ext) = explode('/', $attachment['mimetype']); |
| | | $mimetype = $attachment['mimetype']; |
| | | $file_ident = $attachment['id'] . ':' . $attachment['mimetype'] . ':' . $attachment['size']; |
| | | $cache_basename = $temp_dir . '/' . md5($file_ident . ':' . $RCMAIL->user->ID . ':' . $thumbnail_size); |
| | | $cache_file = $cache_basename . '.' . $ext; |
| | | |
| | | // render thumbnail image if not done yet |
| | | if (!is_file($cache_file)) { |
| | | if (!$attachment['path']) { |
| | | $orig_name = $filename = $cache_basename . '.orig.' . $ext; |
| | | file_put_contents($orig_name, $attachment['data']); |
| | | } |
| | | else { |
| | | $filename = $attachment['path']; |
| | | } |
| | | |
| | | $image = new rcube_image($filename); |
| | | if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) { |
| | | $mimetype = 'image/' . $imgtype; |
| | | |
| | | if ($orig_name) { |
| | | unlink($orig_name); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (is_file($cache_file)) { |
| | | // cache for 1h |
| | | $RCMAIL->output->future_expire_header(3600); |
| | | header('Content-Type: ' . $mimetype); |
| | | header('Content-Length: ' . filesize($cache_file)); |
| | | |
| | | readfile($cache_file); |
| | | exit; |
| | | } |
| | | } |
| | | |
| | | header('Content-Type: ' . $attachment['mimetype']); |
| | | header('Content-Length: ' . $attachment['size']); |
| | | |
| | | if ($attachment['data']) { |
| | | echo $attachment['data']; |
| | | } |
| | | else if ($attachment['path']) { |
| | | readfile($attachment['path']); |
| | | } |
| | | } |
| | | $RCMAIL->display_uploaded_file($COMPOSE['attachments'][$id]); |
| | | |
| | | exit; |
| | | } |
| | |
| | | |
| | | $out .= $form_end; |
| | | |
| | | // add image upload form |
| | | $max_filesize = $RCMAIL->upload_init($RCMAIL->config->get('identity_image_size', 64) * 1024); |
| | | $upload_form_id = 'identityImageUpload'; |
| | | |
| | | $out .= '<form id="' . $upload_form_id . '" style="display: none">' |
| | | . html::div('hint', $RCMAIL->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) |
| | | . '</form>'; |
| | | |
| | | $RCMAIL->output->add_gui_object('uploadform', $upload_form_id); |
| | | |
| | | return $out; |
| | | } |
| | |
| | | 'add-response' => 'edit_response.inc', |
| | | 'save-response' => 'edit_response.inc', |
| | | 'delete-response' => 'responses.inc', |
| | | 'upload-display' => 'upload.inc', |
| | | )); |
| | | |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // XSS protection in HTML signature (#1489251) |
| | | if (!empty($save_data['signature']) && !empty($save_data['html_signature'])) { |
| | | // replace uploaded images with data URIs |
| | | $save_data['signature'] = rcmail_attach_images($save_data['signature']); |
| | | |
| | | // XSS protection in HTML signature (#1489251) |
| | | $save_data['signature'] = rcmail_wash_html($save_data['signature']); |
| | | |
| | | // clear POST data of signature, we want to use safe content |
| | |
| | | |
| | | |
| | | /** |
| | | * Attach uploaded images into signature as data URIs |
| | | */ |
| | | function rcmail_attach_images($html) |
| | | { |
| | | global $RCMAIL; |
| | | |
| | | $offset = 0; |
| | | $regexp = '/\s(poster|src)\s*=\s*[\'"]*\S+upload-display\S+file=rcmfile([0-9]+)[\s\'"]*/'; |
| | | |
| | | while (preg_match($regexp, $html, $matches, 0, $offset)) { |
| | | $file_id = $matches[2]; |
| | | $data_uri = ' '; |
| | | |
| | | if ($file_id && ($file = $_SESSION['identity']['files'][$file_id])) { |
| | | $file = $RCMAIL->plugins->exec_hook('attachment_get', $file); |
| | | |
| | | $data_uri .= 'src="data:' . $file['mimetype'] . ';base64,'; |
| | | $data_uri .= base64_encode($file['data'] ? $file['data'] : file_get_contents($file['path'])); |
| | | $data_uri .= '" '; |
| | | } |
| | | |
| | | $html = str_replace($matches[0], $data_uri, $html); |
| | | $offset += strlen($data_uri) - strlen($matches[0]) + 1; |
| | | } |
| | | |
| | | return $html; |
| | | } |
| | | |
| | | /** |
| | | * Sanity checks/cleanups on HTML body of signature |
| | | */ |
| | | function rcmail_wash_html($html) |