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