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, 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 87 private: 88 BPrivate::Network::BNetworkCookie fCookie; 89 }; 90 91 92 class DomainItem: public BStringItem 93 { 94 public: 95 DomainItem(BString text, bool empty) 96 : 97 BStringItem(text), 98 fEmpty(empty) 99 { 100 } 101 102 public: 103 bool fEmpty; 104 }; 105 106 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 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 191 CookieWindow::Show() 192 { 193 BWindow::Show(); 194 if (IsHidden()) 195 return; 196 197 PostMessage(COOKIE_REFRESH); 198 } 199 200 201 bool 202 CookieWindow::QuitRequested() 203 { 204 if (!IsHidden()) 205 Hide(); 206 return false; 207 } 208 209 210 void 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* 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 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 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