Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to always allow external images #873

Merged
merged 1 commit into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]');
kroky marked this conversation as resolved.
Show resolved Hide resolved

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');
mercihabam marked this conversation as resolved.
Show resolved Hide resolved
$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 @@ -556,10 +556,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 @@ -574,7 +571,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 @@ -712,11 +708,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