Merge lp:~midori/midori/hstsWebExtension into lp:midori

Proposed by Christian Dywan
Status: Work in progress
Proposed branch: lp:~midori/midori/hstsWebExtension
Merge into: lp:midori
Diff against target: 528 lines (+330/-89)
5 files modified
CMakeLists.txt (+1/-1)
extensions/CMakeLists.txt (+92/-82)
extensions/hsts.web.vala (+177/-0)
katze/midori-paths.vala (+10/-6)
midori/main.c (+50/-0)
To merge this branch: bzr merge lp:~midori/midori/hstsWebExtension
Reviewer Review Type Date Requested Status
Midori Devs Pending
Review via email: mp+261897@code.launchpad.net

Commit message

Implement HSTS as a web extension

Description of the change

This branch enables building of separate web extensions.

Note: At this point we probably want to go for libpeas rather than the direct WebKit2 API. That would save us an additional porting step later if we do it in one go. And give us control over extensions that we can't have otherwise.

To post a comment you must log in.

Unmerged revisions

6965. By Christian Dywan

Implement HSTS as a web extension

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2015-03-23 11:33:24 +0000
+++ CMakeLists.txt 2015-06-13 13:38:06 +0000
@@ -268,9 +268,9 @@
268install(FILES AUTHORS COPYING ChangeLog EXPAT README DESTINATION ${CMAKE_INSTALL_DOCDIR})268install(FILES AUTHORS COPYING ChangeLog EXPAT README DESTINATION ${CMAKE_INSTALL_DOCDIR})
269269
270add_subdirectory (midori)270add_subdirectory (midori)
271add_subdirectory (extensions)
272enable_testing()271enable_testing()
273add_subdirectory (tests)272add_subdirectory (tests)
273add_subdirectory (extensions)
274add_subdirectory (po)274add_subdirectory (po)
275add_subdirectory (icons)275add_subdirectory (icons)
276add_subdirectory (data)276add_subdirectory (data)
277277
=== modified file 'extensions/CMakeLists.txt'
--- extensions/CMakeLists.txt 2015-04-19 14:06:12 +0000
+++ extensions/CMakeLists.txt 2015-06-13 13:38:06 +0000
@@ -23,6 +23,10 @@
23 "nsplugin-manager.vala"23 "nsplugin-manager.vala"
24 "tabs2one.c"24 "tabs2one.c"
25 )25 )
26else ()
27 list(REMOVE_ITEM EXTENSIONS
28 "hsts.web.vala"
29 )
26endif ()30endif ()
2731
28# FIXME: re-enable webmedia extension32# FIXME: re-enable webmedia extension
@@ -36,99 +40,105 @@
36 list(REMOVE_ITEM EXTENSIONS "tabs2one.c")40 list(REMOVE_ITEM EXTENSIONS "tabs2one.c")
37endif()41endif()
3842
39foreach(UNIT_SRC ${EXTENSIONS})43include(ValaPrecompile)
40 string(FIND ${UNIT_SRC} ".c" UNIT_EXTENSION)
41 if (UNIT_EXTENSION GREATER -1)
42 string(REPLACE ".c" "" UNIT ${UNIT_SRC})
43 add_library(${UNIT} MODULE ${UNIT_SRC})
44 target_link_libraries(${UNIT}
45 ${LIBMIDORI}
46 )
47 set_target_properties(${UNIT} PROPERTIES
48 COMPILE_FLAGS ${CFLAGS}
49 )
50 install(TARGETS ${UNIT}
51 LIBRARY DESTINATION ${EXTENSIONDIR}
52 )
53 endif ()
54endforeach ()
5544
56foreach(UNIT_SRC ${EXTENSIONS})45macro(build_extension name)
57 string(FIND ${UNIT_SRC} "." UNIT_EXTENSION)46 set(source "${ARGN}")
58 if (UNIT_EXTENSION EQUAL -1)47 set(SOURCE_VALA "")
59 file(GLOB UNIT_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.c")48 foreach(FILE ${source})
60 file(GLOB UNIT_FILES_VALA RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.vala")49 string(FIND ${FILE} ".web." WEB_EXTENSION)
61 if (UNIT_FILES_VALA)50 string(FIND ${FILE} ".vala" VALA_EXTENSION)
62 include(ValaPrecompile)51 if (WEB_EXTENSION GREATER -1)
63 vala_precompile(UNIT_SRC_C ${UNIT_SRC}52 string(REPLACE ".vala" "" WEB_NAME "${name}_${FILE}")
64 ${UNIT_FILES_VALA}53 string(REPLACE "." "_" WEB_NAME "${WEB_NAME}")
65 PACKAGES54 vala_precompile(WEB_EXTENSION_C "${WEB_NAME}"
55 ${FILE}
56 PACKAGES
66 ${PKGS}57 ${PKGS}
67 OPTIONS58 OPTIONS
68 ${VALAFLAGS}59 ${VALAFLAGS}
69 --use-header="${CMAKE_PROJECT_NAME}-core.h"60 GENERATE_HEADER
70 GENERATE_HEADER61 ${FILE}
71 "${UNIT_SRC}"62 CUSTOM_VAPIS
72 GENERATE_HEADER63 "${CMAKE_SOURCE_DIR}/midori/webkit2gtk-web-extension-4.0.vapi"
73 ${UNIT}64 )
74 CUSTOM_VAPIS65 add_library(${WEB_NAME} MODULE ${WEB_EXTENSION_C})
75 ${EXTRA_VAPIS}66 target_link_libraries(${WEB_NAME}
76 "${CMAKE_SOURCE_DIR}/midori/midori.vapi"67 ${LIBMIDORI2}
77 "${CMAKE_BINARY_DIR}/midori/${LIBMIDORI}.vapi"68 )
78 )69 install(TARGETS ${WEB_NAME}
79 set(UNIT_FILES ${UNIT_FILES} ${UNIT_SRC_C})70 LIBRARY DESTINATION ${EXTENSIONDIR}
80 endif ()71 )
81 if (UNIT_FILES)72 # Extensions with Vala code get the lenient VALA_CFLAGS
82 add_library(${UNIT_SRC} MODULE ${UNIT_FILES})73 set_target_properties(${WEB_NAME} PROPERTIES
83 target_link_libraries(${UNIT_SRC}74 COMPILE_FLAGS ${VALA_CFLAGS}
84 ${LIBMIDORI}
85 )75 )
86 install(TARGETS ${UNIT_SRC}76 list(REMOVE_ITEM source ${FILE})
87 LIBRARY DESTINATION ${EXTENSIONDIR}
88 )
89 # extensions with vala code get the lenient VALA_CFLAGS
90 # others get the usual CFLAGS with -Wall and -Werror
91 if (UNIT_FILES_VALA)
92 set_target_properties(${UNIT_SRC} PROPERTIES
93 COMPILE_FLAGS ${VALA_CFLAGS}
94 )
95 else ()
96 set_target_properties(${UNIT_SRC} PROPERTIES
97 COMPILE_FLAGS ${CFLAGS}
98 )
99 endif ()
100 endif ()
101 endif ()
102endforeach ()
10377
104foreach(UNIT_SRC ${EXTENSIONS})78 # Mandatory unit testing
105 string(FIND ${UNIT_SRC} ".vala" UNIT_EXTENSION)79 add_test(NAME "test-${WEB_NAME}" COMMAND $<TARGET_FILE:midori> -t $<TARGET_FILE:${WEB_NAME}>)
106 if (UNIT_EXTENSION GREATER -1)80 contain_test("test-${WEB_NAME}" $<TARGET_FILE:midori> -t $<TARGET_FILE:${WEB_NAME}>)
107 string(REPLACE ".vala" "" UNIT ${UNIT_SRC})81 elseif (VALA_EXTENSION GREATER -1)
108 include(ValaPrecompile)82 list(APPEND SOURCE_VALA ${FILE})
109 vala_precompile(UNIT_SRC_C ${UNIT}83 endif ()
110 ${UNIT_SRC}84 endforeach()
85 if (SOURCE_VALA)
86 vala_precompile(SOURCE_C ${name}
87 ${SOURCE_VALA}
111 PACKAGES88 PACKAGES
112 ${PKGS}89 ${PKGS}
113 OPTIONS90 OPTIONS
114 ${VALAFLAGS}91 ${VALAFLAGS}
115 --use-header="${CMAKE_PROJECT_NAME}-core.h"
116 GENERATE_HEADER92 GENERATE_HEADER
117 ${UNIT}93 ${name}
118 CUSTOM_VAPIS94 CUSTOM_VAPIS
119 ${EXTRA_VAPIS}95 ${EXTRA_VAPIS}
120 "${CMAKE_SOURCE_DIR}/midori/midori.vapi"96 "${CMAKE_SOURCE_DIR}/midori/midori.vapi"
97 # "${CMAKE_SOURCE_DIR}/katze/katze.vapi"
121 "${CMAKE_BINARY_DIR}/midori/${LIBMIDORI}.vapi"98 "${CMAKE_BINARY_DIR}/midori/${LIBMIDORI}.vapi"
122 )99 )
123 add_library(${UNIT} MODULE ${UNIT_SRC_C})100 set(source ${SOURCE_C})
124 target_link_libraries(${UNIT}101 endif ()
125 ${LIBMIDORI}102 if (source)
126 )103 add_library(${name} MODULE ${source})
127 set_target_properties(${UNIT} PROPERTIES104 target_link_libraries(${name}
128 COMPILE_FLAGS "${VALA_CFLAGS}"105 ${LIBMIDORI}
129 )106 )
130 install(TARGETS ${UNIT}107 install(TARGETS ${name}
131 LIBRARY DESTINATION ${EXTENSIONDIR}108 LIBRARY DESTINATION ${EXTENSIONDIR}
132 )109 )
110 if (SOURCE_VALA)
111 # Extensions with Vala code get the lenient VALA_CFLAGS
112 set_target_properties(${name} PROPERTIES
113 COMPILE_FLAGS ${VALA_CFLAGS}
114 )
115 else ()
116 set_target_properties(${name} PROPERTIES
117 COMPILE_FLAGS ${CFLAGS}
118 )
119 endif ()
120 # Optional unit test
121 add_test(NAME "test-${name}" COMMAND $<TARGET_FILE:midori> -t $<TARGET_FILE:${name}>)
122 contain_test("test-${name}" $<TARGET_FILE:midori> -t $<TARGET_FILE:${name}>)
123 endif ()
124endmacro(build_extension)
125
126foreach(UNIT_SRC ${EXTENSIONS})
127 string(FIND ${UNIT_SRC} ".c" UNIT_EXTENSION)
128 if (UNIT_EXTENSION GREATER -1)
129 string(REPLACE ".c" "" UNIT ${UNIT_SRC})
130 build_extension(${UNIT} ${UNIT_SRC})
131 endif ()
132
133 string(FIND ${UNIT_SRC} "." UNIT_EXTENSION)
134 if (UNIT_EXTENSION GREATER -1)
135 file(GLOB UNIT_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.c" "${UNIT_SRC}/*.vala")
136 build_extension(${UNIT_SRC} ${UNIT_FILES})
137 endif ()
138
139 string(FIND ${UNIT_SRC} ".vala" UNIT_EXTENSION)
140 if (UNIT_EXTENSION GREATER -1)
141 string(REPLACE ".vala" "" UNIT ${UNIT_SRC})
142 build_extension(${UNIT} ${UNIT_SRC})
133 endif ()143 endif ()
134endforeach ()144endforeach ()
135145
=== added file 'extensions/hsts.web.vala'
--- extensions/hsts.web.vala 1970-01-01 00:00:00 +0000
+++ extensions/hsts.web.vala 2015-06-13 13:38:06 +0000
@@ -0,0 +1,177 @@
1/*
2 Copyright (C) 2012-2014 Christian Dywan <christian@twotoasts.de>
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Lesser General Public
6 License as published by the Free Software Foundation; either
7 version 2.1 of the License, or (at your option) any later version.
8
9 See the file COPYING for the full license text.
10*/
11
12namespace HSTS {
13 public class Directive {
14 public Soup.Date? expires = null;
15 public bool sub_domains = false;
16
17 public Directive (bool include_sub_domains) {
18 expires = new Soup.Date.from_now (int.MAX);
19 sub_domains = include_sub_domains;
20 }
21
22 public Directive.from_header (string header) {
23 var param_list = Soup.header_parse_param_list (header);
24 if (param_list == null)
25 return;
26
27 string? max_age = param_list.lookup ("max-age");
28 if (max_age != null)
29 expires = new Soup.Date.from_now (max_age.to_int ());
30 if ("includeSubDomains" in header)
31 sub_domains = true;
32 Soup.header_free_param_list (param_list);
33 }
34
35 public bool is_valid () {
36 return expires != null && !expires.is_past ();
37 }
38 }
39
40 public class Whitelist : Object {
41 HashTable<string, Directive> whitelist;
42 string preset = "/etc/xdg/midori/hsts";
43 File config;
44
45 public Whitelist () {
46 whitelist = new HashTable<string, Directive> (str_hash, str_equal);
47 read_cache.begin (File.new_for_path (preset));
48 // FIXME: config = File.new_for_path (Midori.Paths.get_config_filename_for_writing ("hsts"));
49 string config_dir = Path.build_path (Path.DIR_SEPARATOR_S,
50 Environment.get_user_config_dir (), "midori");
51 config = File.new_for_path (Path.build_path (Path.DIR_SEPARATOR_S, config_dir, "hsts"));
52 read_cache.begin (config);
53 }
54
55 async void read_cache (File file) {
56 try {
57 var stream = new DataInputStream (yield file.read_async ());
58 do {
59 string? line = yield stream.read_line_async ();
60 if (line == null)
61 break;
62 string[] parts = line.split (" ", 2);
63 if (parts[0] == null || parts[1] == null)
64 break;
65 var directive = new Directive.from_header (parts[1]);
66 if (directive.is_valid ())
67 append_to_whitelist (parts[0], directive);
68 } while (true);
69 } catch (IOError.NOT_FOUND exist_error) {
70 /* It's no error if no cache file exists */
71 } catch (Error error) {
72 warning ("Failed to read cache %s: %s", file.get_path (), error.message);
73 }
74 }
75
76 public bool should_secure_host (string host) {
77 Directive? directive = whitelist.lookup (host);
78 if (directive == null)
79 directive = whitelist.lookup ("*." + host);
80 return directive != null && directive.is_valid ();
81 }
82
83 void append_to_whitelist (string host, Directive directive) {
84 whitelist.insert (host, directive);
85 if (directive.sub_domains)
86 whitelist.insert ("*." + host, directive);
87 }
88
89 async void append_to_cache (string host, string header) {
90 /* FIXME: Don't write in private browsing */
91
92 try {
93 var stream = yield config.append_to_async (FileCreateFlags.NONE);
94 yield stream.write_async ((host + " " + header + "\n").data);
95 yield stream.flush_async ();
96 }
97 catch (Error error) {
98 critical ("Failed to update %s: %s", config.get_path (), error.message);
99 }
100 }
101
102 public Directive? strict_transport_security_handled (string host, Soup.MessageHeaders headers) {
103 unowned string? hsts = headers.get_one ("Strict-Transport-Security");
104 if (hsts == null)
105 return null;
106
107 var directive = new Directive.from_header (hsts);
108 if (directive.is_valid ()) {
109 append_to_whitelist (host, directive);
110 append_to_cache.begin (host, hsts);
111 }
112 return directive;
113 }
114
115 }
116
117 public class Extension : Object {
118 Whitelist whitelist;
119 bool debug = false;
120
121 public Extension (WebKit.WebExtension extension) {
122 if (strcmp (Environment.get_variable ("MIDORI_DEBUG"), "hsts") == 0)
123 debug = true;
124 whitelist = new Whitelist ();
125 extension.page_created.connect (page_created);
126 }
127
128 void page_created (WebKit.WebPage page) {
129 page.send_request.connect (send_request);
130 }
131
132 bool send_request (WebKit.URIRequest request, WebKit.URIResponse? redirect) {
133 Soup.MessageHeaders? headers = request.get_http_headers ();
134 if (headers == null || !request.uri.contains ("/"))
135 return false;
136
137 string host = request.uri.split ("/")[2];
138 if (whitelist.should_secure_host (host)) {
139 request.uri = request.uri.replace ("http://", "https://");
140 if (debug)
141 stdout.printf ("HSTS: Enforce %s\n", host);
142 } else if (request.uri.has_prefix ("http://")) {
143 var directive = whitelist.strict_transport_security_handled (host, headers);
144 if (debug)
145 stdout.printf ("HSTS: %s valid? %s\n",
146 host, directive != null ? directive.is_valid ().to_string () : "/");
147 }
148 return false;
149 }
150 }
151}
152
153HSTS.Extension? hsts;
154// public void webkit_web_extension_initialize_with_user_data (WebKit.WebExtension extension, Variant user_data) {
155public void webkit_web_extension_initialize (WebKit.WebExtension extension) {
156 // FIXME: Midori.Paths.init_with_user_data (user_data);
157 hsts = new HSTS.Extension (extension);
158}
159
160void hsts_directive () {
161 HSTS.Directive d;
162 d = new HSTS.Directive.from_header ("max-age=31536000");
163 assert (d.is_valid () && !d.sub_domains);
164 d = new HSTS.Directive.from_header ("max-age=15768000 ; includeSubDomains");
165 assert (d.is_valid () && d.sub_domains);
166
167 /* Invalid */
168 d = new HSTS.Directive.from_header ("");
169 assert (!d.is_valid () && !d.sub_domains);
170 d = new HSTS.Directive.from_header ("includeSubDomains");
171 assert (!d.is_valid () && d.sub_domains);
172}
173
174public void extension_test () {
175 Test.add_func ("/hsts/directive", hsts_directive);
176}
177
0178
=== added directory 'ipc'
=== modified file 'katze/midori-paths.vala'
--- katze/midori-paths.vala 2015-06-11 22:33:48 +0000
+++ katze/midori-paths.vala 2015-06-13 13:38:06 +0000
@@ -99,6 +99,7 @@
9999
100 public static void init (RuntimeMode new_mode, string? config) {100 public static void init (RuntimeMode new_mode, string? config) {
101 assert (mode == RuntimeMode.UNDEFINED);101 assert (mode == RuntimeMode.UNDEFINED);
102 assert (exec_path != null);
102 assert (new_mode != RuntimeMode.UNDEFINED);103 assert (new_mode != RuntimeMode.UNDEFINED);
103 mode = new_mode;104 mode = new_mode;
104 if (mode == RuntimeMode.PORTABLE || mode == RuntimeMode.PRIVATE)105 if (mode == RuntimeMode.PORTABLE || mode == RuntimeMode.PRIVATE)
@@ -148,16 +149,19 @@
148 tmp_dir = get_runtime_dir ();149 tmp_dir = get_runtime_dir ();
149 }150 }
150#if HAVE_WEBKIT2151#if HAVE_WEBKIT2
152 var context = WebKit.WebContext.get_default ();
153 context.set_web_extensions_directory (get_lib_path (PACKAGE_NAME));
154 context.initialize_web_extensions.connect (() => {
155 /* TODO: Pass user data */
156 context.set_web_extensions_initialization_user_data ("");
157 });
151 if (cache_dir != null) {158 if (cache_dir != null) {
152 /* Cache and extension dir MUST be set no later than here to work */159 context.set_disk_cache_directory (
153 WebKit.WebContext.get_default ().set_web_extensions_directory (
154 Path.build_path (Path.DIR_SEPARATOR_S, cache_dir, "wk2ext"));
155 WebKit.WebContext.get_default ().set_disk_cache_directory (
156 Path.build_path (Path.DIR_SEPARATOR_S, cache_dir, "web"));160 Path.build_path (Path.DIR_SEPARATOR_S, cache_dir, "web"));
157 }161 }
158162
159 if (config_dir != null) {163 if (config_dir != null) {
160 var cookie_manager = WebKit.WebContext.get_default ().get_cookie_manager ();164 var cookie_manager = context.get_cookie_manager ();
161 cookie_manager.set_persistent_storage (Path.build_filename (config_dir, "cookies.db"),165 cookie_manager.set_persistent_storage (Path.build_filename (config_dir, "cookies.db"),
162 WebKit.CookiePersistentStorage.SQLITE);166 WebKit.CookiePersistentStorage.SQLITE);
163 }167 }
@@ -165,7 +169,7 @@
165 if (user_data_dir != null) {169 if (user_data_dir != null) {
166 string folder = Path.build_filename (user_data_dir, "webkit", "icondatabase");170 string folder = Path.build_filename (user_data_dir, "webkit", "icondatabase");
167#if HAVE_WEBKIT2171#if HAVE_WEBKIT2
168 WebKit.WebContext.get_default ().set_favicon_database_directory (folder);172 context.set_favicon_database_directory (folder);
169#else173#else
170 WebKit.get_favicon_database ().set_path (folder);174 WebKit.get_favicon_database ().set_path (folder);
171#endif175#endif
172176
=== modified file 'midori/main.c'
--- midori/main.c 2014-04-23 03:34:23 +0000
+++ midori/main.c 2015-06-13 13:38:06 +0000
@@ -62,6 +62,7 @@
62 gchar* config;62 gchar* config;
63 gboolean private;63 gboolean private;
64 gboolean portable;64 gboolean portable;
65 gchar* test;
65 gboolean plain;66 gboolean plain;
66 gboolean diagnostic_dialog = FALSE;67 gboolean diagnostic_dialog = FALSE;
67 gboolean debug = FALSE;68 gboolean debug = FALSE;
@@ -85,6 +86,8 @@
85 { "portable", 'P', 0, G_OPTION_ARG_NONE, &portable,86 { "portable", 'P', 0, G_OPTION_ARG_NONE, &portable,
86 N_("Portable mode, all runtime files are stored in one place"), NULL },87 N_("Portable mode, all runtime files are stored in one place"), NULL },
87 #endif88 #endif
89 { "test", 't', 0, G_OPTION_ARG_STRING, &test,
90 N_("Run unit tests for the specified extension"), NULL },
88 { "plain", '\0', 0, G_OPTION_ARG_NONE, &plain,91 { "plain", '\0', 0, G_OPTION_ARG_NONE, &plain,
89 N_("Plain GTK+ window with WebKit, akin to GtkLauncher"), NULL },92 N_("Plain GTK+ window with WebKit, akin to GtkLauncher"), NULL },
90 { "diagnostic-dialog", 'd', 0, G_OPTION_ARG_NONE, &diagnostic_dialog,93 { "diagnostic-dialog", 'd', 0, G_OPTION_ARG_NONE, &diagnostic_dialog,
@@ -118,6 +121,7 @@
118 config = NULL;121 config = NULL;
119 private = FALSE;122 private = FALSE;
120 portable = FALSE;123 portable = FALSE;
124 test = NULL;
121 plain = FALSE;125 plain = FALSE;
122 run = FALSE;126 run = FALSE;
123 snapshot = NULL;127 snapshot = NULL;
@@ -287,6 +291,52 @@
287 return 0;291 return 0;
288 }292 }
289293
294 if (test != NULL)
295 {
296 g_assert (g_module_supported ());
297
298 GModule* module = g_module_open (test, G_MODULE_BIND_LOCAL);
299 if (module == NULL)
300 g_error (_("Failed to load %s."), test);
301
302 midori_test_init (&argc, &argv);
303
304 typedef void (*extension_test_func)(void);
305 extension_test_func extension_test;
306 /* Midori.Extension */
307 typedef GObject* (*extension_init_func)(void);
308 extension_init_func extension_init;
309 /* WebKit.WebExtension */
310 typedef void (*webkit_web_extension_initialize_func)(GObject* extension);
311 webkit_web_extension_initialize_func web_extension_init;
312
313 if (g_module_symbol (module, "extension_init",
314 (gpointer) &extension_init))
315 {
316 /* It's fine to conditionally return no extension */
317 if (!(extension_init ()))
318 return 0;
319 midori_paths_init (MIDORI_RUNTIME_MODE_NORMAL, NULL);
320
321 /* Not all extensions have unit tests :-( */
322 if (!g_module_symbol (module, "extension_test", (gpointer) &extension_test))
323 return 0;
324 }
325 else if (g_module_symbol (module, "webkit_web_extension_initialize",
326 (gpointer) &web_extension_init)
327 || g_module_symbol (module, "webkit_web_extension_initialize_with_user_data",
328 (gpointer) &web_extension_init))
329 {
330 if (!g_module_symbol (module, "extension_test", (gpointer) &extension_test))
331 g_error (_("%s doesn't provide unit tests."), test);
332 }
333 else
334 g_error (_("%s doesn't look like a Midori extension."), test);
335
336 extension_test ();
337 return g_test_run ();
338 }
339
290 if (plain)340 if (plain)
291 {341 {
292 GtkWidget* window = gtk_window_new (GTK_WINDOW_TOPLEVEL);342 GtkWidget* window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

Subscribers

People subscribed via source and target branches

to all changes: