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