Skip to content

Commit be4a5b2

Browse files
authored
Merge pull request #1804 from mercihabam/enh-ext-resources-handler
feat(frontend/backend): improve the external resources alert UI, and make it possible to block resources from a sender
2 parents e5a8d31 + 09f223a commit be4a5b2

File tree

10 files changed

+271
-139
lines changed

10 files changed

+271
-139
lines changed

modules/core/functions.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,15 +632,25 @@ function privacy_setting_callback($val, $key, $mod) {
632632
$key .= '_setting';
633633
$user_setting = $mod->user_config->get($key);
634634
$update = $mod->request->post['update'];
635+
$pop = $mod->request->post['pop'];
635636

636637
if ($update) {
637-
$val = implode($setting['separator'], array_filter(array_merge(explode($setting['separator'], $user_setting), [$val])));
638-
$mod->user_config->set($key, $val);
638+
if ($pop) {
639+
$new_value = implode($setting['separator'], array_filter(explode($setting['separator'], $user_setting), function($item) use ($val) {
640+
return $item != $val;
641+
}));
642+
} else {
643+
$new_value = implode($setting['separator'], array_filter(array_merge(explode($setting['separator'], $user_setting), [$val])));
644+
}
645+
646+
$mod->user_config->set($key, $new_value);
639647

640648
$user_data = $mod->session->get('user_data', array());
641-
$user_data[$key] = $val;
649+
$user_data[$key] = $new_value;
642650
$mod->session->set('user_data', $user_data);
643651
$mod->session->record_unsaved('Privacy settings updated');
652+
653+
return $new_value;
644654
}
645655
return $val;
646656
}

modules/core/js_modules/actions/privacy_controls.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,37 @@ async function addSenderToImagesWhitelist(email) {
1212
reject();
1313
});
1414
});
15-
}
15+
}
16+
17+
async function addSenderToImagesBlackList(email) {
18+
return new Promise((resolve, reject) => {
19+
Hm_Ajax.request([
20+
{ name: "hm_ajax_hook", value: "ajax_privacy_settings" },
21+
{ name: "images_blacklist", value: email },
22+
{ name: "save_settings", value: true },
23+
{ name: "update", value: true }
24+
], (response) => {
25+
resolve(response);
26+
}, [], false, undefined, () => {
27+
Hm_Notices.show('An error occurred while adding the sender to the blacklist', 'danger');
28+
reject();
29+
});
30+
});
31+
}
32+
33+
async function removeSenderFromImagesBlackList(email) {
34+
return new Promise((resolve, reject) => {
35+
Hm_Ajax.request([
36+
{ name: "hm_ajax_hook", value: "ajax_privacy_settings" },
37+
{ name: "images_blacklist", value: email },
38+
{ name: "save_settings", value: true },
39+
{ name: "update", value: true },
40+
{ name: "pop", value: true }
41+
], (response) => {
42+
resolve(response);
43+
}, [], false, undefined, () => {
44+
Hm_Notices.show('An error occurred while removing the sender from the blacklist', 'danger');
45+
reject();
46+
});
47+
});
48+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* Allow external resources for the provided element.
3+
*
4+
* @param {HTMLElement} element - The element containing the allow button.
5+
* @param {string} messagePart - The message part associated with the resource.
6+
* * @param {Boolean} inline - true if the message is displayed in inline mode, false otherwise.
7+
* @returns {void}
8+
*/
9+
function handleAllowResource(element, messagePart, inline = false) {
10+
element.querySelector('a').addEventListener('click', function (e) {
11+
e.preventDefault();
12+
$('.msg_text_inner').remove();
13+
const externalSources = $(this).data('src').split(',');
14+
externalSources?.forEach((source) => Hm_Utils.save_to_local_storage(source, 1));
15+
if (inline) {
16+
return inline_imap_msg(window.inline_msg_details, window.inline_msg_uid);
17+
}
18+
return get_message_content(getParam('part'), getMessageUidParam(), getListPathParam(), getParam('list_parent'), false, false, false);
19+
});
20+
}
21+
22+
/**
23+
* Create and insert in the DOM an element containing a message and a button to allow the resource.
24+
*
25+
* @param {HTMLElement} element - The element having the blocked resource.
26+
* @param {Boolean} inline - true if the message is displayed in inline mode, false otherwise.
27+
* @returns {void}
28+
*/
29+
function handleInvisibleResource(element, inline = false) {
30+
const dataSrc = element.dataset.src;
31+
32+
const allowResource = document.createElement('div');
33+
allowResource.classList.add('alert', 'alert-warning', 'p-1');
34+
35+
const source = dataSrc.substring(0, 40) + (dataSrc.length > 40 ? '...' : '');
36+
allowResource.innerHTML = `Source blocked: ${element.alt ? element.alt : source}
37+
<a href="#" data-src="${dataSrc}" class="btn btn-light btn-sm">
38+
Allow</a></div>
39+
`;
40+
41+
document.querySelector('.external_notices').insertAdjacentElement('beforeend', allowResource);
42+
handleAllowResource(allowResource, element.dataset.messagePart, inline);
43+
}
44+
45+
const handleExternalResources = (inline) => {
46+
const messageContainer = document.querySelector('.msg_text_inner');
47+
const externalNoticesAccordion = document.createElement('div');
48+
externalNoticesAccordion.classList.add('accordion');
49+
externalNoticesAccordion.id = 'externalNoticesAccordion';
50+
messageContainer.insertAdjacentElement('afterbegin', externalNoticesAccordion);
51+
externalNoticesAccordion.innerHTML = '<div class="external_notices accordion-collapse collapse"></div>';
52+
53+
const senderEmail = document.querySelector('#contact_info')?.textContent.match(EMAIL_REGEX)[0];
54+
55+
if (handleBlockedStatus(inline, senderEmail)) {
56+
return;
57+
}
58+
59+
const sender = senderEmail + '_external_resources_allowed';
60+
const elements = messageContainer.querySelectorAll('[data-src]');
61+
const blockedResources = [];
62+
63+
elements.forEach(function (element) {
64+
65+
const dataSrc = element.dataset.src;
66+
const senderAllowed = Hm_Utils.get_from_local_storage(sender);
67+
const allowed = Hm_Utils.get_from_local_storage(dataSrc);
68+
69+
switch (Number(allowed) || Number(senderAllowed)) {
70+
case 1:
71+
element.src = dataSrc;
72+
break;
73+
default:
74+
if ((allowed || senderAllowed) === null) {
75+
Hm_Utils.save_to_local_storage(dataSrc, 0);
76+
}
77+
handleInvisibleResource(element, inline);
78+
blockedResources.push(dataSrc);
79+
break;
80+
}
81+
});
82+
83+
const noticesElement = document.createElement('div');
84+
noticesElement.classList.add('notices');
85+
86+
if (blockedResources.length) {
87+
const allowAll = document.createElement('div');
88+
allowAll.classList.add('allow_image_link', 'all', 'fw-bold');
89+
allowAll.textContent = 'For security reasons, external resources have been blocked.';
90+
if (blockedResources.length > 1) {
91+
const allowAllLink = document.createElement('a');
92+
allowAllLink.classList.add('btn', 'btn-light', 'btn-sm');
93+
allowAllLink.href = '#';
94+
allowAllLink.dataset.src = blockedResources.join(',');
95+
allowAllLink.textContent = 'Allow all';
96+
97+
const expandLink = document.createElement('a');
98+
expandLink.classList.add('btn', 'btn-sm', 'd-flex', 'align-items-center', 'gap-2');
99+
expandLink.href = '#';
100+
expandLink.setAttribute('data-bs-toggle', 'collapse');
101+
expandLink.setAttribute('data-bs-target', '.external_notices');
102+
expandLink.innerHTML = 'Show details <i class="bi bi-chevron-down"></i>';
103+
document.querySelector('.external_notices').addEventListener('show.bs.collapse', function () {
104+
expandLink.innerHTML = 'Hide details<i class="bi bi-chevron-up"></i>';
105+
});
106+
document.querySelector('.external_notices').addEventListener('hide.bs.collapse', function () {
107+
expandLink.innerHTML = 'Show details<i class="bi bi-chevron-down"></i>';
108+
});
109+
110+
const linksWrapper = $('<div class="d-inline-flex"></div>');
111+
linksWrapper.append(allowAllLink);
112+
linksWrapper.append(expandLink);
113+
allowAll.appendChild(linksWrapper[0]);
114+
115+
handleAllowResource(allowAll, getParam('part'), inline);
116+
}
117+
noticesElement.insertAdjacentElement('afterbegin', allowAll);
118+
119+
const definitiveActions = $('<div class="definitive_actions ms-auto">From this sender always:</div>');
120+
121+
const button = document.createElement('a');
122+
button.setAttribute('href', '#');
123+
button.classList.add('always_allow_image', 'btn', 'btn-light', 'btn-sm');
124+
button.innerHTML = '<i class="bi bi-check"></i> Allow';
125+
definitiveActions.append(button);
126+
const popover = sessionAvailableOnlyActionInfo(button)
127+
128+
button.addEventListener('click', function (e) {
129+
e.preventDefault();
130+
addSenderToImagesWhitelist(senderEmail).then(refreshMessageContent.bind(null, inline)).finally(() => {
131+
popover.dispose();
132+
})
133+
});
134+
135+
const alwaysBlockButton = $('<a href="#" class="btn btn-light btn-sm ms-2"><i class="bi bi-shield-lock"></i> Block</a>');
136+
const blockPopover = sessionAvailableOnlyActionInfo(alwaysBlockButton[0]);
137+
definitiveActions.append(alwaysBlockButton[0]);
138+
139+
alwaysBlockButton.on('click', function (e) {
140+
e.preventDefault();
141+
addSenderToImagesBlackList(senderEmail).then(refreshMessageContent.bind(null, inline)).finally(() => {
142+
blockPopover.dispose();
143+
})
144+
});
145+
146+
noticesElement.appendChild(definitiveActions[0]);
147+
}
148+
149+
document.querySelector('.external_notices').insertAdjacentElement('beforebegin', noticesElement);
150+
};
151+
152+
function handleBlockedStatus(inline, senderEmail) {
153+
if ($('[data-external-resources-blocked="1"]').length) {
154+
const infoElement = $('<div class="fw-bold">External resources from this sender are blocked.</div>');
155+
const button = $('<a href="#" class="btn btn-light btn-sm ms-2"><i class="bi bi-unlock"></i> Reset permissions</a>');
156+
157+
$(infoElement).append(button);
158+
$('#externalNoticesAccordion').append(infoElement);
159+
160+
const popover = sessionAvailableOnlyActionInfo(button[0]);
161+
162+
button.on('click', function (e) {
163+
e.preventDefault();
164+
removeSenderFromImagesBlackList(senderEmail).then(refreshMessageContent.bind(null, inline)).finally(() => {
165+
popover.dispose()
166+
});
167+
});
168+
169+
return true;
170+
}
171+
172+
return false;
173+
}
174+
175+
function refreshMessageContent(inline) {
176+
$('.msg_text_inner').remove();
177+
if (inline) {
178+
inline_imap_msg(window.inline_msg_details, window.inline_msg_uid);
179+
} else {
180+
get_message_content(getParam('part'), getMessageUidParam(), getListPathParam(), getParam('list_parent'), false, false, false)
181+
}
182+
}
183+
184+
const observeMessageTextMutationAndHandleExternalResources = (inline) => {
185+
const message = document.querySelector('.msg_text');
186+
if (message) {
187+
new MutationObserver(function (mutations) {
188+
mutations.forEach(function (mutation) {
189+
if (mutation.addedNodes.length > 0) {
190+
mutation.addedNodes.forEach(function (node) {
191+
if (node.classList.contains('msg_text_inner') && !message.querySelector('.external_notices')) {
192+
handleExternalResources(inline);
193+
}
194+
});
195+
}
196+
});
197+
}).observe(message, {
198+
childList: true
199+
});
200+
}
201+
};

modules/core/message_functions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function format_msg_html($str, $images=false) {
3535
foreach ($html_tags as $tag) {
3636
$def->addAttribute($tag, 'data-src', 'Text');
3737
}
38+
$def->addAttribute('div', 'data-external-resources-blocked', 'Text');
3839
}
3940

4041
try {

modules/core/output_modules.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2492,7 +2492,13 @@ class Hm_Output_privacy_settings extends Hm_Output_Module {
24922492
'label' => 'External images whitelist',
24932493
'description' => 'Cypht automatically prevents untrusted external images from loading in messages. Add senders from whom you want to allow images to load.',
24942494
'separator' => ','
2495-
]
2495+
],
2496+
'images_blacklist' => [
2497+
'type' => 'text',
2498+
'label' => 'External images blacklist',
2499+
'description' => 'Add senders from whom you never want to allow external images to load.',
2500+
'separator' => ','
2501+
],
24962502
];
24972503

24982504
protected function output()

modules/core/setup.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,5 +360,7 @@
360360
'srv_setup_stepper_imap_hide_from_c_page' => FILTER_VALIDATE_BOOLEAN,
361361
'images_whitelist' => FILTER_UNSAFE_RAW,
362362
'update' => FILTER_VALIDATE_BOOLEAN,
363+
'images_blacklist' => FILTER_UNSAFE_RAW,
364+
'pop' => FILTER_VALIDATE_BOOLEAN,
363365
)
364366
);

modules/core/site.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,4 +1689,7 @@ pre.msg_source {
16891689
.mobile .msg_text .small_header .col-md-2 { width: 20%; }
16901690
.mobile .msg_text .small_header > div + div { width: 80%; }
16911691
.mobile .msg_text .small_header > div.d-none + div { width: 100%; }
1692+
.notices .definitive_actions {
1693+
margin-left: 0 !important;
1694+
}
16921695
}

0 commit comments

Comments
 (0)