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
26enum {
27	COOKIE_IMPORT = 'cimp',
28	COOKIE_EXPORT = 'cexp',
29	COOKIE_DELETE = 'cdel',
30	COOKIE_REFRESH = 'rfsh',
31
32	DOMAIN_SELECTED = 'dmsl'
33};
34
35
36class CookieDateColumn: public BDateColumn
37{
38public:
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
56class CookieRow: public BRow
57{
58public:
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
83	BPrivate::Network::BNetworkCookie& Cookie() {
84		return fCookie;
85	}
86
87private:
88	BPrivate::Network::BNetworkCookie	fCookie;
89};
90
91
92class DomainItem: public BStringItem
93{
94public:
95	DomainItem(BString text, bool empty)
96		:
97		BStringItem(text),
98		fEmpty(empty)
99	{
100	}
101
102public:
103	bool	fEmpty;
104};
105
106
107CookieWindow::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
163void
164CookieWindow::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
190void
191CookieWindow::Show()
192{
193	BWindow::Show();
194	if (IsHidden())
195		return;
196
197	PostMessage(COOKIE_REFRESH);
198}
199
200
201bool
202CookieWindow::QuitRequested()
203{
204	if (!IsHidden())
205		Hide();
206	return false;
207}
208
209
210void
211CookieWindow::_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
283BStringItem*
284CookieWindow::_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
345void
346CookieWindow::_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
376void
377CookieWindow::_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