#include "e_mod_main.h" #include "e_mod_popup.h" #include "iwd/iwd_manager.h" #include "iwd/iwd_device.h" #include "iwd/iwd_network.h" #include "iwd/iwd_agent.h" #include "iwd/iwd_labels.h" #include "ui/wifi_auth.h" #include "ui/wifi_hidden.h" #include #include #include typedef struct _Popup { E_Gadcon_Popup *gp; Evas_Object *box; Evas_Object *status_lbl; Evas_Object *list; Evas_Object *btn_scan; Evas_Object *btn_toggle; Evas_Object *btn_hidden; Evas_Object *btn_disconnect; /* shown only when connected */ Evas_Object *action_row; Eina_Bool listening; } Popup; static Popup *_popup = NULL; /* Pending passphrase request from the agent — only one at a time. */ static Iwd_Agent_Request *_pending_req = NULL; /* Tracked so iwd's Cancel(reason) can tear down the dialog. */ static Evas_Object *_pending_dialog = NULL; /* One-shot passphrase pre-armed by the hidden-network dialog. Bound to the * SSID it was entered for, with a timeout that wipes it if iwd never comes * asking — so a stashed passphrase can never leak to an unrelated network. */ static char *_hidden_pending_pass = NULL; static char *_hidden_pending_ssid = NULL; static Ecore_Timer *_hidden_pending_timer = NULL; #define HIDDEN_PASS_TIMEOUT 30.0 /* seconds */ static void _hidden_pending_clear(void) { if (_hidden_pending_pass) { explicit_bzero(_hidden_pending_pass, strlen(_hidden_pending_pass)); free(_hidden_pending_pass); _hidden_pending_pass = NULL; } free(_hidden_pending_ssid); _hidden_pending_ssid = NULL; if (_hidden_pending_timer) { ecore_timer_del(_hidden_pending_timer); _hidden_pending_timer = NULL; } } static Eina_Bool _hidden_pending_timeout(void *data EINA_UNUSED) { _hidden_pending_timer = NULL; _hidden_pending_clear(); return ECORE_CALLBACK_CANCEL; } /* ----- helpers --------------------------------------------------------- */ static int _net_cmp(const void *a, const void *b) { const Iwd_Network *na = a, *nb = b; if (na->connected != nb->connected) return nb->connected - na->connected; if (na->known_path && !nb->known_path) return -1; if (!na->known_path && nb->known_path) return 1; /* Higher signal first within same class. */ int ta = iwd_network_signal_tier(na); int tb = iwd_network_signal_tier(nb); if (ta != tb) return tb - ta; if (!na->ssid) return 1; if (!nb->ssid) return -1; return strcasecmp(na->ssid, nb->ssid); } static const char * _signal_bars(int tier) { switch (tier) { case 4: return "▂▄▆█"; case 3: return "▂▄▆ "; case 2: return "▂▄ "; case 1: return "▂ "; default: return " "; } } /* First device that has a station; used for "Disconnect" and hidden connect. */ static Iwd_Device * _active_device(void) { if (!e_iwd || !e_iwd->manager) return NULL; const Eina_Hash *h = iwd_manager_devices(e_iwd->manager); if (!h) return NULL; Eina_Iterator *it = eina_hash_iterator_data_new((Eina_Hash *)h); Iwd_Device *d, *best = NULL; EINA_ITERATOR_FOREACH(it, d) { if (!d->has_station) continue; if (!best) best = d; if (d->connected_network) { best = d; break; } } eina_iterator_free(it); return best; } /* ----- list rendering -------------------------------------------------- */ /* Click data is a strdup'd object path, freed via the row's EVAS_CALLBACK_DEL. * Holding the Iwd_Network * directly would UAF if iwd vanished (its hash is * cleared in _on_name_vanished) between row paint and click. */ static void _row_path_free(void *data, Evas *e EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { free(data); } static void _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { const char *netpath = data; if (!netpath || !e_iwd || !e_iwd->manager) return; const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; if (!n) return; iwd_manager_clear_error(e_iwd->manager); iwd_network_connect(n); } /* The Iwd_Network captured when the confirmation popup was opened may have * disappeared (scan refresh, iwd restart) by the time the user clicks. We * stash its object path on the popup and re-resolve through the live hash * at click time so a stale pointer is never dereferenced. */ static void _forget_confirm_free(void *data, Evas *e EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { free(data); } static void _forget_confirm_yes(void *data, Evas_Object *obj, void *ev EINA_UNUSED) { const char *netpath = data; Evas_Object *pp = evas_object_data_get(obj, "_eiwd_confirm_popup"); if (netpath && e_iwd && e_iwd->manager) { const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; if (n) iwd_network_forget(n); } if (pp) evas_object_del(pp); } static void _forget_confirm_no(void *data EINA_UNUSED, Evas_Object *obj, void *ev EINA_UNUSED) { Evas_Object *pp = evas_object_data_get(obj, "_eiwd_confirm_popup"); if (pp) evas_object_del(pp); } static void _on_net_forget(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { const char *netpath = data; if (!netpath || !e_iwd || !e_iwd->manager) return; const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; if (!n) return; /* Forget destroys the saved passphrase irreversibly — confirm first. * A stray click on the ✕ next to a known network would otherwise wipe * credentials with no recovery. */ Evas_Object *parent = _popup ? _popup->box : e_comp->elm; Evas_Object *pp = elm_popup_add(parent); char msg[256]; snprintf(msg, sizeof(msg), "Forget saved network %s?
" "The passphrase will be permanently deleted.", n->ssid ? n->ssid : "(hidden)"); elm_object_part_text_set(pp, "title,text", "Forget network"); elm_object_text_set(pp, msg); /* Weak ref by netpath — looked up at click time. Freed when the popup * is destroyed (either button or the user closing the window). */ char *netpath_ref = n->path ? strdup(n->path) : NULL; if (netpath_ref) evas_object_event_callback_add(pp, EVAS_CALLBACK_DEL, _forget_confirm_free, netpath_ref); Evas_Object *yes = elm_button_add(pp); elm_object_text_set(yes, "Forget"); evas_object_data_set(yes, "_eiwd_confirm_popup", pp); evas_object_smart_callback_add(yes, "clicked", _forget_confirm_yes, netpath_ref); elm_object_part_content_set(pp, "button1", yes); Evas_Object *no = elm_button_add(pp); elm_object_text_set(no, "Cancel"); evas_object_data_set(no, "_eiwd_confirm_popup", pp); evas_object_smart_callback_add(no, "clicked", _forget_confirm_no, NULL); elm_object_part_content_set(pp, "button2", no); evas_object_show(pp); } static void _rebuild_list(Popup *p) { if (!p->list || !e_iwd || !e_iwd->manager) return; elm_box_clear(p->list); /* When the radio is off, hide the (now-stale) network list entirely. */ if (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF) return; const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); if (!h) return; /* Snapshot into a list so we can sort. */ Eina_List *items = NULL; Eina_Iterator *it = eina_hash_iterator_data_new((Eina_Hash *)h); Iwd_Network *n; EINA_ITERATOR_FOREACH(it, n) items = eina_list_append(items, n); eina_iterator_free(it); items = eina_list_sort(items, eina_list_count(items), _net_cmp); Eina_List *l; EINA_LIST_FOREACH(items, l, n) { Evas_Object *row = elm_box_add(p->list); elm_box_horizontal_set(row, EINA_TRUE); elm_box_padding_set(row, 4, 0); evas_object_size_hint_weight_set(row, EVAS_HINT_EXPAND, 0); evas_object_size_hint_align_set(row, EVAS_HINT_FILL, 0); Evas_Object *btn = elm_button_add(row); /* Truncate long SSIDs so the row never forces horizontal scrolling. * Count codepoints, not bytes — SSIDs may contain UTF-8 and naive * byte truncation would split a multi-byte sequence. */ const char *raw_ssid = n->ssid ? n->ssid : "(hidden)"; char ssid_buf[64]; const int max_chars = 21; int iindex = 0, prev = 0, chars = 0; while (chars < max_chars && eina_unicode_utf8_next_get(raw_ssid, &iindex)) { prev = iindex; chars++; } if (eina_unicode_utf8_next_get(raw_ssid, &iindex)) { /* more remains — truncate at `prev` and append U+2026 */ int copy = prev < (int)sizeof(ssid_buf) - 4 ? prev : (int)sizeof(ssid_buf) - 4; memcpy(ssid_buf, raw_ssid, copy); memcpy(ssid_buf + copy, "\xe2\x80\xa6", 4); /* "…" + NUL */ raw_ssid = ssid_buf; } char label[256]; snprintf(label, sizeof(label), "%s %s%s [%s]%s", _signal_bars(iwd_network_signal_tier(n)), n->known_path ? "★ " : " ", raw_ssid, iwd_security_label(n->security), n->connected ? " ✔" : ""); elm_object_text_set(btn, label); /* Spoken label: avoids the ▂▄▆█ glyphs and ★/✔ markers which * screen readers announce as raw Unicode. */ char access[256]; snprintf(access, sizeof(access), "%s, signal %d of 4, %s%s%s", raw_ssid, iwd_network_signal_tier(n), iwd_security_label(n->security), n->known_path ? ", saved" : "", n->connected ? ", connected" : ""); elm_object_access_info_set(btn, access); evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0); evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0); /* Pass a copy of the object path, not the Iwd_Network *: the network * may disappear (iwd_dbus name-vanished, scan refresh) before the * user clicks, freeing the struct. The path is re-resolved through * the live hash at click time. The buffer is freed via the button's * EVAS_CALLBACK_DEL when the row is rebuilt or popup torn down. */ char *click_path = n->path ? strdup(n->path) : NULL; if (click_path) evas_object_event_callback_add(btn, EVAS_CALLBACK_DEL, _row_path_free, click_path); evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, click_path); elm_box_pack_end(row, btn); evas_object_show(btn); if (n->known_path) { Evas_Object *fb = elm_button_add(row); elm_object_text_set(fb, "✕"); elm_object_tooltip_text_set(fb, "Forget network"); char facc[128]; snprintf(facc, sizeof(facc), "Forget %s", raw_ssid); elm_object_access_info_set(fb, facc); char *forget_path = n->path ? strdup(n->path) : NULL; if (forget_path) evas_object_event_callback_add(fb, EVAS_CALLBACK_DEL, _row_path_free, forget_path); evas_object_smart_callback_add(fb, "clicked", _on_net_forget, forget_path); elm_box_pack_end(row, fb); evas_object_show(fb); } elm_box_pack_end(p->list, row); evas_object_show(row); } eina_list_free(items); } static void _refresh(Popup *p) { if (!p || !e_iwd || !e_iwd->manager) return; Iwd_State s = iwd_manager_state(e_iwd->manager); /* Radio went off: the stash can no longer be useful (iwd won't ask) * and we'd rather not keep a passphrase resident across a toggle. */ if (s == IWD_STATE_OFF) _hidden_pending_clear(); if (p->status_lbl) { const char *err = iwd_manager_last_error(e_iwd->manager); if (err) { char buf[320]; snprintf(buf, sizeof(buf), "%s — %s", iwd_state_label(s), err); elm_object_text_set(p->status_lbl, buf); } else elm_object_text_set(p->status_lbl, iwd_state_label(s)); } if (p->btn_toggle) elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable"); if (p->btn_scan) elm_object_disabled_set(p->btn_scan, s == IWD_STATE_OFF); if (p->btn_hidden) elm_object_disabled_set(p->btn_hidden, s == IWD_STATE_OFF); if (p->btn_disconnect) { Eina_Bool show = (s == IWD_STATE_CONNECTED); if (show) evas_object_show(p->btn_disconnect); else evas_object_hide(p->btn_disconnect); } _rebuild_list(p); } static void _on_manager_change(void *data, Iwd_Manager *m EINA_UNUSED) { _refresh(data); } /* ----- action buttons -------------------------------------------------- */ static void _on_rescan (void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { if (!e_iwd || !e_iwd->manager) return; iwd_manager_clear_error(e_iwd->manager); iwd_manager_scan_request(e_iwd->manager); } static void _on_toggle(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { if (!e_iwd || !e_iwd->manager) return; iwd_manager_clear_error(e_iwd->manager); Eina_Bool off = (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF); iwd_manager_set_powered(e_iwd->manager, off); } static void _on_disconnect(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { Iwd_Device *dev = _active_device(); if (!dev) return; if (e_iwd && e_iwd->manager) iwd_manager_clear_error(e_iwd->manager); iwd_device_disconnect(dev); } static void _on_hidden_done(void *data EINA_UNUSED, const char *ssid, const char *pass, Eina_Bool ok) { if (!ok || !ssid || !*ssid) return; Iwd_Device *dev = _active_device(); if (!dev) return; /* Pre-arm the agent reply so the next RequestPassphrase from iwd for * *this SSID* is answered automatically. The stash is bound to the SSID * and self-clears after HIDDEN_PASS_TIMEOUT seconds — so a typo'd or * out-of-range SSID cannot cause the passphrase to leak to a later, * unrelated network whose RequestPassphrase happens to land first. */ _hidden_pending_clear(); if (pass && *pass) { _hidden_pending_pass = strdup(pass); _hidden_pending_ssid = strdup(ssid); _hidden_pending_timer = ecore_timer_add(HIDDEN_PASS_TIMEOUT, _hidden_pending_timeout, NULL); } iwd_device_connect_hidden(dev, ssid); } static void _on_hidden(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { wifi_hidden_prompt(_popup ? _popup->box : e_comp->elm, _on_hidden_done, NULL); } /* ----- passphrase plumbing -------------------------------------------- */ static void _on_auth_done(void *data EINA_UNUSED, const char *pass, Eina_Bool ok) { _pending_dialog = NULL; if (!_pending_req) { /* Request was already canceled by iwd; nothing to do. The caller * (wifi_auth) wipes its own copy of `pass` after we return. */ return; } if (ok) iwd_agent_reply (_pending_req, pass ? pass : ""); else iwd_agent_cancel(_pending_req); _pending_req = NULL; } static void _on_agent_cancel(void *data EINA_UNUSED, const char *reason EINA_UNUSED) { /* iwd dropped the auth attempt — close any open dialog. The dialog's * DEL handler will fire _on_auth_done(ok=FALSE), but _pending_req has * already been consumed by iwd, so clear it first to avoid double-cancel. */ _pending_req = NULL; if (_pending_dialog) { Evas_Object *d = _pending_dialog; _pending_dialog = NULL; evas_object_del(d); } } static void _on_passphrase_request(void *data, Iwd_Agent_Request *req, const char *netpath); void e_iwd_popup_install_passphrase_handler(void) { if (e_iwd && e_iwd->manager) { iwd_manager_set_passphrase_handler(e_iwd->manager, _on_passphrase_request, NULL); iwd_manager_set_cancel_handler (e_iwd->manager, _on_agent_cancel, NULL); } } static void _on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const char *netpath) { /* Resolve netpath -> network so we can both match the hidden stash * against the requested SSID *and* show a friendly label in the dialog. */ Iwd_Network *n = NULL; if (e_iwd && e_iwd->manager) { const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); if (h) n = eina_hash_find(h, netpath); } const char *req_ssid = (n && n->ssid) ? n->ssid : NULL; /* Use the hidden-network stash *only* if iwd is asking for the same * SSID we entered it for. Anything else: drop the stash on the floor * and prompt normally. */ if (_hidden_pending_pass && _hidden_pending_ssid && req_ssid && !strcmp(req_ssid, _hidden_pending_ssid)) { iwd_agent_reply(req, _hidden_pending_pass); _hidden_pending_clear(); return; } if (_pending_req) { iwd_agent_cancel(req); return; } _pending_req = req; const char *ssid = req_ssid ? req_ssid : "network"; const char *sec = n ? iwd_security_label(n->security) : NULL; _pending_dialog = wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, ssid, sec, _on_auth_done, NULL); } /* ----- popup lifecycle ------------------------------------------------- */ static void _destroy(void) { if (!_popup) return; if (_popup->listening && e_iwd && e_iwd->manager) iwd_manager_listener_del(e_iwd->manager, _on_manager_change, _popup); if (_popup->gp) e_object_del(E_OBJECT(_popup->gp)); free(_popup); _popup = NULL; /* Drop any pre-armed hidden-network passphrase: if the user closes the * popup before iwd asks, the stash would otherwise sit in the heap until * the 30 s timer fires. The stash is process-global, not popup-scoped. */ _hidden_pending_clear(); } void e_iwd_popup_shutdown(void) { _destroy(); _hidden_pending_clear(); } void e_iwd_popup_close(void) { _destroy(); } void e_iwd_popup_refresh(void) { if (_popup) _refresh(_popup); } void e_iwd_popup_toggle(E_Gadcon_Client *gcc) { if (_popup) { _destroy(); return; } if (!gcc || !e_iwd) return; Popup *p = calloc(1, sizeof(*p)); if (!p) return; _popup = p; p->gp = e_gadcon_popup_new(gcc, EINA_FALSE); Evas *evas = evas_object_evas_get(gcc->o_base); Evas_Object *box = elm_box_add(e_comp->elm); (void)evas; elm_box_padding_set(box, 0, 4); evas_object_size_hint_min_set(box, 240, 320); p->box = box; /* Status line */ Evas_Object *st = elm_label_add(box); p->status_lbl = st; elm_box_pack_end(box, st); evas_object_show(st); /* Network list (vertical box of buttons — keeps deps minimal) */ Evas_Object *scroller = elm_scroller_add(box); evas_object_size_hint_weight_set(scroller, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); evas_object_size_hint_align_set(scroller, EVAS_HINT_FILL, EVAS_HINT_FILL); Evas_Object *list_box = elm_box_add(scroller); evas_object_size_hint_weight_set(list_box, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); evas_object_size_hint_align_set(list_box, EVAS_HINT_FILL, EVAS_HINT_FILL); elm_object_content_set(scroller, list_box); evas_object_show(list_box); elm_box_pack_end(box, scroller); evas_object_show(scroller); p->list = list_box; /* Action row */ Evas_Object *row = elm_box_add(box); elm_box_horizontal_set(row, EINA_TRUE); elm_box_padding_set(row, 4, 0); p->action_row = row; p->btn_scan = elm_button_add(row); elm_object_text_set(p->btn_scan, "Rescan"); evas_object_smart_callback_add(p->btn_scan, "clicked", _on_rescan, NULL); elm_box_pack_end(row, p->btn_scan); evas_object_show(p->btn_scan); p->btn_toggle = elm_button_add(row); elm_object_text_set(p->btn_toggle, "Disable"); evas_object_smart_callback_add(p->btn_toggle, "clicked", _on_toggle, NULL); elm_box_pack_end(row, p->btn_toggle); evas_object_show(p->btn_toggle); p->btn_hidden = elm_button_add(row); elm_object_text_set(p->btn_hidden, "Hidden…"); evas_object_smart_callback_add(p->btn_hidden, "clicked", _on_hidden, NULL); elm_box_pack_end(row, p->btn_hidden); evas_object_show(p->btn_hidden); p->btn_disconnect = elm_button_add(row); elm_object_text_set(p->btn_disconnect, "Disconnect"); evas_object_smart_callback_add(p->btn_disconnect, "clicked", _on_disconnect, NULL); elm_box_pack_end(row, p->btn_disconnect); /* Visibility is driven by _refresh() based on connection state. */ elm_box_pack_end(box, row); evas_object_show(row); evas_object_show(box); e_gadcon_popup_content_set(p->gp, box); e_gadcon_popup_show(p->gp); if (e_iwd->manager) { iwd_manager_listener_add(e_iwd->manager, _on_manager_change, p); p->listening = EINA_TRUE; iwd_manager_set_passphrase_handler(e_iwd->manager, _on_passphrase_request, NULL); iwd_manager_scan_request(e_iwd->manager); } _refresh(p); }