xref: /haiku/src/apps/webpositive/CookieWindow.cpp (revision 96efe3b3c0a805e87f608c303638cf4d2c37b9a9)
1 /*
2  * Copyright 2015 Haiku, Inc. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		Adrien Destugues
7  */
8 
9 
10 #include "CookieWindow.h"
11 
12 #include <Button.h>
13 #include <Catalog.h>
14 #include <ColumnListView.h>
15 #include <ColumnTypes.h>
16 #include <GroupLayoutBuilder.h>
17 #include <NetworkCookieJar.h>
18 #include <OutlineListView.h>
19 #include <ScrollView.h>
20 #include <StringView.h>
21 
22 
23 #undef B_TRANSLATION_CONTEXT
24 #define B_TRANSLATION_CONTEXT "Cookie Manager"
25 
26 enum {
27 	COOKIE_IMPORT = 'cimp',
28 	COOKIE_EXPORT = 'cexp',
29 	COOKIE_DELETE = 'cdel',
30 	COOKIE_REFRESH = 'rfsh',
31 
32 	DOMAIN_SELECTED = 'dmsl'
33 };
34 
35 
36 class CookieDateColumn: public BDateColumn
37 {
38 public:
39 	CookieDateColumn(const char* title, float width)
40 		:
41 		BDateColumn(title, width, width / 2, width * 2)
42 	{
43 	}
44 
45 	void DrawField(BField* field, BRect rect, BView* parent) {
46 		BDateField* dateField = (BDateField*)field;
47 		if (dateField->UnixTime() == -1) {
48 			DrawString(B_TRANSLATE("Session cookie"), parent, rect);
49 		} else {
50 			BDateColumn::DrawField(field, rect, parent);
51 		}
52 	}
53 };
54 
55 
56 class CookieRow: public BRow
57 {
58 public:
59 	CookieRow(BColumnListView* list, const BNetworkCookie& cookie)
60 		:
61 		BRow(),
62 		fCookie(cookie)
63 	{
64 		list->AddRow(this);
65 		SetField(new BStringField(cookie.Name().String()), 0);
66 		SetField(new BStringField(cookie.Path().String()), 1);
67 		time_t expiration = cookie.ExpirationDate();
68 		SetField(new BDateField(&expiration), 2);
69 		SetField(new BStringField(cookie.Value().String()), 3);
70 
71 		BString flags;
72 		if (cookie.Secure())
73 			flags = "https ";
74 		if (cookie.HttpOnly())
75 			flags = "http ";
76 
77 		if (cookie.IsHostOnly())
78 			flags += "hostOnly";
79 		SetField(new BStringField(flags.String()), 4);
80 	}
81 
82 	BNetworkCookie& Cookie() {
83 		return fCookie;
84 	}
85 
86 private:
87 	BNetworkCookie	fCookie;
88 };
89 
90 
91 class DomainItem: public BStringItem
92 {
93 public:
94 	DomainItem(BString text, bool empty)
95 		:
96 		BStringItem(text),
97 		fEmpty(empty)
98 	{
99 	}
100 
101 public:
102 	bool	fEmpty;
103 };
104 
105 
106 CookieWindow::CookieWindow(BRect frame, BNetworkCookieJar& jar)
107 	:
108 	BWindow(frame, B_TRANSLATE("Cookie manager"), B_TITLED_WINDOW,
109 		B_NORMAL_WINDOW_FEEL,
110 		B_AUTO_UPDATE_SIZE_LIMITS | B_ASYNCHRONOUS_CONTROLS | B_NOT_ZOOMABLE),
111 	fCookieJar(jar)
112 {
113 	BGroupLayout* root = new BGroupLayout(B_HORIZONTAL, 0.0);
114 	SetLayout(root);
115 
116 	fDomains = new BOutlineListView("domain list");
117 	root->AddView(new BScrollView("scroll", fDomains, 0, false, true), 1);
118 
119 	fHeaderView = new BStringView("label",
120 		B_TRANSLATE("The cookie jar is empty!"));
121 	fCookies = new BColumnListView("cookie list", B_WILL_DRAW, B_FANCY_BORDER,
122 		false);
123 
124 	int em = fCookies->StringWidth("M");
125 	int flagsLength = fCookies->StringWidth("Mhttps hostOnly" B_UTF8_ELLIPSIS);
126 
127 	fCookies->AddColumn(new BStringColumn(B_TRANSLATE("Name"),
128 		20 * em, 10 * em, 50 * em, 0), 0);
129 	fCookies->AddColumn(new BStringColumn(B_TRANSLATE("Path"),
130 		10 * em, 10 * em, 50 * em, 0), 1);
131 	fCookies->AddColumn(new CookieDateColumn(B_TRANSLATE("Expiration"),
132 		fCookies->StringWidth("88/88/8888 88:88:88 AM")), 2);
133 	fCookies->AddColumn(new BStringColumn(B_TRANSLATE("Value"),
134 		20 * em, 10 * em, 50 * em, 0), 3);
135 	fCookies->AddColumn(new BStringColumn(B_TRANSLATE("Flags"),
136 		flagsLength, flagsLength, flagsLength, 0), 4);
137 
138 	root->AddItem(BGroupLayoutBuilder(B_VERTICAL, B_USE_DEFAULT_SPACING)
139 		.SetInsets(5, 5, 5, 5)
140 		.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
141 			.Add(fHeaderView)
142 			.AddGlue()
143 		.End()
144 		.Add(fCookies)
145 		.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
146 			.SetInsets(5, 5, 5, 5)
147 #if DEBUG
148 			.Add(new BButton("import", B_TRANSLATE("Import" B_UTF8_ELLIPSIS),
149 				NULL))
150 			.Add(new BButton("export", B_TRANSLATE("Export" B_UTF8_ELLIPSIS),
151 				NULL))
152 #endif
153 			.AddGlue()
154 			.Add(new BButton("delete", B_TRANSLATE("Delete"),
155 				new BMessage(COOKIE_DELETE))), 3);
156 
157 	fDomains->SetSelectionMessage(new BMessage(DOMAIN_SELECTED));
158 }
159 
160 
161 void
162 CookieWindow::MessageReceived(BMessage* message)
163 {
164 	switch(message->what) {
165 		case DOMAIN_SELECTED:
166 		{
167 			int32 index = message->FindInt32("index");
168 			BStringItem* item = (BStringItem*)fDomains->ItemAt(index);
169 			if (item != NULL) {
170 				BString domain = item->Text();
171 				_ShowCookiesForDomain(domain);
172 			}
173 			return;
174 		}
175 
176 		case COOKIE_REFRESH:
177 			_BuildDomainList();
178 			return;
179 
180 		case COOKIE_DELETE:
181 			_DeleteCookies();
182 			return;
183 	}
184 	BWindow::MessageReceived(message);
185 }
186 
187 
188 void
189 CookieWindow::Show()
190 {
191 	BWindow::Show();
192 	if (IsHidden())
193 		return;
194 
195 	PostMessage(COOKIE_REFRESH);
196 }
197 
198 
199 bool
200 CookieWindow::QuitRequested()
201 {
202 	if (!IsHidden())
203 		Hide();
204 	return false;
205 }
206 
207 
208 void
209 CookieWindow::_BuildDomainList()
210 {
211 	// Empty the domain list (TODO should we do this when hiding instead?)
212 	for (int i = fDomains->FullListCountItems() - 1; i >= 1; i--) {
213 		delete fDomains->FullListItemAt(i);
214 	}
215 	fDomains->MakeEmpty();
216 
217 	// BOutlineListView does not handle parent = NULL in many methods, so let's
218 	// make sure everything always has a parent.
219 	DomainItem* rootItem = new DomainItem("", true);
220 	fDomains->AddItem(rootItem);
221 
222 	// Populate the domain list
223 	BNetworkCookieJar::Iterator it = fCookieJar.GetIterator();
224 
225 	const BNetworkCookie* cookie;
226 	while ((cookie = it.NextDomain()) != NULL) {
227 		_AddDomain(cookie->Domain(), false);
228 	}
229 
230 	int i = 1;
231 	while (i < fDomains->FullListCountItems())
232 	{
233 		DomainItem* item = (DomainItem*)fDomains->FullListItemAt(i);
234 		// Detach items from the fake root
235 		item->SetOutlineLevel(item->OutlineLevel() - 1);
236 		i++;
237 	}
238 	fDomains->RemoveItem(rootItem);
239 	delete rootItem;
240 
241 	i = 0;
242 	int firstNotEmpty = i;
243 	// Collapse empty items to keep the list short
244 	while (i < fDomains->FullListCountItems())
245 	{
246 		DomainItem* item = (DomainItem*)fDomains->FullListItemAt(i);
247 		if (item->fEmpty == true) {
248 			if (fDomains->CountItemsUnder(item, true) == 1) {
249 				// The item has no cookies, and only a single child. We can
250 				// remove it and move its child one level up in the tree.
251 
252 				int count = fDomains->CountItemsUnder(item, false);
253 				int index = fDomains->FullListIndexOf(item) + 1;
254 				for (int j = 0; j < count; j++) {
255 					BListItem* child = fDomains->FullListItemAt(index + j);
256 					child->SetOutlineLevel(child->OutlineLevel() - 1);
257 				}
258 
259 				fDomains->RemoveItem(item);
260 				delete item;
261 
262 				// The moved child is at the same index the removed item was.
263 				// We continue the loop without incrementing i to process it.
264 				continue;
265 			} else {
266 				// The item has no cookies, but has multiple children. Mark it
267 				// as disabled so it is not selectable.
268 				item->SetEnabled(false);
269 				if (i == firstNotEmpty)
270 					firstNotEmpty++;
271 			}
272 		}
273 
274 		i++;
275 	}
276 
277 	fDomains->Select(firstNotEmpty);
278 }
279 
280 
281 BStringItem*
282 CookieWindow::_AddDomain(BString domain, bool fake)
283 {
284 	BStringItem* parent = NULL;
285 	int firstDot = domain.FindFirst('.');
286 	if (firstDot >= 0) {
287 		BString parentDomain(domain);
288 		parentDomain.Remove(0, firstDot + 1);
289 		parent = _AddDomain(parentDomain, true);
290 	} else {
291 		parent = (BStringItem*)fDomains->FullListItemAt(0);
292 	}
293 
294 	BListItem* existing;
295 	int i = 0;
296 	// check that we aren't already there
297 	while ((existing = fDomains->ItemUnderAt(parent, true, i++)) != NULL) {
298 		DomainItem* stringItem = (DomainItem*)existing;
299 		if (stringItem->Text() == domain) {
300 			if (fake == false)
301 				stringItem->fEmpty = false;
302 			return stringItem;
303 		}
304 	}
305 
306 #if DEBUG
307 	puts("==============================");
308 	for (i = 0; i < fDomains->FullListCountItems(); i++) {
309 		BStringItem* t = (BStringItem*)fDomains->FullListItemAt(i);
310 		for (unsigned j = 0; j < t->OutlineLevel(); j++)
311 			printf("  ");
312 		printf("%s\n", t->Text());
313 	}
314 #endif
315 
316 	// Insert the new item, keeping the list alphabetically sorted
317 	BStringItem* domainItem = new DomainItem(domain, fake);
318 	domainItem->SetOutlineLevel(parent->OutlineLevel() + 1);
319 	BStringItem* sibling = NULL;
320 	int siblingCount = fDomains->CountItemsUnder(parent, true);
321 	for (i = 0; i < siblingCount; i++) {
322 		sibling = (BStringItem*)fDomains->ItemUnderAt(parent, true, i);
323 		if (strcmp(sibling->Text(), domainItem->Text()) > 0) {
324 			fDomains->AddItem(domainItem, fDomains->FullListIndexOf(sibling));
325 			return domainItem;
326 		}
327 	}
328 
329 	if (sibling) {
330 		// There were siblings, but all smaller than what we try to insert.
331 		// Insert after the last one (and its subitems)
332 		fDomains->AddItem(domainItem, fDomains->FullListIndexOf(sibling)
333 			+ fDomains->CountItemsUnder(sibling, false) + 1);
334 	} else {
335 		// There were no siblings, insert right after the parent
336 		fDomains->AddItem(domainItem, fDomains->FullListIndexOf(parent) + 1);
337 	}
338 
339 	return domainItem;
340 }
341 
342 
343 void
344 CookieWindow::_ShowCookiesForDomain(BString domain)
345 {
346 	BString label;
347 	label.SetToFormat(B_TRANSLATE("Cookies for %s"), domain.String());
348 	fHeaderView->SetText(label);
349 
350 	// Empty the cookie list
351 	fCookies->Clear();
352 
353 	// Populate the domain list
354 	BNetworkCookieJar::Iterator it = fCookieJar.GetIterator();
355 
356 	const BNetworkCookie* cookie;
357 	/* FIXME A direct access to a domain would be needed in BNetworkCookieJar. */
358 	while ((cookie = it.Next()) != NULL) {
359 		if (cookie->Domain() == domain)
360 			break;
361 	}
362 
363 	if (cookie == NULL)
364 		return;
365 
366 	do {
367 		new CookieRow(fCookies, *cookie); // Attaches itself to the list
368 		cookie = it.Next();
369 	} while (cookie != NULL && cookie->Domain() == domain);
370 }
371 
372 
373 void
374 CookieWindow::_DeleteCookies()
375 {
376 	CookieRow* row;
377 	CookieRow* prevRow;
378 
379 	for (prevRow = NULL; ; prevRow = row) {
380 		row = (CookieRow*)fCookies->CurrentSelection(prevRow);
381 
382 		if (prevRow != NULL) {
383 			fCookies->RemoveRow(prevRow);
384 			delete prevRow;
385 		}
386 
387 		if (row == NULL)
388 			break;
389 
390 		// delete this cookie
391 		BNetworkCookie& cookie = row->Cookie();
392 		cookie.SetExpirationDate(0);
393 		fCookieJar.AddCookie(cookie);
394 	}
395 
396 	// A domain was selected in the domain list
397 	if (prevRow == NULL) {
398 		while (true) {
399 			// Clear the first cookie continuously
400 			row = (CookieRow*)fCookies->RowAt(0);
401 
402 			if (row == NULL)
403 				break;
404 
405 			BNetworkCookie& cookie = row->Cookie();
406 			cookie.SetExpirationDate(0);
407 			fCookieJar.AddCookie(cookie);
408 			fCookies->RemoveRow(row);
409 			delete row;
410 		}
411 	}
412 }
413