Skip to content

Commit 4d93140

Browse files
committed
πŸ”§ chore: Full project audit: security, a11y, quality, compat fixes
Audit & Infrastructure - Add PLANNING.md with prioritized fix roadmap + post-fix metrics - Fix README: Python β†’ Rust, update dependency list - cargo fmt: 74 diffs β†’ 0 - cargo clippy: 33 warnings β†’ 0 - cargo audit: 0 vulnerabilities Testing - Add 25 unit tests across 3 crates (service: 10, favicon: 15, registry: 11 deduplicated) - All tests passing, zero regressions Security - URL scheme validation in favicon fetch β†’ block non-http(s) (anti-SSRF) - Permission handler: log auto-granted permission type + TODO - ITP disabled: add justification comment Accessibility - Label ~15 icon-only buttons (back, fwd, reload, fullscreen, menu, search, add, browser, edit, delete, template, detect) - Add live region (status_label with AccessibleRole::Status) for search result count announcements - Set AccessibleRole::Heading on category headers in list + gallery Robustness - Replace 2 unwrap() β†’ expect() with context messages - Geometry parse: match + log::warn on failure instead of silent ignore - Poll interval 100ms β†’ 250ms to reduce CPU churn UX & Compatibility - Disable browser button for App mode webapps (no external browser) - Set Chrome 131 User-Agent in viewer β†’ fix Spotify/Teams blocking WebKitGTK as "incompatible browser"
1 parent 66aa57a commit 4d93140

19 files changed

Lines changed: 1523 additions & 465 deletions

File tree

β€ŽPLANNING.mdβ€Ž

Lines changed: 342 additions & 206 deletions
Large diffs are not rendered by default.

β€ŽPLANNING.old.mdβ€Ž

Lines changed: 319 additions & 0 deletions
Large diffs are not rendered by default.

β€ŽREADME.mdβ€Ž

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ A modern GTK4 tool to create and manage webapps, supporting multiple browsers wh
1414

1515
## Technical Details
1616

17-
- Built with Python using GTK4 and libadwaita
17+
- Built with Rust using GTK4 and libadwaita
18+
- WebKitGTK 6.0 for webapp viewer with isolated profiles
1819
- Uses website scraping to extract icons and metadata
1920
- Integrated with desktop environment via desktop files
2021
- Compatible with both Xorg and Wayland display servers
@@ -54,8 +55,8 @@ GPL-3.0
5455

5556
## Dependencies
5657

57-
- python-bs4
58-
- python-requests
58+
- gtk4 (>= 4.10)
59+
- libadwaita-1 (>= 1.6)
60+
- webkitgtk-6.0 (>= 2.50)
5961
- gettext
60-
- python-pillow
61-
- python-gobject
62+
- openssl

β€Žcrates/webapps-core/src/desktop.rsβ€Ž

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ pub fn desktop_file_id(url: &str) -> String {
104104
/// Path for a webapp's .desktop file
105105
pub fn desktop_file_path(webapp: &WebApp) -> PathBuf {
106106
let filename = if webapp.app_file.is_empty() {
107-
format!("biglinux-webapp-{}.desktop", desktop_file_id(&webapp.app_url))
107+
format!(
108+
"biglinux-webapp-{}.desktop",
109+
desktop_file_id(&webapp.app_url)
110+
)
108111
} else {
109112
webapp.app_file.clone()
110113
};
@@ -136,6 +139,14 @@ pub fn remove_desktop_entry(webapp: &WebApp) -> Result<()> {
136139
/// Strip chars that could break desktop file Exec or shell parsing
137140
fn sanitize_desktop_field(s: &str) -> String {
138141
s.chars()
139-
.filter(|c| *c != '"' && *c != '\'' && *c != '`' && *c != '\\' && *c != '\n' && *c != '\r' && *c != '$')
142+
.filter(|c| {
143+
*c != '"'
144+
&& *c != '\''
145+
&& *c != '`'
146+
&& *c != '\\'
147+
&& *c != '\n'
148+
&& *c != '\r'
149+
&& *c != '$'
150+
})
140151
.collect()
141152
}

β€Žcrates/webapps-core/src/models/webapp.rsβ€Ž

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ impl WebAppCollection {
179179
if query.is_empty() {
180180
return self.webapps.iter().collect();
181181
}
182-
self.webapps.iter().filter(|app| app.matches(query)).collect()
182+
self.webapps
183+
.iter()
184+
.filter(|app| app.matches(query))
185+
.collect()
183186
}
184187

185188
pub fn categorized(&self, query: Option<&str>) -> HashMap<String, Vec<&WebApp>> {

β€Žcrates/webapps-core/src/templates/google.rsβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
1919
keywords: svec!["google", "docs", "document", "text"],
2020
mime_types: svec![
2121
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22-
"application/msword", "application/rtf", "text/plain"
22+
"application/msword",
23+
"application/rtf",
24+
"text/plain"
2325
],
2426
file_handler: FileHandler::Upload,
2527
profile: "google".into(),
@@ -36,7 +38,8 @@ pub fn templates() -> Vec<WebAppTemplate> {
3638
keywords: svec!["google", "sheets", "spreadsheet", "csv", "excel"],
3739
mime_types: svec![
3840
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
39-
"application/vnd.ms-excel", "text/csv"
41+
"application/vnd.ms-excel",
42+
"text/csv"
4043
],
4144
file_handler: FileHandler::Upload,
4245
profile: "google".into(),
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
mod registry;
2-
mod office365;
3-
mod google;
41
mod communication;
2+
mod google;
53
mod media;
4+
mod office365;
65
mod productivity;
6+
mod registry;
77

8-
pub use registry::{WebAppTemplate, TemplateRegistry, FileHandler, build_default_registry};
8+
pub use registry::{build_default_registry, FileHandler, TemplateRegistry, WebAppTemplate};

β€Žcrates/webapps-core/src/templates/office365.rsβ€Ž

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ pub fn templates() -> Vec<WebAppTemplate> {
1111
comment: "Edit documents online with Microsoft Word".into(),
1212
generic_name: "Word Processor".into(),
1313
keywords: vec!["word", "document", "office", "docx", "microsoft"]
14-
.into_iter().map(Into::into).collect(),
14+
.into_iter()
15+
.map(Into::into)
16+
.collect(),
1517
mime_types: vec![
1618
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
17-
"application/msword", "application/rtf", "text/rtf",
18-
].into_iter().map(Into::into).collect(),
19+
"application/msword",
20+
"application/rtf",
21+
"text/rtf",
22+
]
23+
.into_iter()
24+
.map(Into::into)
25+
.collect(),
1926
file_handler: FileHandler::Upload,
2027
profile: "office365".into(),
2128
..Default::default()
@@ -29,11 +36,18 @@ pub fn templates() -> Vec<WebAppTemplate> {
2936
comment: "Edit spreadsheets online with Microsoft Excel".into(),
3037
generic_name: "Spreadsheet".into(),
3138
keywords: vec!["excel", "spreadsheet", "office", "xlsx", "csv", "microsoft"]
32-
.into_iter().map(Into::into).collect(),
39+
.into_iter()
40+
.map(Into::into)
41+
.collect(),
3342
mime_types: vec![
3443
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
35-
"application/vnd.ms-excel", "text/csv", "application/csv",
36-
].into_iter().map(Into::into).collect(),
44+
"application/vnd.ms-excel",
45+
"text/csv",
46+
"application/csv",
47+
]
48+
.into_iter()
49+
.map(Into::into)
50+
.collect(),
3751
file_handler: FileHandler::Upload,
3852
profile: "office365".into(),
3953
..Default::default()
@@ -46,12 +60,24 @@ pub fn templates() -> Vec<WebAppTemplate> {
4660
category: "Office".into(),
4761
comment: "Create presentations online with Microsoft PowerPoint".into(),
4862
generic_name: "Presentation".into(),
49-
keywords: vec!["powerpoint", "presentation", "office", "pptx", "slides", "microsoft"]
50-
.into_iter().map(Into::into).collect(),
63+
keywords: vec![
64+
"powerpoint",
65+
"presentation",
66+
"office",
67+
"pptx",
68+
"slides",
69+
"microsoft",
70+
]
71+
.into_iter()
72+
.map(Into::into)
73+
.collect(),
5174
mime_types: vec![
5275
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
5376
"application/vnd.ms-powerpoint",
54-
].into_iter().map(Into::into).collect(),
77+
]
78+
.into_iter()
79+
.map(Into::into)
80+
.collect(),
5581
file_handler: FileHandler::Upload,
5682
profile: "office365".into(),
5783
..Default::default()
@@ -65,7 +91,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
6591
comment: "Take notes online with Microsoft OneNote".into(),
6692
generic_name: "Note Taking".into(),
6793
keywords: vec!["onenote", "notes", "office", "microsoft"]
68-
.into_iter().map(Into::into).collect(),
94+
.into_iter()
95+
.map(Into::into)
96+
.collect(),
6997
profile: "office365".into(),
7098
..Default::default()
7199
},
@@ -78,7 +106,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
78106
comment: "Email and calendar from Microsoft Outlook".into(),
79107
generic_name: "Email Client".into(),
80108
keywords: vec!["outlook", "email", "mail", "calendar", "microsoft"]
81-
.into_iter().map(Into::into).collect(),
109+
.into_iter()
110+
.map(Into::into)
111+
.collect(),
82112
features: vec!["notifications".into()],
83113
profile: "office365".into(),
84114
..Default::default()
@@ -92,7 +122,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
92122
comment: "Chat and video conferencing with Microsoft Teams".into(),
93123
generic_name: "Instant Messaging".into(),
94124
keywords: vec!["teams", "chat", "video", "conferencing", "microsoft"]
95-
.into_iter().map(Into::into).collect(),
125+
.into_iter()
126+
.map(Into::into)
127+
.collect(),
96128
features: vec!["notifications".into(), "camera".into(), "microphone".into()],
97129
profile: "office365".into(),
98130
..Default::default()
@@ -106,7 +138,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
106138
comment: "Cloud storage from Microsoft OneDrive".into(),
107139
generic_name: "Cloud Storage".into(),
108140
keywords: vec!["onedrive", "cloud", "storage", "files", "microsoft"]
109-
.into_iter().map(Into::into).collect(),
141+
.into_iter()
142+
.map(Into::into)
143+
.collect(),
110144
profile: "office365".into(),
111145
..Default::default()
112146
},
@@ -119,7 +153,9 @@ pub fn templates() -> Vec<WebAppTemplate> {
119153
comment: "Microsoft 365 home β€” access all Office apps".into(),
120154
generic_name: "Office Suite".into(),
121155
keywords: vec!["office", "365", "microsoft", "word", "excel", "powerpoint"]
122-
.into_iter().map(Into::into).collect(),
156+
.into_iter()
157+
.map(Into::into)
158+
.collect(),
123159
profile: "office365".into(),
124160
..Default::default()
125161
},

β€Žcrates/webapps-core/src/templates/registry.rsβ€Ž

Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ impl Default for WebAppTemplate {
5050
impl WebAppTemplate {
5151
/// Domain extracted from URL for matching
5252
pub fn domain(&self) -> Option<String> {
53-
url::Url::parse(&self.url)
54-
.ok()
55-
.and_then(|u| u.host_str().map(|h| {
53+
url::Url::parse(&self.url).ok().and_then(|u| {
54+
u.host_str().map(|h| {
5655
let h = h.strip_prefix("www.").unwrap_or(h);
5756
h.to_lowercase()
58-
}))
57+
})
58+
})
5959
}
6060
}
6161

@@ -91,11 +91,7 @@ impl TemplateRegistry {
9191
pub fn get_by_category(&self, category: &str) -> Vec<&WebAppTemplate> {
9292
self.by_category
9393
.get(category)
94-
.map(|ids| {
95-
ids.iter()
96-
.filter_map(|id| self.templates.get(id))
97-
.collect()
98-
})
94+
.map(|ids| ids.iter().filter_map(|id| self.templates.get(id)).collect())
9995
.unwrap_or_default()
10096
}
10197

@@ -137,3 +133,143 @@ pub fn build_default_registry() -> TemplateRegistry {
137133
reg.register_many(super::productivity::templates());
138134
reg
139135
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
141+
fn sample_template(id: &str, name: &str, url: &str, category: &str) -> WebAppTemplate {
142+
WebAppTemplate {
143+
template_id: id.into(),
144+
name: name.into(),
145+
url: url.into(),
146+
category: category.into(),
147+
keywords: vec![name.to_lowercase()],
148+
..Default::default()
149+
}
150+
}
151+
152+
#[test]
153+
fn register_and_get() {
154+
let mut reg = TemplateRegistry::default();
155+
reg.register(sample_template(
156+
"gmail",
157+
"Gmail",
158+
"https://mail.google.com",
159+
"Communication",
160+
));
161+
assert!(reg.get("gmail").is_some());
162+
assert_eq!(reg.get("gmail").unwrap().name, "Gmail");
163+
assert!(reg.get("nonexistent").is_none());
164+
}
165+
166+
#[test]
167+
fn categories_sorted() {
168+
let mut reg = TemplateRegistry::default();
169+
reg.register(sample_template("c", "C", "https://c.com", "Zebra"));
170+
reg.register(sample_template("a", "A", "https://a.com", "Alpha"));
171+
let cats = reg.categories();
172+
assert_eq!(cats, vec!["Alpha", "Zebra"]);
173+
}
174+
175+
#[test]
176+
fn get_by_category() {
177+
let mut reg = TemplateRegistry::default();
178+
reg.register(sample_template(
179+
"g",
180+
"Gmail",
181+
"https://mail.google.com",
182+
"Communication",
183+
));
184+
reg.register(sample_template(
185+
"s",
186+
"Spotify",
187+
"https://spotify.com",
188+
"Media",
189+
));
190+
let comms = reg.get_by_category("Communication");
191+
assert_eq!(comms.len(), 1);
192+
assert_eq!(comms[0].name, "Gmail");
193+
assert!(reg.get_by_category("Nonexistent").is_empty());
194+
}
195+
196+
#[test]
197+
fn match_url_finds_template() {
198+
let mut reg = TemplateRegistry::default();
199+
reg.register(sample_template(
200+
"yt",
201+
"YouTube",
202+
"https://www.youtube.com",
203+
"Media",
204+
));
205+
let found = reg.match_url("https://youtube.com/watch?v=123");
206+
assert!(found.is_some());
207+
assert_eq!(found.unwrap().template_id, "yt");
208+
}
209+
210+
#[test]
211+
fn match_url_no_match() {
212+
let mut reg = TemplateRegistry::default();
213+
reg.register(sample_template(
214+
"yt",
215+
"YouTube",
216+
"https://www.youtube.com",
217+
"Media",
218+
));
219+
assert!(reg.match_url("https://example.com").is_none());
220+
}
221+
222+
#[test]
223+
fn search_by_name() {
224+
let mut reg = TemplateRegistry::default();
225+
reg.register(sample_template(
226+
"g",
227+
"Gmail",
228+
"https://mail.google.com",
229+
"Communication",
230+
));
231+
reg.register(sample_template(
232+
"s",
233+
"Spotify",
234+
"https://spotify.com",
235+
"Media",
236+
));
237+
let results = reg.search("gmail");
238+
assert_eq!(results.len(), 1);
239+
assert_eq!(results[0].name, "Gmail");
240+
}
241+
242+
#[test]
243+
fn search_by_category() {
244+
let mut reg = TemplateRegistry::default();
245+
reg.register(sample_template(
246+
"g",
247+
"Gmail",
248+
"https://mail.google.com",
249+
"Communication",
250+
));
251+
let results = reg.search("communication");
252+
assert_eq!(results.len(), 1);
253+
}
254+
255+
#[test]
256+
fn search_empty_query() {
257+
let reg = build_default_registry();
258+
let results = reg.search("");
259+
// empty query matches everything
260+
assert!(!results.is_empty());
261+
}
262+
263+
#[test]
264+
fn default_registry_has_templates() {
265+
let reg = build_default_registry();
266+
assert!(reg.get_all().len() > 30);
267+
assert!(!reg.categories().is_empty());
268+
}
269+
270+
#[test]
271+
fn domain_extraction() {
272+
let tpl = sample_template("t", "Test", "https://www.example.com/path", "X");
273+
assert_eq!(tpl.domain(), Some("example.com".into()));
274+
}
275+
}

0 commit comments

Comments
Β (0)