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