1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/extensions/api/sessions/sessions_api.h" 6 7 #include <vector> 8 9 #include "base/i18n/rtl.h" 10 #include "base/lazy_instance.h" 11 #include "base/prefs/pref_service.h" 12 #include "base/strings/string_number_conversions.h" 13 #include "base/strings/stringprintf.h" 14 #include "base/strings/utf_string_conversions.h" 15 #include "base/time/time.h" 16 #include "chrome/browser/extensions/api/sessions/session_id.h" 17 #include "chrome/browser/extensions/api/tabs/windows_util.h" 18 #include "chrome/browser/extensions/extension_function_dispatcher.h" 19 #include "chrome/browser/extensions/extension_function_registry.h" 20 #include "chrome/browser/extensions/extension_tab_util.h" 21 #include "chrome/browser/extensions/window_controller.h" 22 #include "chrome/browser/extensions/window_controller_list.h" 23 #include "chrome/browser/profiles/profile.h" 24 #include "chrome/browser/search/search.h" 25 #include "chrome/browser/sessions/session_restore.h" 26 #include "chrome/browser/sessions/tab_restore_service_delegate.h" 27 #include "chrome/browser/sessions/tab_restore_service_factory.h" 28 #include "chrome/browser/sync/glue/synced_session.h" 29 #include "chrome/browser/sync/open_tabs_ui_delegate.h" 30 #include "chrome/browser/sync/profile_sync_service.h" 31 #include "chrome/browser/sync/profile_sync_service_factory.h" 32 #include "chrome/browser/ui/browser.h" 33 #include "chrome/browser/ui/browser_finder.h" 34 #include "chrome/browser/ui/host_desktop.h" 35 #include "chrome/browser/ui/tabs/tab_strip_model.h" 36 #include "chrome/common/pref_names.h" 37 #include "content/public/browser/web_contents.h" 38 #include "extensions/common/error_utils.h" 39 #include "net/base/net_util.h" 40 #include "ui/base/layout.h" 41 42 namespace extensions { 43 44 namespace GetRecentlyClosed = api::sessions::GetRecentlyClosed; 45 namespace GetDevices = api::sessions::GetDevices; 46 namespace Restore = api::sessions::Restore; 47 namespace tabs = api::tabs; 48 namespace windows = api::windows; 49 50 const char kNoRecentlyClosedSessionsError[] = 51 "There are no recently closed sessions."; 52 const char kInvalidSessionIdError[] = "Invalid session id: \"*\"."; 53 const char kNoBrowserToRestoreSession[] = 54 "There are no browser windows to restore the session."; 55 const char kSessionSyncError[] = "Synced sessions are not available."; 56 57 // Comparator function for use with std::sort that will sort sessions by 58 // descending modified_time (i.e., most recent first). 59 bool SortSessionsByRecency(const browser_sync::SyncedSession* s1, 60 const browser_sync::SyncedSession* s2) { 61 return s1->modified_time > s2->modified_time; 62 } 63 64 // Comparator function for use with std::sort that will sort tabs in a window 65 // by descending timestamp (i.e., most recent first). 66 bool SortTabsByRecency(const SessionTab* t1, const SessionTab* t2) { 67 return t1->timestamp > t2->timestamp; 68 } 69 70 scoped_ptr<tabs::Tab> CreateTabModelHelper( 71 Profile* profile, 72 const sessions::SerializedNavigationEntry& current_navigation, 73 const std::string& session_id, 74 int index, 75 bool pinned, 76 int selected_index, 77 const Extension* extension) { 78 scoped_ptr<tabs::Tab> tab_struct(new tabs::Tab); 79 80 GURL gurl = current_navigation.virtual_url(); 81 std::string title = UTF16ToUTF8(current_navigation.title()); 82 83 tab_struct->session_id.reset(new std::string(session_id)); 84 tab_struct->url.reset(new std::string(gurl.spec())); 85 if (!title.empty()) { 86 tab_struct->title.reset(new std::string(title)); 87 } else { 88 const std::string languages = 89 profile->GetPrefs()->GetString(prefs::kAcceptLanguages); 90 tab_struct->title.reset(new std::string(UTF16ToUTF8( 91 net::FormatUrl(gurl, languages)))); 92 } 93 tab_struct->index = index; 94 tab_struct->pinned = pinned; 95 tab_struct->selected = index == selected_index; 96 tab_struct->active = false; 97 tab_struct->highlighted = false; 98 tab_struct->incognito = false; 99 ExtensionTabUtil::ScrubTabForExtension(extension, tab_struct.get()); 100 return tab_struct.Pass(); 101 } 102 103 scoped_ptr<windows::Window> CreateWindowModelHelper( 104 scoped_ptr<std::vector<linked_ptr<tabs::Tab> > > tabs, 105 const std::string& session_id, 106 const windows::Window::Type& type, 107 const windows::Window::State& state) { 108 scoped_ptr<windows::Window> window_struct(new windows::Window); 109 window_struct->tabs = tabs.Pass(); 110 window_struct->session_id.reset(new std::string(session_id)); 111 window_struct->incognito = false; 112 window_struct->always_on_top = false; 113 window_struct->focused = false; 114 window_struct->type = type; 115 window_struct->state = state; 116 return window_struct.Pass(); 117 } 118 119 scoped_ptr<api::sessions::Session> CreateSessionModelHelper( 120 int last_modified, 121 scoped_ptr<tabs::Tab> tab, 122 scoped_ptr<windows::Window> window) { 123 scoped_ptr<api::sessions::Session> session_struct(new api::sessions::Session); 124 session_struct->last_modified = last_modified; 125 if (tab) 126 session_struct->tab = tab.Pass(); 127 else if (window) 128 session_struct->window = window.Pass(); 129 else 130 NOTREACHED(); 131 return session_struct.Pass(); 132 } 133 134 bool is_tab_entry(const TabRestoreService::Entry* entry) { 135 return entry->type == TabRestoreService::TAB; 136 } 137 138 bool is_window_entry(const TabRestoreService::Entry* entry) { 139 return entry->type == TabRestoreService::WINDOW; 140 } 141 142 scoped_ptr<tabs::Tab> SessionsGetRecentlyClosedFunction::CreateTabModel( 143 const TabRestoreService::Tab& tab, int session_id, int selected_index) { 144 return CreateTabModelHelper(GetProfile(), 145 tab.navigations[tab.current_navigation_index], 146 base::IntToString(session_id), 147 tab.tabstrip_index, 148 tab.pinned, 149 selected_index, 150 GetExtension()); 151 } 152 153 scoped_ptr<windows::Window> 154 SessionsGetRecentlyClosedFunction::CreateWindowModel( 155 const TabRestoreService::Window& window, 156 int session_id) { 157 DCHECK(!window.tabs.empty()); 158 159 scoped_ptr<std::vector<linked_ptr<tabs::Tab> > > tabs( 160 new std::vector<linked_ptr<tabs::Tab> >); 161 for (size_t i = 0; i < window.tabs.size(); ++i) { 162 tabs->push_back(make_linked_ptr( 163 CreateTabModel(window.tabs[i], window.tabs[i].id, 164 window.selected_tab_index).release())); 165 } 166 167 return CreateWindowModelHelper(tabs.Pass(), 168 base::IntToString(session_id), 169 windows::Window::TYPE_NORMAL, 170 windows::Window::STATE_NORMAL); 171 } 172 173 scoped_ptr<api::sessions::Session> 174 SessionsGetRecentlyClosedFunction::CreateSessionModel( 175 const TabRestoreService::Entry* entry) { 176 scoped_ptr<tabs::Tab> tab; 177 scoped_ptr<windows::Window> window; 178 switch (entry->type) { 179 case TabRestoreService::TAB: 180 tab = CreateTabModel( 181 *static_cast<const TabRestoreService::Tab*>(entry), entry->id, -1); 182 break; 183 case TabRestoreService::WINDOW: 184 window = CreateWindowModel( 185 *static_cast<const TabRestoreService::Window*>(entry), entry->id); 186 break; 187 default: 188 NOTREACHED(); 189 } 190 return CreateSessionModelHelper(entry->timestamp.ToTimeT(), 191 tab.Pass(), 192 window.Pass()); 193 } 194 195 bool SessionsGetRecentlyClosedFunction::RunImpl() { 196 scoped_ptr<GetRecentlyClosed::Params> params( 197 GetRecentlyClosed::Params::Create(*args_)); 198 EXTENSION_FUNCTION_VALIDATE(params); 199 int max_results = api::sessions::MAX_SESSION_RESULTS; 200 if (params->filter && params->filter->max_results) 201 max_results = *params->filter->max_results; 202 EXTENSION_FUNCTION_VALIDATE(max_results >= 0 && 203 max_results <= api::sessions::MAX_SESSION_RESULTS); 204 205 std::vector<linked_ptr<api::sessions::Session> > result; 206 TabRestoreService* tab_restore_service = 207 TabRestoreServiceFactory::GetForProfile(GetProfile()); 208 DCHECK(tab_restore_service); 209 210 // List of entries. They are ordered from most to least recent. 211 // We prune the list to contain max 25 entries at any time and removes 212 // uninteresting entries. 213 TabRestoreService::Entries entries = tab_restore_service->entries(); 214 for (TabRestoreService::Entries::const_iterator it = entries.begin(); 215 it != entries.end() && static_cast<int>(result.size()) < max_results; 216 ++it) { 217 TabRestoreService::Entry* entry = *it; 218 result.push_back(make_linked_ptr(CreateSessionModel(entry).release())); 219 } 220 221 results_ = GetRecentlyClosed::Results::Create(result); 222 return true; 223 } 224 225 scoped_ptr<tabs::Tab> SessionsGetDevicesFunction::CreateTabModel( 226 const std::string& session_tag, 227 const SessionTab& tab, 228 int tab_index, 229 int selected_index) { 230 std::string session_id = SessionId(session_tag, tab.tab_id.id()).ToString(); 231 return CreateTabModelHelper(GetProfile(), 232 tab.navigations[tab.current_navigation_index], 233 session_id, 234 tab_index, 235 tab.pinned, 236 selected_index, 237 GetExtension()); 238 } 239 240 scoped_ptr<windows::Window> SessionsGetDevicesFunction::CreateWindowModel( 241 const SessionWindow& window, const std::string& session_tag) { 242 DCHECK(!window.tabs.empty()); 243 244 // Prune tabs that are not syncable or are NewTabPage. Then, sort the tabs 245 // from most recent to least recent. 246 std::vector<const SessionTab*> tabs_in_window; 247 for (size_t i = 0; i < window.tabs.size(); ++i) { 248 const SessionTab* tab = window.tabs[i]; 249 if (tab->navigations.empty()) 250 continue; 251 const sessions::SerializedNavigationEntry& current_navigation = 252 tab->navigations.at(tab->normalized_navigation_index()); 253 if (chrome::IsNTPURL(current_navigation.virtual_url(), GetProfile())) { 254 continue; 255 } 256 tabs_in_window.push_back(tab); 257 } 258 if (tabs_in_window.empty()) 259 return scoped_ptr<windows::Window>(); 260 std::sort(tabs_in_window.begin(), tabs_in_window.end(), SortTabsByRecency); 261 262 scoped_ptr<std::vector<linked_ptr<tabs::Tab> > > tabs( 263 new std::vector<linked_ptr<tabs::Tab> >); 264 for (size_t i = 0; i < window.tabs.size(); ++i) { 265 tabs->push_back(make_linked_ptr( 266 CreateTabModel(session_tag, *window.tabs[i], i, 267 window.selected_tab_index).release())); 268 } 269 270 std::string session_id = 271 SessionId(session_tag, window.window_id.id()).ToString(); 272 273 windows::Window::Type type = windows::Window::TYPE_NONE; 274 switch (window.type) { 275 case Browser::TYPE_TABBED: 276 type = windows::Window::TYPE_NORMAL; 277 break; 278 case Browser::TYPE_POPUP: 279 type = windows::Window::TYPE_POPUP; 280 break; 281 } 282 283 windows::Window::State state = windows::Window::STATE_NONE; 284 switch (window.show_state) { 285 case ui::SHOW_STATE_NORMAL: 286 state = windows::Window::STATE_NORMAL; 287 break; 288 case ui::SHOW_STATE_MINIMIZED: 289 state = windows::Window::STATE_MINIMIZED; 290 break; 291 case ui::SHOW_STATE_MAXIMIZED: 292 state = windows::Window::STATE_MAXIMIZED; 293 break; 294 case ui::SHOW_STATE_FULLSCREEN: 295 state = windows::Window::STATE_FULLSCREEN; 296 break; 297 case ui::SHOW_STATE_DEFAULT: 298 case ui::SHOW_STATE_INACTIVE: 299 case ui::SHOW_STATE_DETACHED: 300 case ui::SHOW_STATE_END: 301 break; 302 } 303 304 scoped_ptr<windows::Window> window_struct( 305 CreateWindowModelHelper(tabs.Pass(), session_id, type, state)); 306 // TODO(dwankri): Dig deeper to resolve bounds not being optional, so closed 307 // windows in GetRecentlyClosed can have set values in Window helper. 308 window_struct->left.reset(new int(window.bounds.x())); 309 window_struct->top.reset(new int(window.bounds.y())); 310 window_struct->width.reset(new int(window.bounds.width())); 311 window_struct->height.reset(new int(window.bounds.height())); 312 313 return window_struct.Pass(); 314 } 315 316 scoped_ptr<api::sessions::Session> 317 SessionsGetDevicesFunction::CreateSessionModel( 318 const SessionWindow& window, const std::string& session_tag) { 319 scoped_ptr<windows::Window> window_model( 320 CreateWindowModel(window, session_tag)); 321 // There is a chance that after pruning uninteresting tabs the window will be 322 // empty. 323 return !window_model ? scoped_ptr<api::sessions::Session>() 324 : CreateSessionModelHelper(window.timestamp.ToTimeT(), 325 scoped_ptr<tabs::Tab>(), 326 window_model.Pass()); 327 } 328 329 scoped_ptr<api::sessions::Device> SessionsGetDevicesFunction::CreateDeviceModel( 330 const browser_sync::SyncedSession* session) { 331 int max_results = api::sessions::MAX_SESSION_RESULTS; 332 // Already validated in RunImpl(). 333 scoped_ptr<GetDevices::Params> params(GetDevices::Params::Create(*args_)); 334 if (params->filter && params->filter->max_results) 335 max_results = *params->filter->max_results; 336 337 scoped_ptr<api::sessions::Device> device_struct(new api::sessions::Device); 338 device_struct->info = session->session_name; 339 340 for (browser_sync::SyncedSession::SyncedWindowMap::const_iterator it = 341 session->windows.begin(); it != session->windows.end() && 342 static_cast<int>(device_struct->sessions.size()) < max_results; ++it) { 343 scoped_ptr<api::sessions::Session> session_model(CreateSessionModel( 344 *it->second, session->session_tag)); 345 if (session_model) 346 device_struct->sessions.push_back(make_linked_ptr( 347 session_model.release())); 348 } 349 return device_struct.Pass(); 350 } 351 352 bool SessionsGetDevicesFunction::RunImpl() { 353 ProfileSyncService* service = 354 ProfileSyncServiceFactory::GetInstance()->GetForProfile(GetProfile()); 355 if (!(service && service->GetPreferredDataTypes().Has(syncer::SESSIONS))) { 356 // Sync not enabled. 357 results_ = GetDevices::Results::Create( 358 std::vector<linked_ptr<api::sessions::Device> >()); 359 return true; 360 } 361 362 browser_sync::OpenTabsUIDelegate* open_tabs = 363 service->GetOpenTabsUIDelegate(); 364 std::vector<const browser_sync::SyncedSession*> sessions; 365 if (!(open_tabs && open_tabs->GetAllForeignSessions(&sessions))) { 366 results_ = GetDevices::Results::Create( 367 std::vector<linked_ptr<api::sessions::Device> >()); 368 return true; 369 } 370 371 scoped_ptr<GetDevices::Params> params(GetDevices::Params::Create(*args_)); 372 EXTENSION_FUNCTION_VALIDATE(params); 373 if (params->filter && params->filter->max_results) { 374 EXTENSION_FUNCTION_VALIDATE(*params->filter->max_results >= 0 && 375 *params->filter->max_results <= api::sessions::MAX_SESSION_RESULTS); 376 } 377 378 std::vector<linked_ptr<api::sessions::Device> > result; 379 // Sort sessions from most recent to least recent. 380 std::sort(sessions.begin(), sessions.end(), SortSessionsByRecency); 381 for (size_t i = 0; i < sessions.size(); ++i) { 382 result.push_back(make_linked_ptr(CreateDeviceModel(sessions[i]).release())); 383 } 384 385 results_ = GetDevices::Results::Create(result); 386 return true; 387 } 388 389 void SessionsRestoreFunction::SetInvalidIdError(const std::string& invalid_id) { 390 SetError(ErrorUtils::FormatErrorMessage(kInvalidSessionIdError, invalid_id)); 391 } 392 393 394 void SessionsRestoreFunction::SetResultRestoredTab( 395 const content::WebContents* contents) { 396 scoped_ptr<DictionaryValue> tab_value( 397 ExtensionTabUtil::CreateTabValue(contents, GetExtension())); 398 scoped_ptr<tabs::Tab> tab(tabs::Tab::FromValue(*tab_value)); 399 scoped_ptr<api::sessions::Session> restored_session(CreateSessionModelHelper( 400 base::Time::Now().ToTimeT(), 401 tab.Pass(), 402 scoped_ptr<windows::Window>())); 403 results_ = Restore::Results::Create(*restored_session); 404 } 405 406 bool SessionsRestoreFunction::SetResultRestoredWindow(int window_id) { 407 WindowController* controller = NULL; 408 if (!windows_util::GetWindowFromWindowID(this, window_id, &controller)) { 409 // error_ is set by GetWindowFromWindowId function call. 410 return false; 411 } 412 scoped_ptr<DictionaryValue> window_value( 413 controller->CreateWindowValueWithTabs(GetExtension())); 414 scoped_ptr<windows::Window> window(windows::Window::FromValue( 415 *window_value)); 416 results_ = Restore::Results::Create(*CreateSessionModelHelper( 417 base::Time::Now().ToTimeT(), 418 scoped_ptr<tabs::Tab>(), 419 window.Pass())); 420 return true; 421 } 422 423 bool SessionsRestoreFunction::RestoreMostRecentlyClosed(Browser* browser) { 424 TabRestoreService* tab_restore_service = 425 TabRestoreServiceFactory::GetForProfile(GetProfile()); 426 chrome::HostDesktopType host_desktop_type = browser->host_desktop_type(); 427 TabRestoreService::Entries entries = tab_restore_service->entries(); 428 429 if (entries.empty()) { 430 SetError(kNoRecentlyClosedSessionsError); 431 return false; 432 } 433 434 bool is_window = is_window_entry(entries.front()); 435 TabRestoreServiceDelegate* delegate = 436 TabRestoreServiceDelegate::FindDelegateForWebContents( 437 browser->tab_strip_model()->GetActiveWebContents()); 438 std::vector<content::WebContents*> contents = 439 tab_restore_service->RestoreMostRecentEntry(delegate, host_desktop_type); 440 DCHECK(contents.size()); 441 442 if (is_window) { 443 return SetResultRestoredWindow( 444 ExtensionTabUtil::GetWindowIdOfTab(contents[0])); 445 } 446 447 SetResultRestoredTab(contents[0]); 448 return true; 449 } 450 451 bool SessionsRestoreFunction::RestoreLocalSession(const SessionId& session_id, 452 Browser* browser) { 453 TabRestoreService* tab_restore_service = 454 TabRestoreServiceFactory::GetForProfile(GetProfile()); 455 chrome::HostDesktopType host_desktop_type = browser->host_desktop_type(); 456 TabRestoreService::Entries entries = tab_restore_service->entries(); 457 458 if (entries.empty()) { 459 SetInvalidIdError(session_id.ToString()); 460 return false; 461 } 462 463 // Check if the recently closed list contains an entry with the provided id. 464 bool is_window = false; 465 for (TabRestoreService::Entries::iterator it = entries.begin(); 466 it != entries.end(); ++it) { 467 if ((*it)->id == session_id.id()) { 468 // The only time a full window is being restored is if the entry ID 469 // matches the provided ID and the entry type is Window. 470 is_window = is_window_entry(*it); 471 break; 472 } 473 } 474 475 TabRestoreServiceDelegate* delegate = 476 TabRestoreServiceDelegate::FindDelegateForWebContents( 477 browser->tab_strip_model()->GetActiveWebContents()); 478 std::vector<content::WebContents*> contents = 479 tab_restore_service->RestoreEntryById(delegate, 480 session_id.id(), 481 host_desktop_type, 482 UNKNOWN); 483 // If the ID is invalid, contents will be empty. 484 if (!contents.size()) { 485 SetInvalidIdError(session_id.ToString()); 486 return false; 487 } 488 489 // Retrieve the window through any of the tabs in contents. 490 if (is_window) { 491 return SetResultRestoredWindow( 492 ExtensionTabUtil::GetWindowIdOfTab(contents[0])); 493 } 494 495 SetResultRestoredTab(contents[0]); 496 return true; 497 } 498 499 bool SessionsRestoreFunction::RestoreForeignSession(const SessionId& session_id, 500 Browser* browser) { 501 ProfileSyncService* service = 502 ProfileSyncServiceFactory::GetInstance()->GetForProfile(GetProfile()); 503 if (!(service && service->GetPreferredDataTypes().Has(syncer::SESSIONS))) { 504 SetError(kSessionSyncError); 505 return false; 506 } 507 browser_sync::OpenTabsUIDelegate* open_tabs = 508 service->GetOpenTabsUIDelegate(); 509 if (!open_tabs) { 510 SetError(kSessionSyncError); 511 return false; 512 } 513 514 const SessionTab* tab = NULL; 515 if (open_tabs->GetForeignTab(session_id.session_tag(), 516 session_id.id(), 517 &tab)) { 518 TabStripModel* tab_strip = browser->tab_strip_model(); 519 content::WebContents* contents = tab_strip->GetActiveWebContents(); 520 521 content::WebContents* tab_contents = 522 SessionRestore::RestoreForeignSessionTab(contents, *tab, 523 NEW_FOREGROUND_TAB); 524 SetResultRestoredTab(tab_contents); 525 return true; 526 } 527 528 // Restoring a full window. 529 std::vector<const SessionWindow*> windows; 530 if (!open_tabs->GetForeignSession(session_id.session_tag(), &windows)) { 531 SetInvalidIdError(session_id.ToString()); 532 return false; 533 } 534 535 std::vector<const SessionWindow*>::const_iterator window = windows.begin(); 536 while (window != windows.end() 537 && (*window)->window_id.id() != session_id.id()) { 538 ++window; 539 } 540 if (window == windows.end()) { 541 SetInvalidIdError(session_id.ToString()); 542 return false; 543 } 544 545 chrome::HostDesktopType host_desktop_type = browser->host_desktop_type(); 546 // Only restore one window at a time. 547 std::vector<Browser*> browsers = SessionRestore::RestoreForeignSessionWindows( 548 GetProfile(), host_desktop_type, window, window + 1); 549 // Will always create one browser because we only restore one window per call. 550 DCHECK_EQ(1u, browsers.size()); 551 return SetResultRestoredWindow(ExtensionTabUtil::GetWindowId(browsers[0])); 552 } 553 554 bool SessionsRestoreFunction::RunImpl() { 555 scoped_ptr<Restore::Params> params(Restore::Params::Create(*args_)); 556 EXTENSION_FUNCTION_VALIDATE(params); 557 558 Browser* browser = chrome::FindBrowserWithProfile( 559 GetProfile(), chrome::HOST_DESKTOP_TYPE_NATIVE); 560 if (!browser) { 561 SetError(kNoBrowserToRestoreSession); 562 return false; 563 } 564 565 if (!params->session_id) 566 return RestoreMostRecentlyClosed(browser); 567 568 scoped_ptr<SessionId> session_id(SessionId::Parse(*params->session_id)); 569 if (!session_id) { 570 SetInvalidIdError(*params->session_id); 571 return false; 572 } 573 574 return session_id->IsForeign() ? 575 RestoreForeignSession(*session_id, browser) 576 : RestoreLocalSession(*session_id, browser); 577 } 578 579 SessionsAPI::SessionsAPI(Profile* profile) { 580 } 581 582 SessionsAPI::~SessionsAPI() { 583 } 584 585 static base::LazyInstance<ProfileKeyedAPIFactory<SessionsAPI> > 586 g_factory = LAZY_INSTANCE_INITIALIZER; 587 588 // static 589 ProfileKeyedAPIFactory<SessionsAPI>* 590 SessionsAPI::GetFactoryInstance() { 591 return &g_factory.Get(); 592 } 593 594 } // namespace extensions 595