xref: /netbsd-src/external/bsd/wpa/dist/src/utils/browser.c (revision bb6183629cf165db498d8e1f4e2de129f7efb21c)
13c260e60Schristos /*
23c260e60Schristos  * Hotspot 2.0 client - Web browser using WebKit
33c260e60Schristos  * Copyright (c) 2013, Qualcomm Atheros, Inc.
43c260e60Schristos  *
53c260e60Schristos  * This software may be distributed under the terms of the BSD license.
63c260e60Schristos  * See README for more details.
73c260e60Schristos  */
83c260e60Schristos 
93c260e60Schristos #include "includes.h"
10*bb618362Schristos #ifdef USE_WEBKIT2
11*bb618362Schristos #include <webkit2/webkit2.h>
12*bb618362Schristos #else /* USE_WEBKIT2 */
133c260e60Schristos #include <webkit/webkit.h>
14*bb618362Schristos #endif /* USE_WEBKIT2 */
153c260e60Schristos 
163c260e60Schristos #include "common.h"
173c260e60Schristos #include "browser.h"
183c260e60Schristos 
193c260e60Schristos 
203c260e60Schristos struct browser_context {
213c260e60Schristos 	GtkWidget *win;
22*bb618362Schristos 	WebKitWebView *view;
233c260e60Schristos 	int success;
243c260e60Schristos 	int progress;
253c260e60Schristos 	char *hover_link;
263c260e60Schristos 	char *title;
27*bb618362Schristos 	int gtk_main_started;
28*bb618362Schristos 	int quit_gtk_main;
293c260e60Schristos };
303c260e60Schristos 
313c260e60Schristos static void win_cb_destroy(GtkWidget *win, struct browser_context *ctx)
323c260e60Schristos {
333c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s", __func__);
34*bb618362Schristos 	if (ctx->gtk_main_started)
353c260e60Schristos 		gtk_main_quit();
363c260e60Schristos }
373c260e60Schristos 
383c260e60Schristos 
393c260e60Schristos static void browser_update_title(struct browser_context *ctx)
403c260e60Schristos {
413c260e60Schristos 	char buf[100];
423c260e60Schristos 
433c260e60Schristos 	if (ctx->hover_link) {
443c260e60Schristos 		gtk_window_set_title(GTK_WINDOW(ctx->win), ctx->hover_link);
453c260e60Schristos 		return;
463c260e60Schristos 	}
473c260e60Schristos 
483c260e60Schristos 	if (ctx->progress == 100) {
493c260e60Schristos 		gtk_window_set_title(GTK_WINDOW(ctx->win),
503c260e60Schristos 				     ctx->title ? ctx->title :
513c260e60Schristos 				     "Hotspot 2.0 client");
523c260e60Schristos 		return;
533c260e60Schristos 	}
543c260e60Schristos 
553c260e60Schristos 	snprintf(buf, sizeof(buf), "[%d%%] %s", ctx->progress,
563c260e60Schristos 		 ctx->title ? ctx->title : "Hotspot 2.0 client");
573c260e60Schristos 	gtk_window_set_title(GTK_WINDOW(ctx->win), buf);
583c260e60Schristos }
593c260e60Schristos 
603c260e60Schristos 
61*bb618362Schristos static void process_request_starting_uri(struct browser_context *ctx,
62*bb618362Schristos 					 const char *uri)
63*bb618362Schristos {
64*bb618362Schristos 	int quit = 0;
65*bb618362Schristos 
66*bb618362Schristos 	if (g_str_has_prefix(uri, "osu://")) {
67*bb618362Schristos 		ctx->success = atoi(uri + 6);
68*bb618362Schristos 		quit = 1;
69*bb618362Schristos 	} else if (g_str_has_prefix(uri, "http://localhost:12345")) {
70*bb618362Schristos 		/*
71*bb618362Schristos 		 * This is used as a special trigger to indicate that the
72*bb618362Schristos 		 * user exchange has been completed.
73*bb618362Schristos 		 */
74*bb618362Schristos 		ctx->success = 1;
75*bb618362Schristos 		quit = 1;
76*bb618362Schristos 	}
77*bb618362Schristos 
78*bb618362Schristos 	if (quit) {
79*bb618362Schristos 		if (ctx->gtk_main_started) {
80*bb618362Schristos 			gtk_main_quit();
81*bb618362Schristos 			ctx->gtk_main_started = 0;
82*bb618362Schristos 		} else {
83*bb618362Schristos 			ctx->quit_gtk_main = 1;
84*bb618362Schristos 		}
85*bb618362Schristos 	}
86*bb618362Schristos }
87*bb618362Schristos 
88*bb618362Schristos 
89*bb618362Schristos #ifdef USE_WEBKIT2
90*bb618362Schristos 
91*bb618362Schristos static void view_cb_notify_estimated_load_progress(WebKitWebView *view,
92*bb618362Schristos 						   GParamSpec *pspec,
93*bb618362Schristos 						   struct browser_context *ctx)
94*bb618362Schristos {
95*bb618362Schristos 	ctx->progress = 100 * webkit_web_view_get_estimated_load_progress(view);
96*bb618362Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s progress=%d", __func__,
97*bb618362Schristos 		   ctx->progress);
98*bb618362Schristos 	browser_update_title(ctx);
99*bb618362Schristos }
100*bb618362Schristos 
101*bb618362Schristos 
102*bb618362Schristos static void view_cb_resource_load_starting(WebKitWebView *view,
103*bb618362Schristos 					   WebKitWebResource *res,
104*bb618362Schristos 					   WebKitURIRequest *req,
105*bb618362Schristos 					   struct browser_context *ctx)
106*bb618362Schristos {
107*bb618362Schristos 	const gchar *uri = webkit_uri_request_get_uri(req);
108*bb618362Schristos 
109*bb618362Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri);
110*bb618362Schristos 	process_request_starting_uri(ctx, uri);
111*bb618362Schristos }
112*bb618362Schristos 
113*bb618362Schristos 
114*bb618362Schristos static gboolean view_cb_decide_policy(WebKitWebView *view,
115*bb618362Schristos 				      WebKitPolicyDecision *policy,
116*bb618362Schristos 				      WebKitPolicyDecisionType type,
117*bb618362Schristos 				      struct browser_context *ctx)
118*bb618362Schristos {
119*bb618362Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s type=%d", __func__, type);
120*bb618362Schristos 	switch (type) {
121*bb618362Schristos 	case WEBKIT_POLICY_DECISION_TYPE_RESPONSE: {
122*bb618362Schristos 		/* This function makes webkit send a download signal for all
123*bb618362Schristos 		 * unknown mime types. */
124*bb618362Schristos 		WebKitResponsePolicyDecision *response;
125*bb618362Schristos 
126*bb618362Schristos 		response = WEBKIT_RESPONSE_POLICY_DECISION(policy);
127*bb618362Schristos 		if (!webkit_response_policy_decision_is_mime_type_supported(
128*bb618362Schristos 			    response)) {
129*bb618362Schristos 			webkit_policy_decision_download(policy);
130*bb618362Schristos 			return TRUE;
131*bb618362Schristos 		}
132*bb618362Schristos 		break;
133*bb618362Schristos 	}
134*bb618362Schristos 	case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: {
135*bb618362Schristos 		WebKitNavigationPolicyDecision *d;
136*bb618362Schristos 		WebKitNavigationAction *a;
137*bb618362Schristos 		WebKitURIRequest *req;
138*bb618362Schristos 		const gchar *uri;
139*bb618362Schristos 
140*bb618362Schristos 		d = WEBKIT_NAVIGATION_POLICY_DECISION(policy);
141*bb618362Schristos 		a = webkit_navigation_policy_decision_get_navigation_action(d);
142*bb618362Schristos 		req = webkit_navigation_action_get_request(a);
143*bb618362Schristos 		uri = webkit_uri_request_get_uri(req);
144*bb618362Schristos 		wpa_printf(MSG_DEBUG, "BROWSER:%s navigation action: uri=%s",
145*bb618362Schristos 			   __func__, uri);
146*bb618362Schristos 		process_request_starting_uri(ctx, uri);
147*bb618362Schristos 		break;
148*bb618362Schristos 	}
149*bb618362Schristos 	default:
150*bb618362Schristos 		break;
151*bb618362Schristos 	}
152*bb618362Schristos 
153*bb618362Schristos 	return FALSE;
154*bb618362Schristos }
155*bb618362Schristos 
156*bb618362Schristos 
157*bb618362Schristos static void view_cb_mouse_target_changed(WebKitWebView *view,
158*bb618362Schristos 					 WebKitHitTestResult *h,
159*bb618362Schristos 					 guint modifiers,
160*bb618362Schristos 					 struct browser_context *ctx)
161*bb618362Schristos {
162*bb618362Schristos 	WebKitHitTestResultContext hc = webkit_hit_test_result_get_context(h);
163*bb618362Schristos 	const char *uri = NULL;
164*bb618362Schristos 
165*bb618362Schristos 	if (hc & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK)
166*bb618362Schristos 		uri = webkit_hit_test_result_get_link_uri(h);
167*bb618362Schristos 	else if (hc & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE)
168*bb618362Schristos 		uri = webkit_hit_test_result_get_image_uri(h);
169*bb618362Schristos 	else if (hc & WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA)
170*bb618362Schristos 		uri = webkit_hit_test_result_get_media_uri(h);
171*bb618362Schristos 
172*bb618362Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri ? uri : "N/A");
173*bb618362Schristos 	os_free(ctx->hover_link);
174*bb618362Schristos 	if (uri)
175*bb618362Schristos 		ctx->hover_link = os_strdup(uri);
176*bb618362Schristos 	else
177*bb618362Schristos 		ctx->hover_link = NULL;
178*bb618362Schristos 
179*bb618362Schristos 	browser_update_title(ctx);
180*bb618362Schristos }
181*bb618362Schristos 
182*bb618362Schristos 
183*bb618362Schristos static void view_cb_notify_title(WebKitWebView *view, GParamSpec *ps,
184*bb618362Schristos 				 struct browser_context *ctx)
185*bb618362Schristos {
186*bb618362Schristos 	const char *title;
187*bb618362Schristos 
188*bb618362Schristos 	title = webkit_web_view_get_title(ctx->view);
189*bb618362Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s title=%s", __func__, title);
190*bb618362Schristos 	os_free(ctx->title);
191*bb618362Schristos 	ctx->title = os_strdup(title);
192*bb618362Schristos 	browser_update_title(ctx);
193*bb618362Schristos }
194*bb618362Schristos 
195*bb618362Schristos #else /* USE_WEBKIT2 */
196*bb618362Schristos 
1973c260e60Schristos static void view_cb_notify_progress(WebKitWebView *view, GParamSpec *pspec,
1983c260e60Schristos 				    struct browser_context *ctx)
1993c260e60Schristos {
2003c260e60Schristos 	ctx->progress = 100 * webkit_web_view_get_progress(view);
2013c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s progress=%d", __func__,
2023c260e60Schristos 		   ctx->progress);
2033c260e60Schristos 	browser_update_title(ctx);
2043c260e60Schristos }
2053c260e60Schristos 
2063c260e60Schristos 
2073c260e60Schristos static void view_cb_notify_load_status(WebKitWebView *view, GParamSpec *pspec,
2083c260e60Schristos 				       struct browser_context *ctx)
2093c260e60Schristos {
2103c260e60Schristos 	int status = webkit_web_view_get_load_status(view);
2113c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s load-status=%d uri=%s",
2123c260e60Schristos 		   __func__, status, webkit_web_view_get_uri(view));
213*bb618362Schristos 	if (ctx->quit_gtk_main) {
214*bb618362Schristos 		gtk_main_quit();
215*bb618362Schristos 		ctx->gtk_main_started = 0;
216*bb618362Schristos 	}
2173c260e60Schristos }
2183c260e60Schristos 
2193c260e60Schristos 
2203c260e60Schristos static void view_cb_resource_request_starting(WebKitWebView *view,
2213c260e60Schristos 					      WebKitWebFrame *frame,
2223c260e60Schristos 					      WebKitWebResource *res,
2233c260e60Schristos 					      WebKitNetworkRequest *req,
2243c260e60Schristos 					      WebKitNetworkResponse *resp,
2253c260e60Schristos 					      struct browser_context *ctx)
2263c260e60Schristos {
2273c260e60Schristos 	const gchar *uri = webkit_network_request_get_uri(req);
228*bb618362Schristos 
2293c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri);
2303c260e60Schristos 	if (g_str_has_suffix(uri, "/favicon.ico"))
2313c260e60Schristos 		webkit_network_request_set_uri(req, "about:blank");
232*bb618362Schristos 
233*bb618362Schristos 	process_request_starting_uri(ctx, uri);
2343c260e60Schristos }
2353c260e60Schristos 
2363c260e60Schristos 
2373c260e60Schristos static gboolean view_cb_mime_type_policy_decision(
2383c260e60Schristos 	WebKitWebView *view, WebKitWebFrame *frame, WebKitNetworkRequest *req,
2393c260e60Schristos 	gchar *mime, WebKitWebPolicyDecision *policy,
2403c260e60Schristos 	struct browser_context *ctx)
2413c260e60Schristos {
2423c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s mime=%s", __func__, mime);
2433c260e60Schristos 
2443c260e60Schristos 	if (!webkit_web_view_can_show_mime_type(view, mime)) {
2453c260e60Schristos 		webkit_web_policy_decision_download(policy);
2463c260e60Schristos 		return TRUE;
2473c260e60Schristos 	}
2483c260e60Schristos 
2493c260e60Schristos 	return FALSE;
2503c260e60Schristos }
2513c260e60Schristos 
2523c260e60Schristos 
2533c260e60Schristos static gboolean view_cb_download_requested(WebKitWebView *view,
2543c260e60Schristos 					   WebKitDownload *dl,
2553c260e60Schristos 					   struct browser_context *ctx)
2563c260e60Schristos {
2573c260e60Schristos 	const gchar *uri;
2583c260e60Schristos 	uri = webkit_download_get_uri(dl);
2593c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri);
2603c260e60Schristos 	return FALSE;
2613c260e60Schristos }
2623c260e60Schristos 
2633c260e60Schristos 
2643c260e60Schristos static void view_cb_hovering_over_link(WebKitWebView *view, gchar *title,
2653c260e60Schristos 				       gchar *uri, struct browser_context *ctx)
2663c260e60Schristos {
2673c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s title=%s uri=%s", __func__, title,
2683c260e60Schristos 		   uri);
2693c260e60Schristos 	os_free(ctx->hover_link);
2703c260e60Schristos 	if (uri)
2713c260e60Schristos 		ctx->hover_link = os_strdup(uri);
2723c260e60Schristos 	else
2733c260e60Schristos 		ctx->hover_link = NULL;
2743c260e60Schristos 
2753c260e60Schristos 	browser_update_title(ctx);
2763c260e60Schristos }
2773c260e60Schristos 
2783c260e60Schristos 
2793c260e60Schristos static void view_cb_title_changed(WebKitWebView *view, WebKitWebFrame *frame,
2803c260e60Schristos 				  const char *title,
2813c260e60Schristos 				  struct browser_context *ctx)
2823c260e60Schristos {
2833c260e60Schristos 	wpa_printf(MSG_DEBUG, "BROWSER:%s title=%s", __func__, title);
2843c260e60Schristos 	os_free(ctx->title);
2853c260e60Schristos 	ctx->title = os_strdup(title);
2863c260e60Schristos 	browser_update_title(ctx);
2873c260e60Schristos }
2883c260e60Schristos 
289*bb618362Schristos #endif /* USE_WEBKIT2 */
2903c260e60Schristos 
291*bb618362Schristos 
292*bb618362Schristos int hs20_web_browser(const char *url, int ignore_tls)
2933c260e60Schristos {
2943c260e60Schristos 	GtkWidget *scroll;
2953c260e60Schristos 	WebKitWebView *view;
296*bb618362Schristos #ifdef USE_WEBKIT2
297*bb618362Schristos 	WebKitSettings *settings;
298*bb618362Schristos #else /* USE_WEBKIT2 */
2993c260e60Schristos 	WebKitWebSettings *settings;
300*bb618362Schristos 	SoupSession *s;
301*bb618362Schristos #endif /* USE_WEBKIT2 */
3023c260e60Schristos 	struct browser_context ctx;
3033c260e60Schristos 
3043c260e60Schristos 	memset(&ctx, 0, sizeof(ctx));
3053c260e60Schristos 	if (!gtk_init_check(NULL, NULL))
3063c260e60Schristos 		return -1;
3073c260e60Schristos 
308*bb618362Schristos #ifndef USE_WEBKIT2
3093c260e60Schristos 	s = webkit_get_default_session();
3103c260e60Schristos 	g_object_set(G_OBJECT(s), "ssl-ca-file",
3113c260e60Schristos 		     "/etc/ssl/certs/ca-certificates.crt", NULL);
312*bb618362Schristos 	if (ignore_tls)
3133c260e60Schristos 		g_object_set(G_OBJECT(s), "ssl-strict", FALSE, NULL);
314*bb618362Schristos #endif /* USE_WEBKIT2 */
3153c260e60Schristos 
3163c260e60Schristos 	ctx.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
3173d6c0713Schristos 	gtk_window_set_role(GTK_WINDOW(ctx.win), "Hotspot 2.0 client");
3183c260e60Schristos 	gtk_window_set_default_size(GTK_WINDOW(ctx.win), 800, 600);
3193c260e60Schristos 
3203c260e60Schristos 	scroll = gtk_scrolled_window_new(NULL, NULL);
3213c260e60Schristos 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
3223c260e60Schristos 				       GTK_POLICY_NEVER, GTK_POLICY_NEVER);
3233c260e60Schristos 
3243c260e60Schristos 	g_signal_connect(G_OBJECT(ctx.win), "destroy",
3253c260e60Schristos 			 G_CALLBACK(win_cb_destroy), &ctx);
3263c260e60Schristos 
3273c260e60Schristos 	view = WEBKIT_WEB_VIEW(webkit_web_view_new());
328*bb618362Schristos 	ctx.view = view;
329*bb618362Schristos #ifdef USE_WEBKIT2
330*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "notify::estimated-load-progress",
331*bb618362Schristos 			 G_CALLBACK(view_cb_notify_estimated_load_progress),
332*bb618362Schristos 			 &ctx);
333*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "resource-load-started",
334*bb618362Schristos 			 G_CALLBACK(view_cb_resource_load_starting), &ctx);
335*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "decide-policy",
336*bb618362Schristos 			 G_CALLBACK(view_cb_decide_policy), &ctx);
337*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "mouse-target-changed",
338*bb618362Schristos 			 G_CALLBACK(view_cb_mouse_target_changed), &ctx);
339*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "notify::title",
340*bb618362Schristos 			 G_CALLBACK(view_cb_notify_title), &ctx);
341*bb618362Schristos #else /* USE_WEBKIT2 */
3423c260e60Schristos 	g_signal_connect(G_OBJECT(view), "notify::load-status",
3433c260e60Schristos 			 G_CALLBACK(view_cb_notify_load_status), &ctx);
344*bb618362Schristos 	g_signal_connect(G_OBJECT(view), "notify::progress",
345*bb618362Schristos 			 G_CALLBACK(view_cb_notify_progress), &ctx);
3463c260e60Schristos 	g_signal_connect(G_OBJECT(view), "resource-request-starting",
3473c260e60Schristos 			 G_CALLBACK(view_cb_resource_request_starting), &ctx);
3483c260e60Schristos 	g_signal_connect(G_OBJECT(view), "mime-type-policy-decision-requested",
3493c260e60Schristos 			 G_CALLBACK(view_cb_mime_type_policy_decision), &ctx);
3503c260e60Schristos 	g_signal_connect(G_OBJECT(view), "download-requested",
3513c260e60Schristos 			 G_CALLBACK(view_cb_download_requested), &ctx);
3523c260e60Schristos 	g_signal_connect(G_OBJECT(view), "hovering-over-link",
3533c260e60Schristos 			 G_CALLBACK(view_cb_hovering_over_link), &ctx);
3543c260e60Schristos 	g_signal_connect(G_OBJECT(view), "title-changed",
3553c260e60Schristos 			 G_CALLBACK(view_cb_title_changed), &ctx);
356*bb618362Schristos #endif /* USE_WEBKIT2 */
3573c260e60Schristos 
3583c260e60Schristos 	gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(view));
3593c260e60Schristos 	gtk_container_add(GTK_CONTAINER(ctx.win), GTK_WIDGET(scroll));
3603c260e60Schristos 
3613c260e60Schristos 	gtk_widget_grab_focus(GTK_WIDGET(view));
3623c260e60Schristos 	gtk_widget_show_all(ctx.win);
3633c260e60Schristos 
3643c260e60Schristos 	settings = webkit_web_view_get_settings(view);
3653c260e60Schristos 	g_object_set(G_OBJECT(settings), "user-agent",
3663c260e60Schristos 		     "Mozilla/5.0 (X11; U; Unix; en-US) "
3673c260e60Schristos 		     "AppleWebKit/537.15 (KHTML, like Gecko) "
3683c260e60Schristos 		     "hs20-client/1.0", NULL);
3693c260e60Schristos 	g_object_set(G_OBJECT(settings), "auto-load-images", TRUE, NULL);
3703c260e60Schristos 
371*bb618362Schristos #ifdef USE_WEBKIT2
372*bb618362Schristos 	if (ignore_tls) {
373*bb618362Schristos #if WEBKIT_CHECK_VERSION(2, 32, 0)
374*bb618362Schristos 		WebKitWebContext *wkctx;
375*bb618362Schristos 		WebKitWebsiteDataManager *wkmgr;
376*bb618362Schristos 
377*bb618362Schristos 		wkctx = webkit_web_context_get_default();
378*bb618362Schristos 		wkmgr = webkit_web_context_get_website_data_manager(wkctx);
379*bb618362Schristos 		webkit_website_data_manager_set_tls_errors_policy(
380*bb618362Schristos 			wkmgr, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
381*bb618362Schristos #else
382*bb618362Schristos 		WebKitWebContext *wkctx;
383*bb618362Schristos 
384*bb618362Schristos 		wkctx = webkit_web_context_get_default();
385*bb618362Schristos 		webkit_web_context_set_tls_errors_policy(
386*bb618362Schristos 			wkctx, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
387*bb618362Schristos #endif
388*bb618362Schristos 	}
389*bb618362Schristos #endif /* USE_WEBKIT2 */
390*bb618362Schristos 
3913c260e60Schristos 	webkit_web_view_load_uri(view, url);
3923c260e60Schristos 
393*bb618362Schristos 	ctx.gtk_main_started = 1;
3943c260e60Schristos 	gtk_main();
3953c260e60Schristos 	gtk_widget_destroy(ctx.win);
3963c260e60Schristos 	while (gtk_events_pending())
3973c260e60Schristos 		gtk_main_iteration();
3983c260e60Schristos 
3993c260e60Schristos 	free(ctx.hover_link);
4003c260e60Schristos 	free(ctx.title);
4013c260e60Schristos 	return ctx.success;
4023c260e60Schristos }
403