Reenable basic deceptive link highlighting.

* bindings/vapi/javascriptcore-4.0.vapi (Object::get_property): Fix
  return type.

* src/client/conversation-viewer/conversation-message.vala (GtkTemplate):
  Hook up to new deceptive_link_clicked signal, remove old DOM-based
  implementation.

* src/client/conversation-viewer/conversation-web-view.vala
  (ConversationWebView): Add new deceptive_link_clicked signal and
  DeceptiveText enum, listen for deceptiveLinkClicked JS message and fire
  signal when received.

* src/client/util/util-webkit.vala (WebKitUtil): Add to_object util function.

* src/engine/util/util-js.vala (Geary.JS): Add to_object and get_property
  util functions.

* ui/conversation-web-view.js (ConversationPageState) Listen for link
  clicks, check for deceptive text and send message if found. Add unit
  tests for deceptive text check.

* test/js/composer-page-state-test.vala: Move ::run_javascript to parent
  class so new ConversationPageStateTest class can use it, adapt call
  sites to different parent signature.
This commit is contained in:
Michael James Gratton 2017-01-24 00:05:44 +11:00
parent 69da046ff3
commit 2b5f94da7d
11 changed files with 371 additions and 140 deletions

View file

@ -16,10 +16,24 @@ let ConversationPageState = function() {
ConversationPageState.QUOTE_CONTAINER_CLASS = "geary-quote-container";
ConversationPageState.QUOTE_HIDE_CLASS = "geary-hide";
// Keep these in sync with ConversationWebView
ConversationPageState.NOT_DECEPTIVE = 0;
ConversationPageState.DECEPTIVE_HREF = 1;
ConversationPageState.DECEPTIVE_DOMAIN = 2;
ConversationPageState.prototype = {
__proto__: PageState.prototype,
init: function() {
PageState.prototype.init.apply(this, []);
let state = this;
document.addEventListener("click", function(e) {
if (e.target.tagName == "A" &&
state.linkClicked(e.target)) {
e.preventDefault();
}
}, true);
},
loaded: function() {
this.updateDirection();
@ -209,9 +223,87 @@ ConversationPageState.prototype = {
}
}
return value;
},
linkClicked: function(link) {
let cancelClick = false;
let href = link.href;
if (!href.startsWith("mailto:")) {
let text = link.innerText;
let reason = ConversationPageState.isDeceptiveText(text, href);
if (reason != ConversationPageState.NOT_DECEPTIVE) {
cancelClick = true;
window.webkit.messageHandlers.deceptiveLinkClicked.postMessage({
reason: reason,
text: text,
href: href,
location: ConversationPageState.getNodeBounds(link)
});
}
}
return cancelClick;
}
};
/**
* Returns an [x, y, width, height] array of a node's bounds.
*/
ConversationPageState.getNodeBounds = function(node) {
let x = 0;
let y = 0;
let parent = node;
while (parent != null) {
x += parent.offsetLeft;
y += parent.offsetTop;
parent = parent.offsetParent;
}
return {
x: x,
y: y,
width: node.offsetWidth,
height: node.offsetHeight
};
};
/**
* Test for URL-like `text` that leads somewhere other than `href`.
*/
ConversationPageState.isDeceptiveText = function(text, href) {
// First, does text look like a URI? Right now, just test whether
// it has <string>.<string> in it. More sophisticated tests are
// possible.
let domain = new RegExp("([a-z]*://)?" // Optional scheme
+ "([^\\s:/]+\\.[^\\s:/\\.]+)" // Domain
+ "(/[^\\s]*)?"); // Optional path
let textParts = text.match(domain);
if (textParts == null) {
return ConversationPageState.NOT_DECEPTIVE;
}
let hrefParts = href.match(domain);
if (hrefParts == null) {
// If href doesn't look like a URL, something is fishy, so
// warn the user
return ConversationPageState.DECEPTIVE_HREF;
}
// 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.
let textDomain = textParts[2].toLowerCase().split(".").reverse();
let hrefDomain = hrefParts[2].toLowerCase().split(".").reverse();
let segmentCount = Math.min(textDomain.length, hrefDomain.length);
if (segmentCount == 0) {
return ConversationPageState.DECEPTIVE_DOMAIN;
}
for (let i = 0; i < segmentCount; i++) {
if (textDomain[i] != hrefDomain[i]) {
return ConversationPageState.DECEPTIVE_DOMAIN;
}
}
return ConversationPageState.NOT_DECEPTIVE;
};
ConversationPageState.isDescendantOf = function(node, ancestorTag) {
let ancestor = node.parentNode;
while (ancestor != null) {