geary/ui/components-web-view.js
Michael Gratton 946f6d786f client: Fix remote images not being loaded for remembered senders/email
Since commit 6a0ad721 landed the client can no longer reply on
pre-filling JS page state before loading the message body HTML, since
it would be cleared when the JS page state is initialised in the
window-object-cleared handler.

Instead, set the load remote flag as a GObject data property on the
WebKit.WebPage object in the web process via the extension, and consult
that when determining whether to allow loading remote content.

Fixes #1170
2021-04-05 14:14:46 +10:00

184 lines
6 KiB
JavaScript

/*
* Copyright © 2016-2020 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Application logic for Components.WebView and subclasses.
*/
let PageState = function() {
this.init.apply(this, arguments);
};
PageState.prototype = {
init: function() {
this.isLoaded = false;
this.undoEnabled = false;
this.redoEnabled = false;
this.hasSelection = false;
this.lastPreferredHeight = 0;
this._selectionChanged = MessageSender("selection_changed");
this._contentLoaded = MessageSender("content_loaded");
this._preferredHeightChanged = MessageSender("preferred_height_changed");
this._commandStackChanged = MessageSender("command_stack_changed");
this._documentModified = MessageSender("document_modified");
// Set up an observer to keep track of modifications made to
// the document when editing.
let modifiedId = null;
this.bodyObserver = new MutationObserver((records) => {
if (modifiedId == null) {
modifiedId = window.setTimeout(() => {
this.documentModified();
this.checkCommandStack();
modifiedId = null;
}, 1000);
}
});
this.heightObserver = new ResizeObserver((entries) => {
this.updatePreferredHeight();
});
document.addEventListener("DOMContentLoaded", (e) => {
this.heightObserver.observe(window.document.documentElement);
this.loaded();
});
document.addEventListener("selectionchange", (e) => {
this.selectionChanged();
});
this.testResult = null;
},
getPreferredHeight: function() {
// Return the scroll height of the HTML element since the BODY
// may have margin/border/padding and we want to know
// precisely how high the widget needs to be to avoid
// scrolling.
return window.document.documentElement.scrollHeight;
},
getHtml: function() {
return document.body.innerHTML;
},
loaded: function() {
this.isLoaded = true;
// Always fire a preferred height update first so that it will
// be vaguegly correct when notifying of the HTML load
// completing.
this.updatePreferredHeight();
this._contentLoaded();
},
loadRemoteResources: function() {
const TYPES = "*[src], *[srcset]";
for (const element of document.body.querySelectorAll(TYPES)) {
let src = "";
try {
src = element.src;
} catch (e) {
// fine
}
if (src != "") {
element.src = "";
element.src = src;
}
let srcset = "";
try {
srcset = element.srcset;
} catch (e) {
// fine
}
if (srcset != "") {
element.srcset = "";
element.srcset = srcset;
}
}
},
setEditable: function(enabled) {
if (!enabled) {
this.stopBodyObserver();
}
document.body.contentEditable = enabled;
if (enabled) {
// Enable modification observation only after the document
// has been set editable as WebKit will alter some attrs
this.startBodyObserver();
}
},
startBodyObserver: function() {
let config = {
attributes: true,
childList: true,
characterData: true,
subtree: true
};
this.bodyObserver.observe(document.body, config);
},
stopBodyObserver: function() {
this.bodyObserver.disconnect();
},
/**
* Sends "preferredHeightChanged" message if it has changed.
*/
updatePreferredHeight: function(height) {
if (height === undefined) {
height = this.getPreferredHeight();
}
// Don't send the message until after the DOM has been fully
// loaded and processed by any derived classes. Since
// ConversationPageState may collapse any quotes, sending the
// current preferred height before then may send a value that
// is too large, causing the message body view to grow then
// shrink again, leading to visual flicker.
if (this.isLoaded && height > 0 && height != this.lastPreferredHeight) {
this.lastPreferredHeight = height;
this._preferredHeightChanged(height);
}
},
checkCommandStack: function() {
let canUndo = document.queryCommandEnabled("undo");
let canRedo = document.queryCommandEnabled("redo");
if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
this.undoEnabled = canUndo;
this.redoEnabled = canRedo;
this._commandStackChanged(this.undoEnabled, this.redoEnabled);
}
},
documentModified: function(element) {
this._documentModified();
},
selectionChanged: function() {
let hasSelection = !window.getSelection().isCollapsed;
if (this.hasSelection != hasSelection) {
this.hasSelection = hasSelection;
this._selectionChanged(hasSelection);
}
},
// Methods below are for unit tests.
testVoid: function() {
this.testResult = "void";
},
testReturn: function(value) {
this.testResult = value;
return value;
},
testThrow: function(value) {
this.testResult = value;
throw this.testResult;
}
};
let MessageSender = function(name) {
return function() {
// Since typeof(arguments) == 'object', convert to an array so
// that Components.WebView.MessageCallback callbacks get
// arrays or tuples rather than dicts as arguments
_GearyWebExtension.send(name, Array.from(arguments));
};
};