[geary/wip/765516-gtk-widget-conversation-viewer: 10/112] Renable and update code for clicking on links in messages.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/765516-gtk-widget-conversation-viewer: 10/112] Renable and update code for clicking on links in messages.
- Date: Thu, 11 Aug 2016 14:42:30 +0000 (UTC)
commit 33a01e745740eb65c4a8db5b226fd59848feb832
Author: Michael James Gratton <mike vee net>
Date: Sun Apr 10 10:46:49 2016 +1000
Renable and update code for clicking on links in messages.
Requires GTK+ 3.12.
* src/client/conversation-viewer/conversation-message.vala
(ConversationMessage::link_selected): Added to pass through successful
link clicks to the message_viewer, hook it up to the web_view.
(ConversationMessage::load_message_body): Hook up WebKit event handler
for when links are clicked, so phishing links can be intercepted.
(ConversationMessage::on_link_clicked_self): Use a popover to display
phishing warning, recursively check link's offset parent's when
calculating box position, escape link text/href before using it as
markup.
* src/client/conversation-viewer/conversation-viewer.vala
(ConversationViewer::add_message): Ensure unhandled mouse clicks o the
web_view are not used to activate the message's ListBoxRow.
* ui/conversation-message.ui: Add popover (GTK+ 3.12) for phishing links.
.../conversation-viewer/conversation-message.vala | 263 ++++++++++---------
.../conversation-viewer/conversation-viewer.vala | 11 +-
ui/conversation-message.ui | 72 ++++++-
3 files changed, 219 insertions(+), 127 deletions(-)
---
diff --git a/src/client/conversation-viewer/conversation-message.vala
b/src/client/conversation-viewer/conversation-message.vala
index 6aee5c4..8851206 100644
--- a/src/client/conversation-viewer/conversation-message.vala
+++ b/src/client/conversation-viewer/conversation-message.vala
@@ -93,7 +93,14 @@ public class ConversationMessage : Gtk.Box {
[GtkChild]
private Gtk.Box body_box;
+ [GtkChild]
+ private Gtk.Popover link_popover;
+ [GtkChild]
+ private Gtk.Label good_link_label;
+
+ [GtkChild]
+ private Gtk.Label bad_link_label;
// The folder containing the message
private Geary.Folder containing_folder = null; // XXX weak??
@@ -110,6 +117,11 @@ public class ConversationMessage : Gtk.Box {
private Gee.HashMap<string, ReplacedImage> replaced_images = new Gee.HashMap<string, ReplacedImage>();
private Gee.HashSet<string> replaced_content_ids = new Gee.HashSet<string>();
+
+ // Fired when the user clicks a link.
+ public signal void link_selected(string link);
+
+
public ConversationMessage(Geary.Email email, Geary.Folder containing_folder) {
this.email = email;
this.containing_folder = containing_folder;
@@ -179,7 +191,7 @@ public class ConversationMessage : Gtk.Box {
web_view.realize.connect(() => { debug("web_view: realised"); });
web_view.size_allocate.connect(() => { debug("web_view: allocated"); });
- // web_view.link_selected.connect((link) => { link_selected(link); });
+ web_view.link_selected.connect((link) => { link_selected(link); });
// if (email.from != null && email.from.contains_normalized(current_account_information.email)) {
// // XXX set a RO property?
@@ -418,8 +430,15 @@ public class ConversationMessage : Gtk.Box {
debug("Could not get message text. %s", err.message);
}
- body_text = clean_html_markup(body_text, message, out remote_images);
- web_view.load_string(body_text ?? "", "text/html", "UTF8", "");
+ body_text = clean_html_markup(body_text ?? "", message, out remote_images);
+
+ web_view.notify["load-status"].connect((source, param) => {
+ if (web_view.load_status == WebKit.LoadStatus.FINISHED) {
+ bind_event(web_view, "body a", "click",
+ (Callback) on_link_clicked, this);
+ }
+ });
+ web_view.load_string(body_text, "text/html", "UTF8", "");
// XXX The following will probably need to happen after the
// message has been loaded.
@@ -725,128 +744,6 @@ public class ConversationMessage : Gtk.Box {
// }
// }
- // private bool on_link_clicked_self(WebKit.DOM.Element element) {
- // if (!Geary.String.is_empty(element.get_attribute("warning"))) {
- // // A warning is open, so ignore clicks.
- // return true;
- // }
-
- // string? href = element.get_attribute("href");
- // if (Geary.String.is_empty(href))
- // return false;
- // string text = ((WebKit.DOM.HTMLElement) element).get_inner_text();
- // string href_short, text_short;
- // if (!deceptive_text(href, ref text, out href_short, out text_short))
- // return false;
-
- // WebKit.DOM.HTMLElement div = Util.DOM.clone_select(web_view.get_dom_document(),
- // "#link_warning_template");
- // try {
- // div.set_inner_html("""%s %s <span><a href="%s">%s</a></span> %s
- // <span><a href="%s">%s</a></span>""".printf(div.get_inner_html(),
- // _("This link appears to go to"), text, text_short,
- // _("but actually goes to"), href, href_short));
- // div.remove_attribute("id");
- // element.parent_node.insert_before(div, element);
- // element.set_attribute("warning", "open");
-
- // long overhang = div.get_offset_left() + div.get_offset_width() -
- // web_view.get_dom_document().get_body().get_offset_width();
- // if (overhang > 0)
- // div.set_attribute("style", @"margin-left: -$(overhang)px;");
- // } catch (Error error) {
- // warning("Error showing link warning dialog: %s", error.message);
- // }
- // bind_event(web_view, ".link_warning .close_link_warning, .link_warning a", "click",
- // (Callback) on_close_link_warning, this);
- // return true;
- // }
-
- // private void on_close_link_warning(WebKit.DOM.Element element, WebKit.DOM.Event event,
- // ConversationMessage conversation_message) {
- // try {
- // WebKit.DOM.Element warning_div = closest_ancestor(element, ".link_warning");
- // WebKit.DOM.Element link = (WebKit.DOM.Element) warning_div.get_next_sibling();
- // link.remove_attribute("warning");
- // warning_div.parent_node.remove_child(warning_div);
- // } catch (Error error) {
- // warning("Error removing link warning dialog: %s", error.message);
- // }
- // }
-
- // private void on_draft_edit_menu() {
- // get_viewer().edit_draft(email);
- // }
-
- // /*
- // * Test whether text looks like a URI that leads somewhere other than href. The text
- // * will have a scheme prepended if it doesn't already have one, and the short versions
- // * have the scheme skipped and long paths truncated.
- // */
- // private bool deceptive_text(string href, ref string text, out string href_short,
- // out string text_short) {
- // href_short = "";
- // text_short = "";
- // // mailto URLs have a different form, and the worst they can do is pop up a composer,
- // // so we don't trigger on them.
- // if (href.has_prefix("mailto:"))
- // return false;
-
- // // First, does text look like a URI? Right now, just test whether it has
- // // <string>.<string> in it. More sophisticated tests are possible.
- // GLib.MatchInfo text_match, href_match;
- // try {
- // GLib.Regex domain = new GLib.Regex(
- // "([a-z]*://)?" // Optional scheme
- // + "([^\\s:/]+\\.[^\\s:/\\.]+)" // Domain
- // + "(/[^\\s]*)?" // Optional path
- // );
- // if (!domain.match(text, 0, out text_match))
- // return false;
- // if (!domain.match(href, 0, out href_match)) {
- // // If href doesn't look like a URL, something is fishy, so warn the user
- // href_short = href + _(" (Invalid?)");
- // text_short = text;
- // return true;
- // }
- // } catch (Error error) {
- // warning("Error in Regex text for deceptive urls: %s", error.message);
- // return false;
- // }
-
- // // Second, do the top levels of the two domains match? We compare the top n levels,
- // // where n is the minimum of the number of levels of the two domains.
- // string[] href_parts = href_match.fetch_all();
- // string[] text_parts = text_match.fetch_all();
- // string[] text_domain = text_parts[2].down().reverse().split(".");
- // string[] href_domain = href_parts[2].down().reverse().split(".");
- // for (int i = 0; i < text_domain.length && i < href_domain.length; i++) {
- // if (text_domain[i] != href_domain[i]) {
- // if (href_parts[1] == "")
- // href_parts[1] = "http://";
- // if (text_parts[1] == "")
- // text_parts[1] = href_parts[1];
- // string temp;
- // assemble_uris(href_parts, out temp, out href_short);
- // assemble_uris(text_parts, out text, out text_short);
- // return true;
- // }
- // }
- // return false;
- // }
-
- // private void assemble_uris(string[] parts, out string full, out string short_) {
- // full = parts[1] + parts[2];
- // short_ = parts[2];
- // if (parts.length == 4 && parts[3] != "/") {
- // full += parts[3];
- // if (parts[3].length > 20)
- // short_ += parts[3].substring(0, 20) + "…";
- // else
- // short_ += parts[3];
- // }
- // }
-
// private void on_attachment_clicked(string attachment_id) {
// Geary.Attachment? attachment = null;
// try {
@@ -1268,6 +1165,122 @@ public class ConversationMessage : Gtk.Box {
return false;
}
+ /*
+ * Test whether text looks like a URI that leads somewhere other than href. The text
+ * will have a scheme prepended if it doesn't already have one, and the short versions
+ * have the scheme skipped and long paths truncated.
+ */
+ private bool deceptive_text(string href, ref string text, out string href_short,
+ out string text_short) {
+ href_short = "";
+ text_short = "";
+ // mailto URLs have a different form, and the worst they can do is pop up a composer,
+ // so we don't trigger on them.
+ if (href.has_prefix("mailto:"))
+ return false;
+
+ // First, does text look like a URI? Right now, just test whether it has
+ // <string>.<string> in it. More sophisticated tests are possible.
+ GLib.MatchInfo text_match, href_match;
+ try {
+ GLib.Regex domain = new GLib.Regex(
+ "([a-z]*://)?" // Optional scheme
+ + "([^\\s:/]+\\.[^\\s:/\\.]+)" // Domain
+ + "(/[^\\s]*)?" // Optional path
+ );
+ if (!domain.match(text, 0, out text_match))
+ return false;
+ if (!domain.match(href, 0, out href_match)) {
+ // If href doesn't look like a URL, something is fishy, so warn the user
+ href_short = href + _(" (Invalid?)");
+ text_short = text;
+ return true;
+ }
+ } catch (Error error) {
+ warning("Error in Regex text for deceptive urls: %s", error.message);
+ return false;
+ }
+
+ // Second, do the top levels of the two domains match? We compare the top n levels,
+ // where n is the minimum of the number of levels of the two domains.
+ string[] href_parts = href_match.fetch_all();
+ string[] text_parts = text_match.fetch_all();
+ string[] text_domain = text_parts[2].down().reverse().split(".");
+ string[] href_domain = href_parts[2].down().reverse().split(".");
+ for (int i = 0; i < text_domain.length && i < href_domain.length; i++) {
+ if (text_domain[i] != href_domain[i]) {
+ if (href_parts[1] == "")
+ href_parts[1] = "http://";
+ if (text_parts[1] == "")
+ text_parts[1] = href_parts[1];
+ string temp;
+ assemble_uris(href_parts, out temp, out href_short);
+ assemble_uris(text_parts, out text, out text_short);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void assemble_uris(string[] parts, out string full, out string short_) {
+ full = parts[1] + parts[2];
+ short_ = parts[2];
+ if (parts.length == 4 && parts[3] != "/") {
+ full += parts[3];
+ if (parts[3].length > 20)
+ short_ += parts[3].substring(0, 20) + "…";
+ else
+ short_ += parts[3];
+ }
+ }
+
+ private static void on_link_clicked(WebKit.DOM.Element element,
+ WebKit.DOM.Event event,
+ ConversationMessage message) {
+ if (message.on_link_clicked_self(element)) {
+ event.prevent_default();
+ }
+ }
+
+ // Check for possible phishing links, displays a popover if found.
+ // If not, lets it go through to the default handler.
+ private bool on_link_clicked_self(WebKit.DOM.Element element) {
+ string? href = element.get_attribute("href");
+ if (Geary.String.is_empty(href))
+ return false;
+ string text = ((WebKit.DOM.HTMLElement) element).get_inner_text();
+ string href_short, text_short;
+ if (!deceptive_text(href, ref text, out href_short, out text_short))
+ return false;
+
+ // Escape text and especially URLs since we got them from the
+ // HREF, and Gtk.Label.set_markup is a strict parser.
+ good_link_label.set_markup(
+ Markup.printf_escaped("<a href=\"%s\">%s</a>", text, text_short)
+ );
+ bad_link_label.set_markup(
+ Markup.printf_escaped("<a href=\"%s\">%s</a>", href, href_short)
+ );
+
+ // Work out the link's position, update the popover.
+ Gdk.Rectangle link_rect = Gdk.Rectangle();
+ web_view.get_allocation(out link_rect);
+ link_rect.x += (int) element.get_offset_left();
+ link_rect.y += (int) element.get_offset_top();
+ WebKit.DOM.Element? offset_parent = element.get_offset_parent();
+ while (offset_parent != null) {
+ link_rect.x += (int) offset_parent.get_offset_left();
+ link_rect.y += (int) offset_parent.get_offset_top();
+ offset_parent = offset_parent.get_offset_parent();
+ }
+ link_rect.width = (int) element.get_offset_width();
+ link_rect.height = (int) element.get_offset_height();
+ link_popover.set_pointing_to(link_rect);
+
+ link_popover.show();
+ return true;
+ }
+
// private void on_hovering_over_link(string? title, string? url) {
// // Copy the link the user is hovering over. Note that when the user mouses-out,
// // this signal is called again with null for both parameters.
diff --git a/src/client/conversation-viewer/conversation-viewer.vala
b/src/client/conversation-viewer/conversation-viewer.vala
index b77a8cc..21a8e54 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -529,10 +529,19 @@ public class ConversationViewer : Gtk.Stack {
if (messages.contains(email))
return;
+ ConversationMessage message = new ConversationMessage(email, current_folder);
+ message.link_selected.connect((link) => { link_selected(link); });
+ message.web_view.button_release_event.connect_after((event) => {
+ // Consume all non-consumed clicks so the row is not
+ // inadvertently activated after clicking on the
+ // message body.
+ return true;
+ });
+
Gtk.ListBoxRow row = new Gtk.ListBoxRow();
row.get_style_context().add_class("frame");
row.show();
- row.add(new ConversationMessage(email, current_folder));
+ row.add(message);
conversation_listbox.add(row);
messages.add(email);
diff --git a/ui/conversation-message.ui b/ui/conversation-message.ui
index 0b1eb16..4aff05c 100644
--- a/ui/conversation-message.ui
+++ b/ui/conversation-message.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
- <requires lib="gtk+" version="3.10"/>
+ <requires lib="gtk+" version="3.12"/>
<template class="ConversationMessage" parent="GtkBox">
<property name="name">ConversationMessage</property>
<property name="visible">True</property>
@@ -436,4 +436,74 @@
<widget name="date_label"/>
</widgets>
</object>
+ <object class="GtkPopover" id="link_popover">
+ <property name="can_focus">False</property>
+ <property name="relative_to">body_box</property>
+ <property name="position">bottom</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This link appears to go to:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="good_link_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label"><a
href="http://goodlink.com">http://goodlink.com</a></property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">But actually goes to:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="bad_link_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label"><a href="http://badlink.com">http://badlink.com</a></property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <style>
+ <class name="tooltip"/>
+ </style>
+ </object>
</interface>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]