Skip to content

Commit

Permalink
Merge pull request #873 from jacob-js/external-images
Browse files Browse the repository at this point in the history
Option to always allow external images
  • Loading branch information
kroky authored Feb 22, 2024
2 parents fe1c658 + 2e9519a commit ddf1dce
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 26 deletions.
12 changes: 11 additions & 1 deletion modules/core/message_functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@
if (!hm_exists('format_msg_html')) {
function format_msg_html($str, $images=false) {
$str = str_ireplace('</body>', '', $str);

$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.DefinitionID', 'hm-message');
$config->set('HTML.DefinitionRev', 1);
$config->set('Cache.DefinitionImpl', null);
$config->set('HTML.TargetBlank', true);
$config->set('HTML.TargetNoopener', true);
$config->set('HTML.Allowed', 'a[href|target]');

if (!$images) {
$config->set('URI.DisableExternalResources', true);
}
$config->set('URI.AllowedSchemes', array('mailto' => true, 'data' => true, 'http' => true, 'https' => true));
$config->set('Filter.ExtractStyleBlocks.TidyImpl', true);

$def = $config->getHTMLDefinition(true);
$html_tags = ['img', 'script', 'iframe', 'audio', 'embed', 'source', 'track', 'video'];
foreach ($html_tags as $tag) {
$def->addAttribute($tag, 'data-src', 'Text');
}

try {
$purifier = new HTMLPurifier($config);
return $purifier->purify($str);
Expand Down
4 changes: 1 addition & 3 deletions modules/imap/handler_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -1829,9 +1829,7 @@ public function process() {
elseif (isset($this->request->post['imap_prefetch']) && $this->request->post['imap_prefetch']) {
$prefetch = true;
}
if (array_key_exists('imap_allow_images', $this->request->post) && $this->request->post['imap_allow_images']) {
$this->out('imap_allow_images', true);
}

$this->out('header_allow_images', $this->config->get('allow_external_image_sources'));

$cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']);
Expand Down
21 changes: 11 additions & 10 deletions modules/imap/output_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Hm_Output_filter_message_body extends Hm_Output_Module {
/**
* Format html, text, or image content
*/

protected function output() {
$txt = '<div class="msg_text_inner">';
if ($this->get('msg_text')) {
Expand All @@ -97,17 +98,17 @@ protected function output() {
}
if (isset($struct['subtype']) && strtolower($struct['subtype']) == 'html') {
$allowed = $this->get('header_allow_images');
$images = $this->get('imap_allow_images', false);
if ($allowed && stripos($this->get('msg_text'), 'img')) {
if (!$images) {
$id = $this->get('imap_msg_part');
$txt .= '<div class="allow_image_link">'.
'<a href="#" class="msg_part_link" data-allow-images="1" '.
'data-message-part="'.$this->html_safe($id).'">'.
$this->trans('Allow Images').'</a></div>';
}
$msgText = $this->get('msg_text');
// Everything in the message starting with src="http:// or src="https:// or src='http:// or src='https://
$externalResRegexp = '/src="(https?:\/\/[^"]*)"|src=\'(https?:\/\/[^\']*)\'/i';

if ($allowed) {
$msgText = preg_replace_callback($externalResRegexp, function ($matches) {
return 'data-src="' . $matches[1] . '" ' . 'src="" ' . 'data-message-part="' . $this->html_safe($this->get('imap_msg_part')) . '"';
}, $msgText);
}
$txt .= format_msg_html($this->get('msg_text'), $images);

$txt .= format_msg_html($msgText, $allowed);
}
elseif (isset($struct['type']) && strtolower($struct['type']) == 'image') {
$txt .= format_msg_image($this->get('msg_text'), strtolower($struct['subtype']));
Expand Down
4 changes: 3 additions & 1 deletion modules/imap/site.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.imap_debug_data { margin-left: 10px; }
.imap_connect { display: inline; }
.allow_image_link { margin-right: 20px; float: right; margin-top: -10px; margin-bottom: 10px; }
.notices { display: flex; gap: 20px; align-items: center; margin-bottom: 20px;}
.mobile .notices { flex-direction: column; align-items: flex-start; }
.external_notices { display: flex; gap: 20px; flex-wrap: wrap; }
.imap_debug { border: solid 1px #aaa; float: left; padding: 10px; height: 300px; width: 300px; overflow: scroll; white-space: pre; margin-top: 50px; font-size: 75%; }
.server_link { line-height: 10pt; margin: 2px; margin-left: 5px; display: block; padding: 5px; border: solid 1px #ddd; background-color: #fff; float: right; clear: none; color: #333; text-decoration: none; border-radius: 3px; }
.hl { padding-right: 5px; color: #666; }
Expand Down
138 changes: 128 additions & 10 deletions modules/imap/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,7 @@ var expand_imap_folders = function(path) {
return false;
};

var get_message_content = function(msg_part, uid, list_path, detail, callback, noupdate, images) {
if (!images) {
images = 0;
}
var get_message_content = function(msg_part, uid, list_path, detail, callback, noupdate) {
if (!uid) {
uid = $('.msg_uid').val();
}
Expand All @@ -577,7 +574,6 @@ var get_message_content = function(msg_part, uid, list_path, detail, callback, n
[{'name': 'hm_ajax_hook', 'value': 'ajax_imap_message_content'},
{'name': 'imap_msg_uid', 'value': uid},
{'name': 'imap_msg_part', 'value': msg_part},
{'name': 'imap_allow_images', 'value': images},
{'name': 'imap_server_id', 'value': detail.server_id},
{'name': 'folder', 'value': detail.folder}],
function(res) {
Expand Down Expand Up @@ -715,11 +711,6 @@ var imap_message_view_finished = function(msg_uid, detail, skip_links) {
}
$('.all_headers').on("click", function() { return Hm_Utils.toggle_long_headers(); });
$('.small_headers').on("click", function() { return Hm_Utils.toggle_long_headers(); });
$('.msg_part_link').on("click", function() {
$('.header_subject')[0].scrollIntoView();
$('.msg_text_inner').css('visibility', 'hidden');
return get_message_content($(this).data('messagePart'), false, false, false, false, false, $(this).data('allowImages'));
});
$('#flag_msg').on("click", function() { return imap_flag_message($(this).data('state')); });
$('#unflag_msg').on("click", function() { return imap_flag_message($(this).data('state')); });
$('#delete_message').on("click", function() { return imap_delete_message(); });
Expand Down Expand Up @@ -1257,3 +1248,130 @@ var imap_archive_message = function(state, supplied_uid, supplied_detail) {
return false;
};

var imap_show_add_contact_popup = function() {
var popup = document.getElementById("contact_popup");
popup.classList.toggle("show");
};

var imap_hide_add_contact_popup = function(event) {
event.stopPropagation()
var popup = document.getElementById("contact_popup");
popup.classList.toggle("show");
};

/**
* Allow external resources for the provided element.
*
* @param {HTMLElement} element - The element containing the allow button.
* @param {string} messagePart - The message part associated with the resource.
* @returns {void}
*/
function handleAllowResource(element, messagePart) {
element.querySelector('a').addEventListener('click', function (e) {
e.preventDefault();
$('.msg_text_inner').remove();
const externalSources = $(this).data('src').split(',');
externalSources?.forEach((source) => Hm_Utils.save_to_local_storage(source, 1));
return get_message_content(messagePart, false, false, false, false, false);
});
}

/**
* Create and insert in the DOM an element containing a message and a button to allow the resource.
*
* @param {HTMLElement} element - The element having the blocked resource.
* @returns {void}
*/
function handleInvisibleResource(element) {
const dataSrc = element.dataset.src;

const allowResource = document.createElement('div');
// allowResource.classList.add('allow_image_link');
allowResource.classList.add('alert', 'alert-warning', 'p-1');

const source = dataSrc.substring(0, 40) + (dataSrc.length > 40 ? '...' : '');
allowResource.innerHTML = `Source blocked: ${element.alt ? element.alt : source}
<a href="#" data-src="${dataSrc}" class="btn btn-light btn-sm">
Allow</a></div>
`;

document.querySelector('.external_notices').insertAdjacentElement('beforeend', allowResource);
handleAllowResource(allowResource, element.dataset.messagePart);
}

const mutation = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function (node) {
if (node.classList.contains('msg_text_inner')) {

// Extarnal resources notice boxes container
document.querySelector('.msg_text_inner').insertAdjacentHTML('afterbegin', '<div class="external_notices"></div>');

const sender = document.querySelector('#contact_info').textContent.trim().replace(/\s/g, '_') + 'external_resources_allowed';
const elements = node.querySelectorAll('[data-src]');
const blockedResources = [];
elements.forEach(function (element) {

const dataSrc = element.dataset.src;
const senderAllowed = Hm_Utils.get_from_local_storage(sender);
const allowed = Hm_Utils.get_from_local_storage(dataSrc);

switch (Number(allowed) || Number(senderAllowed)) {
case 1:
element.src = dataSrc;
break;
default:
if ((allowed || senderAllowed) === null) {
Hm_Utils.save_to_local_storage(dataSrc, 0);
}
handleInvisibleResource(element);
blockedResources.push(dataSrc);
break;
}
});

const noticesElement = document.createElement('div');
noticesElement.classList.add('notices');

if(blockedResources.length) {
const allowAll = document.createElement('div');
allowAll.classList.add('allow_image_link', 'all', 'fw-bold');
allowAll.textContent = 'For security reasons, external resources have been blocked.';
if (blockedResources.length > 1) {
const allowAllLink = document.createElement('a');
allowAllLink.classList.add('btn', 'btn-light', 'btn-sm');
allowAllLink.href = '#';
allowAllLink.dataset.src = blockedResources.join(',');
allowAllLink.textContent = 'Allow all';
allowAll.appendChild(allowAllLink);
handleAllowResource(allowAll, elements[0].dataset.messagePart);
}
noticesElement.appendChild(allowAll);

const button = document.createElement('a');
button.classList.add('always_allow_image', 'btn', 'btn-light', 'btn-sm');
button.textContent = 'Always allow from this sender';
noticesElement.appendChild(button);

button.addEventListener('click', function (e) {
e.preventDefault();
Hm_Utils.save_to_local_storage(sender, 1);
$('.msg_text_inner').remove();
get_message_content(elements[0].dataset.messagePart, false, false, false, false, false)
});
}

document.querySelector('.external_notices').insertAdjacentElement('beforebegin', noticesElement);
}
});
}
});
});

const message = document.querySelector('.msg_text');
if (message) {
mutation.observe(document.querySelector('.msg_text'), {
childList: true
});
}
4 changes: 3 additions & 1 deletion modules/ldap_contacts/modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,9 @@ function fetch_ldap_contacts($config, $user_config, $contact_store, $session=fal
$ldap_config = ldap_add_user_auth($ldap_config, $user_config->get('ldap_contacts_auth_setting', array()));
if (count($ldap_config) > 0) {
foreach ($ldap_config as $name => $vals) {
$vals['name'] = $name;
if (is_array($vals)) {
$vals['name'] = $name;
}
$ldap = new Hm_LDAP_Contacts($vals);
if ($ldap->connect()) {
$contacts = $ldap->fetch();
Expand Down

0 comments on commit ddf1dce

Please sign in to comment.