Skip to content

Commit

Permalink
Refactor web extension class for re-use
Browse files Browse the repository at this point in the history
- A new Extension base class supports both Peas plugins and web extensions
- Preferences renders a list based on Extension instead of Peas' PluginManagerView
- Separate loading and activation of web extensions
- Allow loading of web extensions at runtime
- Support icon and description for web extensions

Note: disabling of web extensions is not implemented here.
  • Loading branch information
kalikiana committed Sep 18, 2019
1 parent 435ef6d commit 4e996af
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 50 deletions.
41 changes: 41 additions & 0 deletions core/plugins.vala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@
*/

namespace Midori {
public class Extension : Object {
internal static List<Extension> extensions = null;

public string id { get; set; }
public string name { get; set; }
public string description { get; set; }
public Icon icon { get; set; default = new ThemedIcon.with_default_fallbacks ("libpeas-plugin-symbolic"); }
public bool available { get; set; default = false; }
public bool active { get {
return Midori.CoreSettings.get_default ().get_plugin_enabled (id);
} set {
Midori.CoreSettings.get_default ().set_plugin_enabled (id, value);
} }

construct {
extensions.append (this);
}

internal Extension (string id, string name, string description, Icon icon) {
Object (id: id, name: name, description: description, icon: icon);
}
}

public class Plugins : Peas.Engine, Loggable {
public string builtin_path { get; construct set; }

Expand Down Expand Up @@ -39,6 +62,24 @@ namespace Midori {
var settings = CoreSettings.get_default ();
foreach (var plugin in get_plugin_list ()) {
debug ("Found plugin %s", plugin.get_name ());
if (!plugin.is_builtin ()) {
var extension = new Extension (
"lib%s.so".printf (plugin.get_module_name ()), plugin.get_name (),
plugin.get_description (), new ThemedIcon.with_default_fallbacks (plugin.get_icon_name ()));
try {
extension.available = plugin.is_available ();
extension.notify["active"].connect (() => {
if (extension.active) {
try_load_plugin (plugin);
}
else {
try_unload_plugin (plugin);
}
});
} catch (Error error) {
critical ("Failed to prepare plugin %s", plugin.get_module_name ());
}
}
if (plugin.is_builtin ()
|| settings.get_plugin_enabled ("lib%s.so".printf (plugin.get_module_name ()))) {
if (!try_load_plugin (plugin)) {
Expand Down
36 changes: 35 additions & 1 deletion core/preferences.vala
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,41 @@ namespace Midori {
add (_("Privacy"), box);

box = new Gtk.Box (Gtk.Orientation.VERTICAL, 4);
box.add (new PeasGtk.PluginManagerView (null));
var plugins = new Gtk.ListBox ();
plugins.selection_mode = Gtk.SelectionMode.NONE;
foreach (var extension in Extension.extensions) {
var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
extension.bind_property ("available", row, "sensitive", BindingFlags.SYNC_CREATE);
checkbox = new Gtk.CheckButton ();
extension.bind_property ("active", checkbox, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
row.pack_start (checkbox, false, false, 4);
var icon = new Gtk.Image.from_gicon (extension.icon, Gtk.IconSize.BUTTON);
// Icon size only applies to named icons
if (extension.icon is Gdk.Pixbuf) {
int icon_width = 16, icon_height = 16;
Gtk.icon_size_lookup ((Gtk.IconSize)icon.icon_size, out icon_width, out icon_height);
// Take scale factor into account
icon_width *= scale_factor;
icon_height *= scale_factor;
icon.pixbuf = ((Gdk.Pixbuf)extension.icon).scale_simple (icon_width, icon_height, Gdk.InterpType.BILINEAR);
}
row.pack_start (icon, false, false, 4);
var label = new Gtk.Box (Gtk.Orientation.VERTICAL, 4);
var name = new Gtk.Label (extension.name);
name.ellipsize = Pango.EllipsizeMode.END;
name.xalign = 0.0f;
label.pack_start (name);
var description = new Gtk.Label (extension.description);
description.ellipsize = Pango.EllipsizeMode.END;
description.xalign = 0.0f;
label.pack_start (description);
row.pack_end (label, true, true, 4);
plugins.add (row);
}
var scrolled = new Gtk.ScrolledWindow (null, null);
scrolled.min_content_height = 333;
scrolled.add (plugins);
box.add (scrolled);
box.show_all ();
add (_("Extensions"), box);

Expand Down
134 changes: 85 additions & 49 deletions extensions/web-extensions.vala
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
*/

namespace WebExtension {
public class Extension : Object {
public class Extension : Midori.Extension {
HashTable<string, Bytes> _files;
public File file { get; protected set; }

public string name { get; set; }
public string description { get; set; }
public string? background_page { get; owned set; }
public List<string> background_scripts { get; owned set; }
public List<string> content_scripts { get; owned set; }
Expand All @@ -24,7 +22,7 @@ namespace WebExtension {
public Action? sidebar { get; set; }

public Extension (File file) {
Object (file: file, name: file.get_basename ());
Object (id: file.get_basename (), file: file, name: file.get_basename ());
}

public void add_resource (string resource, Bytes data) {
Expand Down Expand Up @@ -79,6 +77,8 @@ namespace WebExtension {
// "WebExtensionExtensionManager::extension_added" is not a value type
public signal void extension_added (Object extension);

public signal void extension_removed (Object extension);

string? pick_default_icon (Json.Object action) {
if (action.has_member ("default_icon")) {
var node = action.get_member ("default_icon");
Expand All @@ -103,21 +103,17 @@ namespace WebExtension {
return _default;
}

public async void load_from_folder (WebKit.UserContentManager content, File folder) throws Error {
public async void load_from_folder (File folder) throws Error {
debug ("Load web extensions from %s", folder.get_path ());
var enumerator = yield folder.enumerate_children_async (FileAttribute.STANDARD_NAME, 0);
FileInfo info;
while ((info = enumerator.next_file ()) != null) {
var file = folder.get_child (info.get_name ());
string id = file.get_basename ();
if (!Midori.CoreSettings.get_default ().get_plugin_enabled (id)) {
continue;
}

var extension = extensions.lookup (id);
if (extension == null) {
InputStream? stream = null;
extension = new Extension (file);

try {
// Try reading from a ZIP archive ie. .crx (Chrome/ Opera/ Vivaldi), .nex (Opera) or .xpi (Firefox)
Expand All @@ -126,6 +122,7 @@ namespace WebExtension {
var archive = new Archive.Read ();
archive.support_format_zip ();
if (archive.open_filename (file.get_path (), 10240) == Archive.Result.OK) {
extension = new Extension (file);
unowned Archive.Entry entry;
while (archive.next_header (out entry) == Archive.Result.OK) {
if (entry.pathname () == "manifest.json") {
Expand Down Expand Up @@ -153,6 +150,7 @@ namespace WebExtension {
// If we find a manifest, this is a web extension
var manifest_file = file.get_child ("manifest.json");
if (manifest_file.query_exists ()) {
extension = new Extension (file);
stream = new DataInputStream (yield manifest_file.read_async ());
} else {
continue;
Expand All @@ -167,6 +165,9 @@ namespace WebExtension {
if (manifest.has_member ("name")) {
extension.name = manifest.get_string_member ("name");
}
if (manifest.has_member ("description")) {
extension.description = manifest.get_string_member ("description");
}

if (manifest.has_member ("background")) {
var background = manifest.get_object_member ("background");
Expand All @@ -193,8 +194,21 @@ namespace WebExtension {
}
}

if (manifest.has_member ("icons")) {
var node = manifest.get_member ("icons");
if (node.get_node_type () == Json.NodeType.OBJECT) {
foreach (var size in node.get_object ().get_members ()) {
var image = yield extension.get_resource (node.get_object ().get_string_member (size));
// Note: The from_bytes variant has no autodetection
var image_stream = new MemoryInputStream.from_data (image.get_data (), free);
extension.icon = yield new Gdk.Pixbuf.from_stream_async (image_stream, null);
break;
}
}
}

if (manifest.has_member ("sidebar_action")) {
var sidebar = manifest.has_member ("sidebar_action") ? manifest.get_object_member ("sidebar_action") : null;
var sidebar = manifest.get_object_member ("sidebar_action");
if (sidebar != null) {
extension.sidebar = new Action (
pick_default_icon (sidebar),
Expand All @@ -220,33 +234,20 @@ namespace WebExtension {
}
}

extension.available = true;
extension.notify["active"].connect (() => {
if (extension.active) {
extension_added (extension);
} else {
extension_removed (extension);
}
});
extensions.insert (id, extension);
extension_added (extension);
} catch (Error error) {
warning ("Failed to load extension '%s': %s\n", extension.name, error.message);
}
}

foreach (var filename in extension.content_scripts) {
try {
var script = yield extension.get_resource (filename);
content.add_script (new WebKit.UserScript ((string)(script.get_data ()),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserScriptInjectionTime.END,
null, null));
} catch (Error error) {
warning ("Failed to inject content script for '%s': %s", extension.name, filename);
}
}
foreach (var filename in extension.content_styles) {
try {
var stylesheet = yield extension.get_resource (filename);
content.add_style_sheet (new WebKit.UserStyleSheet ((string)(stylesheet.get_data ()),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserStyleLevel.USER,
null, null));
if (extension.active) {
extension_added (extension);
}
} catch (Error error) {
warning ("Failed to inject content stylesheet for '%s': %s", extension.name, filename);
warning ("Failed to load extension '%s': %s\n", extension != null ? extension.name : id, error.message);
}
}
}
Expand Down Expand Up @@ -363,7 +364,7 @@ namespace WebExtension {
tooltip_text = extension.browser_action.title ?? extension.name;
visible = true;
focus_on_click = false;
var icon = new Gtk.Image.from_icon_name ("midori-symbolic", Gtk.IconSize.BUTTON);
var icon = new Gtk.Image.from_icon_name ("libpeas-plugin-symbolic", Gtk.IconSize.BUTTON);
icon.use_fallback = true;
icon.visible = true;
if (extension.browser_action.icon != null) {
Expand Down Expand Up @@ -430,6 +431,31 @@ namespace WebExtension {
}

async void install_extension (Extension extension) throws Error {
var content = browser.tab.get_user_content_manager ();
foreach (var filename in extension.content_scripts) {
try {
var script = yield extension.get_resource (filename);
content.add_script (new WebKit.UserScript ((string)(script.get_data ()),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserScriptInjectionTime.END,
null, null));
} catch (Error error) {
warning ("Failed to inject content script for '%s': %s", extension.name, filename);
}
}

foreach (var filename in extension.content_styles) {
try {
var stylesheet = yield extension.get_resource (filename);
content.add_style_sheet (new WebKit.UserStyleSheet ((string)(stylesheet.get_data ()),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserStyleLevel.USER,
null, null));
} catch (Error error) {
warning ("Failed to inject content stylesheet for '%s': %s", extension.name, filename);
}
}

if (extension.browser_action != null) {
browser.add_button (new Button (extension as Extension));
}
Expand Down Expand Up @@ -476,14 +502,6 @@ namespace WebExtension {
extension_scheme.begin (request);
});

var manager = ExtensionManager.get_default ();
manager.extension_added.connect ((extension) => {
install_extension.begin ((Extension)extension);
});
manager.foreach ((extension) => {
install_extension.begin ((Extension)extension);
});

browser.tabs.add.connect (tab_added);
if (browser.tab != null) {
tab_added (browser.tab);
Expand All @@ -494,18 +512,34 @@ namespace WebExtension {
browser.tabs.add.disconnect (tab_added);

var manager = ExtensionManager.get_default ();
var tab = widget as Midori.Tab;
manager.extension_added.connect ((extension) => {
install_extension.begin ((Extension)extension);
});
manager.extension_removed.connect ((extension) => {
((Extension)extension).available = false;
});
manager.foreach ((extension) => {
if (extension.active) {
install_extension.begin ((Extension)extension);
}
});
}
}

public class App : Peas.ExtensionBase, Midori.AppActivatable {
public Midori.App app { owned get; set; }

var content = tab.get_user_content_manager ();
public void activate () {
var manager = ExtensionManager.get_default ();
// Try and load plugins from build folder
var builtin_path = ((Midori.App)Application.get_default ()).exec_path.get_parent ().get_child ("extensions");
manager.load_from_folder.begin (content, builtin_path);
manager.load_from_folder.begin (builtin_path);
// System-wide plugins
manager.load_from_folder.begin (content, File.new_for_path (Config.PLUGINDIR));
manager.load_from_folder.begin (File.new_for_path (Config.PLUGINDIR));
// Plugins installed by the user
string user_path = Path.build_path (Path.DIR_SEPARATOR_S,
Environment.get_user_data_dir (), Config.PROJECT_NAME, "extensions");
manager.load_from_folder.begin (content, File.new_for_path (user_path));
manager.load_from_folder.begin (File.new_for_path (user_path));
}
}
}
Expand All @@ -514,4 +548,6 @@ namespace WebExtension {
public void peas_register_types(TypeModule module) {
((Peas.ObjectModule)module).register_extension_type (
typeof (Midori.BrowserActivatable), typeof (WebExtension.Browser));
((Peas.ObjectModule)module).register_extension_type (
typeof (Midori.AppActivatable), typeof (WebExtension.App));
}

0 comments on commit 4e996af

Please sign in to comment.