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