summaryrefslogtreecommitdiffstats
path: root/comm/suite/components
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/suite/components/SuiteComponents.manifest18
-rw-r--r--comm/suite/components/autocomplete/content/autocomplete.css46
-rw-r--r--comm/suite/components/autocomplete/content/autocomplete.xml1641
-rw-r--r--comm/suite/components/autocomplete/jar.mn9
-rw-r--r--comm/suite/components/autocomplete/moz.build7
-rw-r--r--comm/suite/components/bindings/datetimepicker.xml1316
-rw-r--r--comm/suite/components/bindings/findbar.xml162
-rw-r--r--comm/suite/components/bindings/general.xml37
-rw-r--r--comm/suite/components/bindings/generalBindings.xml31
-rw-r--r--comm/suite/components/bindings/jar.mn20
-rw-r--r--comm/suite/components/bindings/moz.build7
-rw-r--r--comm/suite/components/bindings/notification.xml2423
-rw-r--r--comm/suite/components/bindings/numberbox.xml217
-rw-r--r--comm/suite/components/bindings/preferences.xml817
-rw-r--r--comm/suite/components/bindings/prefwindow.xml548
-rw-r--r--comm/suite/components/bindings/spinbuttons.xml92
-rw-r--r--comm/suite/components/bindings/textbox.xml251
-rw-r--r--comm/suite/components/bindings/toolbar-xpfe.xml333
-rw-r--r--comm/suite/components/bindings/toolbar.xml579
-rw-r--r--comm/suite/components/build/Makefile.in8
-rw-r--r--comm/suite/components/build/moz.build23
-rw-r--r--comm/suite/components/build/nsSuiteCID.h24
-rw-r--r--comm/suite/components/build/nsSuiteModule.cpp87
-rw-r--r--comm/suite/components/console/content/console.css74
-rw-r--r--comm/suite/components/console/content/console.js111
-rw-r--r--comm/suite/components/console/content/console.xul208
-rw-r--r--comm/suite/components/console/content/consoleBindings.xml543
-rw-r--r--comm/suite/components/console/jar.mn9
-rw-r--r--comm/suite/components/console/jsconsole-clhandler.js34
-rw-r--r--comm/suite/components/console/jsconsole-clhandler.manifest3
-rw-r--r--comm/suite/components/console/moz.build12
-rw-r--r--comm/suite/components/customizeToolbar.css107
-rw-r--r--comm/suite/components/customizeToolbar.js855
-rw-r--r--comm/suite/components/customizeToolbar.xhtml110
-rw-r--r--comm/suite/components/dataman/content/dataman.css45
-rw-r--r--comm/suite/components/dataman/content/dataman.js3270
-rw-r--r--comm/suite/components/dataman/content/dataman.xml249
-rw-r--r--comm/suite/components/dataman/content/dataman.xul571
-rw-r--r--comm/suite/components/dataman/jar.mn9
-rw-r--r--comm/suite/components/dataman/moz.build11
-rw-r--r--comm/suite/components/dataman/tests/browser.ini8
-rw-r--r--comm/suite/components/dataman/tests/browser_dataman_basics.js831
-rw-r--r--comm/suite/components/dataman/tests/browser_dataman_callviews.js213
-rw-r--r--comm/suite/components/dataman/tests/dataman_storage.appcache5
-rw-r--r--comm/suite/components/dataman/tests/dataman_storage.appcache^headers^2
-rw-r--r--comm/suite/components/dataman/tests/dataman_storage.html40
-rw-r--r--comm/suite/components/downloads/DownloadsCommon.jsm800
-rw-r--r--comm/suite/components/downloads/DownloadsTaskbar.jsm182
-rw-r--r--comm/suite/components/downloads/content/DownloadProgressListener.js30
-rw-r--r--comm/suite/components/downloads/content/downloadmanager.js634
-rw-r--r--comm/suite/components/downloads/content/downloadmanager.xul452
-rw-r--r--comm/suite/components/downloads/content/progressDialog.js240
-rw-r--r--comm/suite/components/downloads/content/progressDialog.xul108
-rw-r--r--comm/suite/components/downloads/content/treeView.js483
-rw-r--r--comm/suite/components/downloads/content/uploadProgress.js189
-rw-r--r--comm/suite/components/downloads/content/uploadProgress.xul33
-rw-r--r--comm/suite/components/downloads/jar.mn14
-rw-r--r--comm/suite/components/downloads/moz.build17
-rw-r--r--comm/suite/components/downloads/tests/chrome/chrome.ini21
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_action_keys_respect_focus.xul376
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_basic_functionality.xul281
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_cleanup_search.xul172
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_clear_button_disabled.xul201
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_close_download_manager.xul117
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_delete_key_cancels.xul200
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_delete_key_removes.xul198
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_drag.xul201
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_enter_dblclick_opens.xul243
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_multi_select.xul204
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_multiword_search.xul173
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_open_properties.xul197
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_removeDownload_updates_ui.xul150
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_search_clearlist.xul168
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_search_keys.xul128
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_select_all.xul145
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_space_key_pauses_resumes.xul221
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_space_key_retries.xul198
-rw-r--r--comm/suite/components/downloads/tests/chrome/test_ui_stays_open_on_alert_clickback.xul115
-rw-r--r--comm/suite/components/feeds/FeedConverter.js461
-rw-r--r--comm/suite/components/feeds/FeedWriter.js1211
-rw-r--r--comm/suite/components/feeds/SuiteFeeds.manifest11
-rw-r--r--comm/suite/components/feeds/WebContentConverter.js818
-rw-r--r--comm/suite/components/feeds/content/subscribe.css7
-rw-r--r--comm/suite/components/feeds/content/subscribe.xhtml59
-rw-r--r--comm/suite/components/feeds/content/subscribe.xml42
-rw-r--r--comm/suite/components/feeds/jar.mn8
-rw-r--r--comm/suite/components/feeds/moz.build27
-rw-r--r--comm/suite/components/feeds/nsFeedSniffer.cpp356
-rw-r--r--comm/suite/components/feeds/nsFeedSniffer.h35
-rw-r--r--comm/suite/components/feeds/nsIFeedResultService.idl66
-rw-r--r--comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl116
-rw-r--r--comm/suite/components/helpviewer/content/contextHelp.js68
-rw-r--r--comm/suite/components/helpviewer/content/help.js856
-rw-r--r--comm/suite/components/helpviewer/content/help.xul284
-rw-r--r--comm/suite/components/helpviewer/content/helpContextOverlay.xul58
-rw-r--r--comm/suite/components/helpviewer/content/platformClasses.css13
-rw-r--r--comm/suite/components/helpviewer/jar.mn11
-rw-r--r--comm/suite/components/helpviewer/moz.build7
-rw-r--r--comm/suite/components/migration/SuiteProfileMigrator.js149
-rw-r--r--comm/suite/components/migration/SuiteProfileMigrator.manifest2
-rw-r--r--comm/suite/components/migration/content/migration.js413
-rw-r--r--comm/suite/components/migration/content/migration.xul95
-rw-r--r--comm/suite/components/migration/jar.mn7
-rw-r--r--comm/suite/components/migration/moz.build19
-rw-r--r--comm/suite/components/migration/public/moz.build15
-rw-r--r--comm/suite/components/migration/public/nsISuiteProfileMigrator.idl76
-rw-r--r--comm/suite/components/migration/public/nsSuiteMigrationCID.h8
-rw-r--r--comm/suite/components/migration/src/moz.build13
-rw-r--r--comm/suite/components/migration/src/nsSuiteProfileMigratorBase.cpp844
-rw-r--r--comm/suite/components/migration/src/nsSuiteProfileMigratorBase.h147
-rw-r--r--comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.cpp66
-rw-r--r--comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.h60
-rw-r--r--comm/suite/components/migration/src/nsThunderbirdProfileMigrator.cpp571
-rw-r--r--comm/suite/components/migration/src/nsThunderbirdProfileMigrator.h44
-rw-r--r--comm/suite/components/moz.build52
-rw-r--r--comm/suite/components/nsAbout.js76
-rw-r--r--comm/suite/components/nsGopherProtocolStubHandler.js67
-rw-r--r--comm/suite/components/nsISuiteGlue.idl42
-rw-r--r--comm/suite/components/nsSuiteGlue.js1676
-rw-r--r--comm/suite/components/permissions/content/cookieViewer.js531
-rw-r--r--comm/suite/components/permissions/content/cookieViewer.xul225
-rw-r--r--comm/suite/components/permissions/content/permissionsManager.js287
-rw-r--r--comm/suite/components/permissions/content/permissionsManager.xul81
-rw-r--r--comm/suite/components/permissions/content/permissionsUtils.js130
-rw-r--r--comm/suite/components/permissions/jar.mn10
-rw-r--r--comm/suite/components/permissions/moz.build7
-rw-r--r--comm/suite/components/places/PlacesUIUtils.jsm1499
-rw-r--r--comm/suite/components/places/content/bookmarkProperties.js524
-rw-r--r--comm/suite/components/places/content/bookmarkProperties.xul41
-rw-r--r--comm/suite/components/places/content/bookmarksPanel.js24
-rw-r--r--comm/suite/components/places/content/bookmarksPanel.xul54
-rw-r--r--comm/suite/components/places/content/browserPlacesViews.js2287
-rw-r--r--comm/suite/components/places/content/controller.js1442
-rw-r--r--comm/suite/components/places/content/editBookmarkOverlay.js1129
-rw-r--r--comm/suite/components/places/content/editBookmarkOverlay.xul191
-rw-r--r--comm/suite/components/places/content/history-panel.js86
-rw-r--r--comm/suite/components/places/content/history-panel.xul95
-rw-r--r--comm/suite/components/places/content/menu.xml624
-rw-r--r--comm/suite/components/places/content/organizer.css7
-rw-r--r--comm/suite/components/places/content/places.css37
-rw-r--r--comm/suite/components/places/content/places.js1366
-rw-r--r--comm/suite/components/places/content/places.xul337
-rw-r--r--comm/suite/components/places/content/placesOverlay.xul228
-rw-r--r--comm/suite/components/places/content/sidebarUtils.js105
-rw-r--r--comm/suite/components/places/content/tree.xml812
-rw-r--r--comm/suite/components/places/content/treeView.js1823
-rw-r--r--comm/suite/components/places/jar.mn30
-rw-r--r--comm/suite/components/places/moz.build28
-rw-r--r--comm/suite/components/places/nsPlacesAutoComplete.js1323
-rw-r--r--comm/suite/components/places/nsPlacesAutoComplete.manifest3
-rw-r--r--comm/suite/components/places/tests/autocomplete/head_autocomplete.js307
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_416211.js30
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_416214.js38
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_417798.js36
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_418257.js43
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_422277.js25
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js54
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_download_embed_bookmarks.js53
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_empty_search.js69
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_enabled.js69
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_escape_self.js30
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_ignore_protocol.js27
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_keyword_search.js73
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_match_beginning.js45
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_multi_word_search.js49
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_special_search.js183
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_swap_protocol.js63
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_tabmatches.js97
-rw-r--r--comm/suite/components/places/tests/autocomplete/test_word_boundary_search.js105
-rw-r--r--comm/suite/components/places/tests/autocomplete/xpcshell.ini29
-rw-r--r--comm/suite/components/places/tests/browser/browser.ini12
-rw-r--r--comm/suite/components/places/tests/browser/browser_0_library_left_pane_migration.js93
-rw-r--r--comm/suite/components/places/tests/browser/browser_425884.js103
-rw-r--r--comm/suite/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js233
-rw-r--r--comm/suite/components/places/tests/browser/browser_library_infoBox.js171
-rw-r--r--comm/suite/components/places/tests/browser/browser_library_left_pane_commands.js100
-rw-r--r--comm/suite/components/places/tests/browser/browser_library_left_pane_fixnames.js92
-rw-r--r--comm/suite/components/places/tests/browser/browser_library_open_leak.js23
-rw-r--r--comm/suite/components/places/tests/browser/browser_library_views_liveupdate.js303
-rw-r--r--comm/suite/components/places/tests/browser/browser_sort_in_library.js254
-rw-r--r--comm/suite/components/places/tests/browser/head.js95
-rw-r--r--comm/suite/components/places/tests/chrome/chrome.ini10
-rw-r--r--comm/suite/components/places/tests/chrome/head.js55
-rw-r--r--comm/suite/components/places/tests/chrome/test_0_bug510634.xul87
-rw-r--r--comm/suite/components/places/tests/chrome/test_0_multiple_left_pane.xul82
-rw-r--r--comm/suite/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul83
-rw-r--r--comm/suite/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul82
-rw-r--r--comm/suite/components/places/tests/chrome/test_bug549192.xul118
-rw-r--r--comm/suite/components/places/tests/chrome/test_bug549491.xul100
-rw-r--r--comm/suite/components/places/tests/chrome/test_treeview_date.xul179
-rw-r--r--comm/suite/components/places/tests/head_common.js868
-rw-r--r--comm/suite/components/places/tests/unit/bookmarks.glue.html16
-rw-r--r--comm/suite/components/places/tests/unit/bookmarks.glue.json1
-rw-r--r--comm/suite/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--comm/suite/components/places/tests/unit/distribution.ini21
-rw-r--r--comm/suite/components/places/tests/unit/head_bookmarks.js46
-rw-r--r--comm/suite/components/places/tests/unit/test_421483.js84
-rw-r--r--comm/suite/components/places/tests/unit/test_PUIU_makeTransaction.js355
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_corrupt.js85
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js81
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js80
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_distribution.js124
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_migrate.js89
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_prefs.js272
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_restore.js81
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_shutdown.js152
-rw-r--r--comm/suite/components/places/tests/unit/test_browserGlue_smartBookmarks.js351
-rw-r--r--comm/suite/components/places/tests/unit/test_clearHistory_shutdown.js181
-rw-r--r--comm/suite/components/places/tests/unit/test_leftpane_corruption_handling.js189
-rw-r--r--comm/suite/components/places/tests/unit/xpcshell.ini19
-rw-r--r--comm/suite/components/pref/content/pref-advanced.js90
-rw-r--r--comm/suite/components/pref/content/pref-advanced.xul174
-rw-r--r--comm/suite/components/pref/content/pref-appearance.js102
-rw-r--r--comm/suite/components/pref/content/pref-appearance.xul103
-rw-r--r--comm/suite/components/pref/content/pref-applicationManager.js100
-rw-r--r--comm/suite/components/pref/content/pref-applicationManager.xul56
-rw-r--r--comm/suite/components/pref/content/pref-applications.js1606
-rw-r--r--comm/suite/components/pref/content/pref-applications.xul113
-rw-r--r--comm/suite/components/pref/content/pref-cache.js113
-rw-r--r--comm/suite/components/pref/content/pref-cache.xul142
-rw-r--r--comm/suite/components/pref/content/pref-colors.js26
-rw-r--r--comm/suite/components/pref/content/pref-colors.xul131
-rw-r--r--comm/suite/components/pref/content/pref-content.js141
-rw-r--r--comm/suite/components/pref/content/pref-content.xul131
-rw-r--r--comm/suite/components/pref/content/pref-cookies.js34
-rw-r--r--comm/suite/components/pref/content/pref-cookies.xul89
-rw-r--r--comm/suite/components/pref/content/pref-debugging.js15
-rw-r--r--comm/suite/components/pref/content/pref-debugging.xul120
-rw-r--r--comm/suite/components/pref/content/pref-download.js197
-rw-r--r--comm/suite/components/pref/content/pref-download.xul120
-rw-r--r--comm/suite/components/pref/content/pref-findasyoutype.js15
-rw-r--r--comm/suite/components/pref/content/pref-findasyoutype.xul70
-rw-r--r--comm/suite/components/pref/content/pref-fonts.js220
-rw-r--r--comm/suite/components/pref/content/pref-fonts.xul260
-rw-r--r--comm/suite/components/pref/content/pref-history.js55
-rw-r--r--comm/suite/components/pref/content/pref-history.xul99
-rw-r--r--comm/suite/components/pref/content/pref-http.js42
-rw-r--r--comm/suite/components/pref/content/pref-http.xul82
-rw-r--r--comm/suite/components/pref/content/pref-images.xul47
-rw-r--r--comm/suite/components/pref/content/pref-keynav.js54
-rw-r--r--comm/suite/components/pref/content/pref-keynav.xul104
-rw-r--r--comm/suite/components/pref/content/pref-languages-add.js147
-rw-r--r--comm/suite/components/pref/content/pref-languages-add.xul54
-rw-r--r--comm/suite/components/pref/content/pref-languages.js200
-rw-r--r--comm/suite/components/pref/content/pref-languages.xul124
-rw-r--r--comm/suite/components/pref/content/pref-links.js15
-rw-r--r--comm/suite/components/pref/content/pref-links.xul78
-rw-r--r--comm/suite/components/pref/content/pref-locationbar.js42
-rw-r--r--comm/suite/components/pref/content/pref-locationbar.xul127
-rw-r--r--comm/suite/components/pref/content/pref-media.xul60
-rw-r--r--comm/suite/components/pref/content/pref-mousewheel.js45
-rw-r--r--comm/suite/components/pref/content/pref-mousewheel.xul298
-rw-r--r--comm/suite/components/pref/content/pref-navigator.js262
-rw-r--r--comm/suite/components/pref/content/pref-navigator.xul188
-rw-r--r--comm/suite/components/pref/content/pref-offlineapps.js178
-rw-r--r--comm/suite/components/pref/content/pref-offlineapps.xul81
-rw-r--r--comm/suite/components/pref/content/pref-popups.js95
-rw-r--r--comm/suite/components/pref/content/pref-popups.xul132
-rw-r--r--comm/suite/components/pref/content/pref-privatedata.js30
-rw-r--r--comm/suite/components/pref/content/pref-privatedata.xul181
-rw-r--r--comm/suite/components/pref/content/pref-proxies-advanced.xul194
-rw-r--r--comm/suite/components/pref/content/pref-proxies.js188
-rw-r--r--comm/suite/components/pref/content/pref-proxies.xul156
-rw-r--r--comm/suite/components/pref/content/pref-scripts.js29
-rw-r--r--comm/suite/components/pref/content/pref-scripts.xul92
-rwxr-xr-xcomm/suite/components/pref/content/pref-search.js60
-rwxr-xr-xcomm/suite/components/pref/content/pref-search.xul50
-rw-r--r--comm/suite/components/pref/content/pref-security.js15
-rw-r--r--comm/suite/components/pref/content/pref-security.xul108
-rw-r--r--comm/suite/components/pref/content/pref-smartupdate.js87
-rw-r--r--comm/suite/components/pref/content/pref-smartupdate.xul139
-rw-r--r--comm/suite/components/pref/content/pref-spelling.js119
-rw-r--r--comm/suite/components/pref/content/pref-spelling.xul80
-rw-r--r--comm/suite/components/pref/content/pref-sync.js143
-rw-r--r--comm/suite/components/pref/content/pref-sync.xul158
-rw-r--r--comm/suite/components/pref/content/pref-tabs.xul113
-rw-r--r--comm/suite/components/pref/content/preferences.js99
-rw-r--r--comm/suite/components/pref/content/preferences.xul264
-rwxr-xr-xcomm/suite/components/pref/content/prefpanels.css33
-rw-r--r--comm/suite/components/pref/content/prefpanels.xml59
-rw-r--r--comm/suite/components/pref/jar.mn73
-rw-r--r--comm/suite/components/pref/moz.build11
-rw-r--r--comm/suite/components/pref/tests/browser/browser.ini3
-rw-r--r--comm/suite/components/pref/tests/browser/browser_bug410900.js55
-rw-r--r--comm/suite/components/profile/content/profileSelection.js344
-rw-r--r--comm/suite/components/profile/content/profileSelection.xul85
-rw-r--r--comm/suite/components/profile/jar.mn8
-rw-r--r--comm/suite/components/profile/moz.build13
-rwxr-xr-xcomm/suite/components/profile/nsSuiteDirectoryProvider.cpp248
-rw-r--r--comm/suite/components/profile/nsSuiteDirectoryProvider.h58
-rw-r--r--comm/suite/components/sanitize/Sanitizer.jsm947
-rw-r--r--comm/suite/components/sanitize/content/sanitizeDialog.js111
-rw-r--r--comm/suite/components/sanitize/content/sanitizeDialog.xul154
-rw-r--r--comm/suite/components/sanitize/jar.mn8
-rw-r--r--comm/suite/components/sanitize/moz.build14
-rw-r--r--comm/suite/components/search/content/engineManager.js508
-rw-r--r--comm/suite/components/search/content/engineManager.xul98
-rw-r--r--comm/suite/components/search/content/search-panel.js86
-rw-r--r--comm/suite/components/search/content/search-panel.xul48
-rw-r--r--comm/suite/components/search/content/search.xml739
-rw-r--r--comm/suite/components/search/content/searchbarBindings.css21
-rw-r--r--comm/suite/components/search/jar.mn16
-rw-r--r--comm/suite/components/search/moz.build7
-rw-r--r--comm/suite/components/search/searchplugins/allegro-pl.xml18
-rw-r--r--comm/suite/components/search/searchplugins/amazon-br.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-de.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-en-GB.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-es.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-fr.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-it.xml20
-rw-r--r--comm/suite/components/search/searchplugins/amazon-jp.xml32
-rw-r--r--comm/suite/components/search/searchplugins/amazon-zh-CN.xml23
-rw-r--r--comm/suite/components/search/searchplugins/amazon.xml27
-rw-r--r--comm/suite/components/search/searchplugins/atlas-sk.xml14
-rw-r--r--comm/suite/components/search/searchplugins/azet-sk.xml15
-rw-r--r--comm/suite/components/search/searchplugins/bing.xml22
-rw-r--r--comm/suite/components/search/searchplugins/bolcom-nl.xml16
-rw-r--r--comm/suite/components/search/searchplugins/chambers-en-GB.xml19
-rw-r--r--comm/suite/components/search/searchplugins/cnrtl-tlfi-fr.xml20
-rw-r--r--comm/suite/components/search/searchplugins/drae.xml16
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-cs-CZ.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-de-DE.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-el-GR.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-en-GB.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-en-US.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-es-AR.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-es-ES.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-fi-FI.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-fr-FR.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-hu-HU.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-it-IT.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-ja-JP.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-nb-NO.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-nl-NL.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-pl-PL.xml27
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-pt-BR.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-pt-PT.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-ru-RU.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-sk-SK.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-sv-SE.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-zh-CN.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo-zh-TW.xml25
-rw-r--r--comm/suite/components/search/searchplugins/duckduckgo.xml23
-rw-r--r--comm/suite/components/search/searchplugins/ebay-de.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay-en-GB.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay-es.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay-fr.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay-it.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay-nl.xml20
-rw-r--r--comm/suite/components/search/searchplugins/ebay.xml20
-rw-r--r--comm/suite/components/search/searchplugins/google-jp.xml31
-rw-r--r--comm/suite/components/search/searchplugins/google.xml24
-rw-r--r--comm/suite/components/search/searchplugins/heureka-cz.xml22
-rw-r--r--comm/suite/components/search/searchplugins/hoepli.xml20
-rw-r--r--comm/suite/components/search/searchplugins/huuto-fi.xml24
-rw-r--r--comm/suite/components/search/searchplugins/images/amazon.icobin0 -> 1407 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/duckduckgo.icobin0 -> 5430 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/ebay.icobin0 -> 1455 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/google.icobin0 -> 5430 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/startpage.icobin0 -> 1150 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/wikipedia.icobin0 -> 884 bytes
-rw-r--r--comm/suite/components/search/searchplugins/images/yahoo.icobin0 -> 5430 bytes
-rw-r--r--comm/suite/components/search/searchplugins/list.json217
-rw-r--r--comm/suite/components/search/searchplugins/mapy-cz.xml18
-rw-r--r--comm/suite/components/search/searchplugins/marktplaats-nl.xml17
-rw-r--r--comm/suite/components/search/searchplugins/priberam.xml16
-rw-r--r--comm/suite/components/search/searchplugins/prisjakt-sv-SE.xml25
-rw-r--r--comm/suite/components/search/searchplugins/pwn-pl.xml14
-rw-r--r--comm/suite/components/search/searchplugins/sapo.xml22
-rw-r--r--comm/suite/components/search/searchplugins/seznam-cz.xml22
-rw-r--r--comm/suite/components/search/searchplugins/startpage-pl.xml17
-rw-r--r--comm/suite/components/search/searchplugins/startpage.xml17
-rw-r--r--comm/suite/components/search/searchplugins/tyda-sv-SE.xml17
-rw-r--r--comm/suite/components/search/searchplugins/vatera.xml18
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-NO.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-cz.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-de.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-el.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-es.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-fi.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-fr.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-hu.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-it.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-ja.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-ka.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-nl.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-pl.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-pt.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-ru.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-sk.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-sv-SE.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-zh-CN.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia-zh-TW.xml25
-rw-r--r--comm/suite/components/search/searchplugins/wikipedia.xml24
-rw-r--r--comm/suite/components/search/searchplugins/wolnelektury-pl.xml22
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-NO.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-ar.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-bid-zh-TW.xml18
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-br.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-de.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-en-GB.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-es.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-fi.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-fr.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-it.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-jp.xml18
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-nl.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-sv-SE.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-zh-CN.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo-zh-TW.xml25
-rw-r--r--comm/suite/components/search/searchplugins/yahoo.xml25
-rw-r--r--comm/suite/components/search/searchplugins/zoznam-sk.xml13
-rw-r--r--comm/suite/components/security/content/prefs/pref-certs.js32
-rw-r--r--comm/suite/components/security/content/prefs/pref-certs.xul100
-rw-r--r--comm/suite/components/security/content/prefs/pref-passwords.js31
-rw-r--r--comm/suite/components/security/content/prefs/pref-passwords.xul82
-rw-r--r--comm/suite/components/security/content/prefs/pref-ssl.js82
-rw-r--r--comm/suite/components/security/content/prefs/pref-ssl.xul120
-rw-r--r--comm/suite/components/security/jar.mn11
-rw-r--r--comm/suite/components/security/moz.build6
-rw-r--r--comm/suite/components/sessionstore/XPathGenerator.jsm97
-rw-r--r--comm/suite/components/sessionstore/content/aboutSessionRestore.js291
-rw-r--r--comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml84
-rw-r--r--comm/suite/components/sessionstore/jar.mn7
-rw-r--r--comm/suite/components/sessionstore/moz.build24
-rw-r--r--comm/suite/components/sessionstore/nsISessionStartup.idl39
-rw-r--r--comm/suite/components/sessionstore/nsISessionStore.idl216
-rw-r--r--comm/suite/components/sessionstore/nsSessionStartup.js223
-rw-r--r--comm/suite/components/sessionstore/nsSessionStartup.manifest11
-rw-r--r--comm/suite/components/sessionstore/nsSessionStore.js4174
-rw-r--r--comm/suite/components/shell/ShellService.jsm110
-rw-r--r--comm/suite/components/shell/content/setDesktopBackground.js78
-rw-r--r--comm/suite/components/shell/content/setDesktopBackground.xul49
-rw-r--r--comm/suite/components/shell/jar.mn7
-rw-r--r--comm/suite/components/shell/moz.build49
-rw-r--r--comm/suite/components/shell/nsGNOMEShellService.cpp463
-rw-r--r--comm/suite/components/shell/nsGNOMEShellService.h37
-rw-r--r--comm/suite/components/shell/nsIGNOMEShellService.idl18
-rw-r--r--comm/suite/components/shell/nsIMacShellService.idl14
-rw-r--r--comm/suite/components/shell/nsIShellService.idl98
-rw-r--r--comm/suite/components/shell/nsMacShellService.cpp398
-rw-r--r--comm/suite/components/shell/nsMacShellService.h36
-rw-r--r--comm/suite/components/shell/nsSetDefault.js53
-rw-r--r--comm/suite/components/shell/nsSetDefault.manifest3
-rw-r--r--comm/suite/components/shell/nsShellService.h11
-rw-r--r--comm/suite/components/shell/nsWindowsShellService.cpp793
-rw-r--r--comm/suite/components/shell/nsWindowsShellService.h41
-rw-r--r--comm/suite/components/sidebar/SuiteSidebar.manifest4
-rw-r--r--comm/suite/components/sidebar/content/PageNotFound.xul13
-rw-r--r--comm/suite/components/sidebar/content/customize-panel.js43
-rw-r--r--comm/suite/components/sidebar/content/customize-panel.xul23
-rw-r--r--comm/suite/components/sidebar/content/customize.js692
-rw-r--r--comm/suite/components/sidebar/content/customize.xul137
-rw-r--r--comm/suite/components/sidebar/content/preview.js15
-rw-r--r--comm/suite/components/sidebar/content/preview.xul30
-rw-r--r--comm/suite/components/sidebar/content/sidebarBindings.xml34
-rw-r--r--comm/suite/components/sidebar/content/sidebarOverlay.css78
-rw-r--r--comm/suite/components/sidebar/content/sidebarOverlay.js1704
-rw-r--r--comm/suite/components/sidebar/content/sidebarOverlay.xul247
-rw-r--r--comm/suite/components/sidebar/jar.mn16
-rw-r--r--comm/suite/components/sidebar/moz.build18
-rw-r--r--comm/suite/components/sidebar/nsISidebar.idl26
-rw-r--r--comm/suite/components/sidebar/nsSidebar.js348
-rw-r--r--comm/suite/components/sync/content/aboutSyncTabs-bindings.xml46
-rw-r--r--comm/suite/components/sync/content/aboutSyncTabs.css11
-rw-r--r--comm/suite/components/sync/content/aboutSyncTabs.js293
-rw-r--r--comm/suite/components/sync/content/aboutSyncTabs.xul69
-rw-r--r--comm/suite/components/sync/content/syncAddDevice.js142
-rw-r--r--comm/suite/components/sync/content/syncAddDevice.xul128
-rw-r--r--comm/suite/components/sync/content/syncGenericChange.js232
-rw-r--r--comm/suite/components/sync/content/syncGenericChange.xul116
-rw-r--r--comm/suite/components/sync/content/syncKey.xhtml49
-rw-r--r--comm/suite/components/sync/content/syncNotification.xml93
-rw-r--r--comm/suite/components/sync/content/syncQuota.js252
-rw-r--r--comm/suite/components/sync/content/syncQuota.xul62
-rw-r--r--comm/suite/components/sync/content/syncSetup.js961
-rw-r--r--comm/suite/components/sync/content/syncSetup.xul482
-rw-r--r--comm/suite/components/sync/content/syncUI.js454
-rw-r--r--comm/suite/components/sync/content/syncUtils.js224
-rw-r--r--comm/suite/components/sync/jar.mn21
-rw-r--r--comm/suite/components/sync/moz.build7
-rw-r--r--comm/suite/components/tests/browser/browser.ini72
-rw-r--r--comm/suite/components/tests/browser/browser_339445.js34
-rw-r--r--comm/suite/components/tests/browser/browser_339445_sample.html17
-rw-r--r--comm/suite/components/tests/browser/browser_345898.js45
-rw-r--r--comm/suite/components/tests/browser/browser_346337.js122
-rw-r--r--comm/suite/components/tests/browser/browser_346337_sample.html37
-rw-r--r--comm/suite/components/tests/browser/browser_350525.js100
-rw-r--r--comm/suite/components/tests/browser/browser_354894.js459
-rw-r--r--comm/suite/components/tests/browser/browser_367052.js38
-rw-r--r--comm/suite/components/tests/browser/browser_393716.js74
-rw-r--r--comm/suite/components/tests/browser/browser_394759_basic.js77
-rw-r--r--comm/suite/components/tests/browser/browser_394759_behavior.js66
-rw-r--r--comm/suite/components/tests/browser/browser_408470.js57
-rw-r--r--comm/suite/components/tests/browser/browser_408470_sample.html19
-rw-r--r--comm/suite/components/tests/browser/browser_423132.js86
-rw-r--r--comm/suite/components/tests/browser/browser_423132_sample.html13
-rw-r--r--comm/suite/components/tests/browser/browser_447951.js50
-rw-r--r--comm/suite/components/tests/browser/browser_447951_sample.html4
-rw-r--r--comm/suite/components/tests/browser/browser_448741.js62
-rw-r--r--comm/suite/components/tests/browser/browser_454908.js52
-rw-r--r--comm/suite/components/tests/browser/browser_454908_sample.html8
-rw-r--r--comm/suite/components/tests/browser/browser_456342.js47
-rw-r--r--comm/suite/components/tests/browser/browser_456342_sample.xhtml28
-rw-r--r--comm/suite/components/tests/browser/browser_461634.js88
-rw-r--r--comm/suite/components/tests/browser/browser_463206.js64
-rw-r--r--comm/suite/components/tests/browser/browser_463206_sample.html10
-rw-r--r--comm/suite/components/tests/browser/browser_465215.js39
-rw-r--r--comm/suite/components/tests/browser/browser_465223.js60
-rw-r--r--comm/suite/components/tests/browser/browser_466937.js43
-rw-r--r--comm/suite/components/tests/browser/browser_466937_sample.html21
-rw-r--r--comm/suite/components/tests/browser/browser_477657.js85
-rw-r--r--comm/suite/components/tests/browser/browser_480893.js58
-rw-r--r--comm/suite/components/tests/browser/browser_483330.js37
-rw-r--r--comm/suite/components/tests/browser/browser_485482.js36
-rw-r--r--comm/suite/components/tests/browser/browser_485482_sample.html12
-rw-r--r--comm/suite/components/tests/browser/browser_490040.js139
-rw-r--r--comm/suite/components/tests/browser/browser_491168.js64
-rw-r--r--comm/suite/components/tests/browser/browser_491577.js119
-rw-r--r--comm/suite/components/tests/browser/browser_493467.js48
-rw-r--r--comm/suite/components/tests/browser/browser_500328.js115
-rw-r--r--comm/suite/components/tests/browser/browser_514751.js59
-rw-r--r--comm/suite/components/tests/browser/browser_522545.js280
-rw-r--r--comm/suite/components/tests/browser/browser_524745.js59
-rw-r--r--comm/suite/components/tests/browser/browser_526613.js71
-rw-r--r--comm/suite/components/tests/browser/browser_528776.js29
-rw-r--r--comm/suite/components/tests/browser/browser_581937.js38
-rw-r--r--comm/suite/components/tests/browser/browser_586068-cascaded_restore.js730
-rw-r--r--comm/suite/components/tests/browser/browser_597315.js64
-rwxr-xr-xcomm/suite/components/tests/browser/browser_597315_a.html5
-rwxr-xr-xcomm/suite/components/tests/browser/browser_597315_b.html10
-rwxr-xr-xcomm/suite/components/tests/browser/browser_597315_c.html5
-rwxr-xr-xcomm/suite/components/tests/browser/browser_597315_c1.html5
-rwxr-xr-xcomm/suite/components/tests/browser/browser_597315_c2.html5
-rw-r--r--comm/suite/components/tests/browser/browser_597315_index.html10
-rw-r--r--comm/suite/components/tests/browser/browser_607016.js120
-rw-r--r--comm/suite/components/tests/browser/browser_615394-SSWindowState_events.js362
-rw-r--r--comm/suite/components/tests/browser/browser_625257.js87
-rw-r--r--comm/suite/components/tests/browser/browser_636279.js102
-rw-r--r--comm/suite/components/tests/browser/browser_637020.js65
-rw-r--r--comm/suite/components/tests/browser/browser_637020_slow.sjs18
-rw-r--r--comm/suite/components/tests/browser/browser_645428.js22
-rw-r--r--comm/suite/components/tests/browser/browser_665702-state_session.js25
-rw-r--r--comm/suite/components/tests/browser/browser_687710.js44
-rw-r--r--comm/suite/components/tests/browser/browser_687710_2.js64
-rw-r--r--comm/suite/components/tests/browser/browser_694378.js33
-rw-r--r--comm/suite/components/tests/browser/browser_bug431826.js42
-rw-r--r--comm/suite/components/tests/browser/browser_isempty.js28
-rw-r--r--comm/suite/components/tests/browser/browser_markPageAsFollowedLink.js87
-rw-r--r--comm/suite/components/tests/browser/frameLeft.html8
-rw-r--r--comm/suite/components/tests/browser/frameRight.html8
-rw-r--r--comm/suite/components/tests/browser/framedPage.html9
-rw-r--r--comm/suite/components/tests/browser/head.js151
-rw-r--r--comm/suite/components/tests/chrome/chrome.ini4
-rw-r--r--comm/suite/components/tests/chrome/test_idcheck.xul302
555 files changed, 89867 insertions, 0 deletions
diff --git a/comm/suite/components/SuiteComponents.manifest b/comm/suite/components/SuiteComponents.manifest
new file mode 100644
index 0000000000..12a0d1ad77
--- /dev/null
+++ b/comm/suite/components/SuiteComponents.manifest
@@ -0,0 +1,18 @@
+component {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f} nsAbout.js
+contract @mozilla.org/network/protocol/about;1?what= {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=blocked {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=certerror {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=data {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=feeds {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=life {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=newserror {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=privatebrowsing {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=rights {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+contract @mozilla.org/network/protocol/about;1?what=sessionrestore {d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}
+component {22042bdb-56e4-47c6-8b12-fdfa859c05a9} nsGopherProtocolStubHandler.js
+contract @mozilla.org/network/protocol;1?name=gopher {22042bdb-56e4-47c6-8b12-fdfa859c05a9}
+component {bbbbe845-5a1b-40ee-813c-f84b8faaa07c} nsSuiteGlue.js
+contract @mozilla.org/suite/suiteglue;1 {bbbbe845-5a1b-40ee-813c-f84b8faaa07c}
+category app-startup nsSuiteGlue service,@mozilla.org/suite/suiteglue;1
+component {9d4c845d-3f09-402a-b66d-50f291d7d50f} nsSuiteGlue.js
+contract @mozilla.org/content-permission/prompt;1 {9d4c845d-3f09-402a-b66d-50f291d7d50f}
diff --git a/comm/suite/components/autocomplete/content/autocomplete.css b/comm/suite/components/autocomplete/content/autocomplete.css
new file mode 100644
index 0000000000..6c67bad2ed
--- /dev/null
+++ b/comm/suite/components/autocomplete/content/autocomplete.css
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+.autocomplete-result-popupset {
+ width: 0 !important;
+}
+
+.autocomplete-result-popup {
+ display: -moz-popup !important;
+}
+
+/* the C++ implementation of widgets is too eager to make popups visible.
+ this causes problems (bug 120155 and others), thus this workaround: */
+.autocomplete-result-popup[hidden="true"] {
+ visibility: hidden;
+}
+
+.autocomplete-tree {
+ -moz-user-focus: ignore;
+}
+
+.autocomplete-history-dropmarker {
+ display: none;
+}
+
+.autocomplete-history-dropmarker[enablehistory="true"] {
+ display: -moz-box;
+}
+
+/* The following rule is here to fix bug 96899 (and now 117952).
+ Somehow trees create a situation
+ in which a popupset flows itself as if its popup child is directly within it
+ instead of the placeholder child that should actually be inside the popupset.
+ This is a stopgap measure, and it does not address the real bug. */
+popupset {
+ max-width: 0px;
+ width: 0px;
+ min-width: 0%;
+ min-height: 0%;
+}
+
+treecolpicker {
+ display: none;
+}
diff --git a/comm/suite/components/autocomplete/content/autocomplete.xml b/comm/suite/components/autocomplete/content/autocomplete.xml
new file mode 100644
index 0000000000..58b171e641
--- /dev/null
+++ b/comm/suite/components/autocomplete/content/autocomplete.xml
@@ -0,0 +1,1641 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="autocompleteBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="autocomplete" role="xul:combobox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content>
+ <children includes="menupopup"/>
+
+ <xul:hbox class="autocomplete-textbox-container" flex="1" align="center">
+ <children includes="image|deck|stack|box">
+ <xul:image class="autocomplete-icon" allowevents="true"/>
+ </children>
+
+ <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,tooltiptext=inputtooltiptext">
+ <children/>
+ <html:input anonid="input" class="autocomplete-textbox textbox-input"
+ allowevents="true"
+ xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,userAction"/>
+ </xul:hbox>
+ <children includes="hbox"/>
+ </xul:hbox>
+
+ <xul:dropmarker class="autocomplete-history-dropmarker" allowevents="true"
+ xbl:inherits="open,enablehistory" anonid="historydropmarker"/>
+
+ <xul:popupset>
+ <xul:panel type="autocomplete" anonid="popup"
+ ignorekeys="true" noautofocus="true" level="top"
+ xbl:inherits="for=id,nomatch"/>
+ </xul:popupset>
+ </content>
+
+ <implementation implements="nsIDOMXULMenuListElement">
+
+ <constructor><![CDATA[
+ // XXX bug 90337 band-aid until we figure out what's going on here
+ if (this.value != this.mInputElt.value)
+ this.mInputElt.value = this.value;
+ delete this.value;
+
+ // listen for pastes
+ this.mInputElt.controllers.insertControllerAt(0, this.mPasteController);
+
+ // listen for menubar activation
+ window.top.addEventListener("DOMMenuBarActive", this.mMenuBarListener, true);
+
+ // set default property values
+ this.ifSetAttribute("timeout", 50);
+ this.ifSetAttribute("pastetimeout", 1000);
+ this.ifSetAttribute("maxrows", 5);
+ this.ifSetAttribute("showpopup", true);
+ this.ifSetAttribute("disableKeyNavigation", true);
+
+ // initialize the search sessions
+ if (this.hasAttribute("autocompletesearch"))
+ this.initAutoCompleteSearch();
+
+ // hack to work around lack of bottom-up constructor calling
+ if ("initialize" in this.popup)
+ this.popup.initialize();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.clearResults(false);
+ window.top.removeEventListener("DOMMenuBarActive", this.mMenuBarListener, true);
+ this.mInputElt.controllers.removeController(this.mPasteController);
+ ]]></destructor>
+
+ <!-- =================== nsIAutoCompleteInput =================== -->
+ <!-- XXX: This implementation is currently incomplete. -->
+
+ <!-- reference to the results popup element -->
+ <field name="popup"><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "popup");
+ ]]></field>
+
+ <property name="popupOpen"
+ onget="return this.mMenuOpen;"
+ onset="if (val) this.openPopup(); else this.closePopup(); return val;"/>
+
+ <!-- option to turn off autocomplete -->
+ <property name="disableAutoComplete"
+ onset="this.setAttribute('disableautocomplete', val); return val;"
+ onget="return this.getAttribute('disableautocomplete') == 'true';"/>
+
+ <!-- if the resulting match string is not at the beginning of the typed string,
+ this will optionally autofill like this "bar |>> foobar|" -->
+ <property name="completeDefaultIndex"
+ onset="this.setAttribute('completedefaultindex', val); return val;"
+ onget="return this.getAttribute('completedefaultindex') == 'true';"/>
+
+ <!-- option for completing to the default result whenever the user hits
+ enter or the textbox loses focus -->
+ <property name="forceComplete"
+ onset="this.setAttribute('forcecomplete', val); return val;"
+ onget="return this.getAttribute('forcecomplete') == 'true';"/>
+
+ <property name="minResultsForPopup"
+ onset="this.setAttribute('minresultsforpopup', val); return val;"
+ onget="var t = this.getAttribute('minresultsforpopup'); return t ? parseInt(t) : 1;"/>
+
+ <!-- maximum number of rows to display -->
+ <property name="maxRows"
+ onset="this.setAttribute('maxrows', val); return val;"
+ onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
+
+ <!-- toggles a second column in the results list which contains
+ the string in the comment field of each autocomplete result -->
+ <property name="showCommentColumn"
+ onget="return this.getAttribute('showcommentcolumn') == 'true';">
+ <setter><![CDATA[
+ this.popup.showCommentColumn = val;
+ this.setAttribute('showcommentcolumn', val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- number of milliseconds after a keystroke before a search begins -->
+ <property name="timeout"
+ onset="this.setAttribute('timeout', val); return val;"
+ onget="return parseInt(this.getAttribute('timeout')) || 0;"/>
+
+ <property name="searchParam"
+ onget="return this.getAttribute('autocompletesearchparam') || '';"
+ onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+
+ <property name="searchCount" readonly="true"
+ onget="return this.sessionCount;"/>
+
+ <method name="getSearchAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var idx = -1;
+ for (var name in this.mSessions)
+ if (++idx == aIndex)
+ return name;
+
+ return null;
+ ]]></body>
+ </method>
+
+ <property name="textValue"
+ onget="return this.value;"
+ onset="this.setTextValue(val); return val;"/>
+
+ <method name="onSearchBegin">
+ <body><![CDATA[
+ this._fireEvent("searchbegin");
+ ]]></body>
+ </method>
+
+ <method name="onSearchComplete">
+ <body><![CDATA[
+ if (this.noMatch)
+ this.setAttribute("nomatch", "true");
+ else
+ this.removeAttribute("nomatch");
+
+ this._fireEvent("searchcomplete");
+ ]]></body>
+ </method>
+
+ <method name="onTextReverted">
+ <body><![CDATA[
+ return this._fireEvent("textreverted");
+ ]]></body>
+ </method>
+
+ <!-- =================== nsIDOMXULMenuListElement =================== -->
+
+ <property name="editable" readonly="true"
+ onget="return true;" />
+
+ <property name="crop"
+ onset="this.setAttribute('crop', val); return val;"
+ onget="return this.getAttribute('crop');"/>
+
+ <property name="label" readonly="true"
+ onget="return this.mInputElt.value;"/>
+
+ <property name="open"
+ onget="return this.getAttribute('open') == 'true';">
+ <setter>
+ <![CDATA[
+ var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
+ if (val) {
+ this.setAttribute('open', true);
+ historyPopup.showPopup();
+ } else {
+ this.removeAttribute('open');
+ historyPopup.hidePopup();
+ }
+ ]]>
+ </setter>
+ </property>
+
+ <!-- =================== PUBLIC PROPERTIES =================== -->
+
+ <property name="value"
+ onget="return this.mInputElt.value;">
+ <setter><![CDATA[
+ this.ignoreInputEvent = true;
+ this.mInputElt.value = val;
+ this.ignoreInputEvent = false;
+ var event = document.createEvent('Events');
+ event.initEvent('ValueChange', true, true);
+ this.mInputElt.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="focused"
+ onget="return this.getAttribute('focused') == 'true';"/>
+
+ <method name="initAutoCompleteSearch">
+ <body><![CDATA[
+ var list = this.getAttribute("autocompletesearch").split(" ");
+ for (var i = 0; i < list.length; i++) {
+ var name = list[i];
+ var contractid = "@mozilla.org/autocomplete/search;1?name=" + name;
+ if (contractid in Cc) {
+ try {
+ this.mSessions[name] =
+ Cc[contractid].getService(Ci.nsIAutoCompleteSearch);
+ this.mLastResults[name] = null;
+ this.mLastRows[name] = 0;
+ ++this.sessionCount;
+ } catch (e) {
+ dump("### ERROR - unable to create search \"" + name + "\".\n");
+ }
+ } else {
+ dump("search \"" + name + "\" not found - skipping.\n");
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- the number of sessions currently in use -->
+ <field name="sessionCount">0</field>
+
+ <!-- number of milliseconds after a paste before a search begins -->
+ <property name="pasteTimeout"
+ onset="this.setAttribute('pastetimeout', val); return val;"
+ onget="var t = parseInt(this.getAttribute('pastetimeout')); return t ? t : 0;"/>
+
+ <!-- option for filling the textbox with the best match while typing
+ and selecting the difference -->
+ <property name="autoFill"
+ onset="this.setAttribute('autofill', val); return val;"
+ onget="return this.getAttribute('autofill') == 'true';"/>
+
+ <!-- if this attribute is set, allow different style for
+ non auto-completed lines -->
+ <property name="highlightNonMatches"
+ onset="this.setAttribute('highlightnonmatches', val); return val;"
+ onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
+
+ <!-- option to show the popup containing the results -->
+ <property name="showPopup"
+ onset="this.setAttribute('showpopup', val); return val;"
+ onget="return this.getAttribute('showpopup') == 'true';"/>
+
+ <!-- option to allow scrolling through the list via the tab key, rather than
+ tab moving focus out of the textbox -->
+ <property name="tabScrolling"
+ onset="this.setAttribute('tabscrolling', val); return val;"
+ onget="return this.getAttribute('tabscrolling') == 'true';"/>
+
+ <!-- option to completely ignore any blur events while
+ searches are still going on. This is useful so that nothing
+ gets autopicked if the window is required to lose focus for
+ some reason (eg in LDAP autocomplete, another window may be
+ brought up so that the user can enter a password to authenticate
+ to an LDAP server). -->
+ <property name="ignoreBlurWhileSearching"
+ onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
+ onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
+
+ <!-- state which indicates the current action being performed by the user.
+ Possible values are : none, typing, scrolling -->
+ <property name="userAction"
+ onset="this.setAttribute('userAction', val); return val;"
+ onget="return this.getAttribute('userAction');"/>
+
+ <!-- state which indicates if the last search had no matches -->
+ <field name="noMatch">true</field>
+
+ <!-- state which indicates a search is currently happening -->
+ <field name="isSearching">false</field>
+
+ <!-- state which indicates a search timeout is current waiting -->
+ <property name="isWaiting"
+ onget="return this.mAutoCompleteTimer != 0;"/>
+
+ <!-- =================== PRIVATE PROPERTIES =================== -->
+
+ <field name="mSessions">({})</field>
+ <field name="mLastResults">({})</field>
+ <field name="mLastRows">({})</field>
+ <field name="mLastKeyCode">null</field>
+ <field name="mAutoCompleteTimer">0</field>
+ <field name="mMenuOpen">false</field>
+ <field name="mFireAfterSearch">false</field>
+ <field name="mFinishAfterSearch">false</field>
+ <field name="mNeedToFinish">false</field>
+ <field name="mNeedToComplete">false</field>
+ <field name="mTransientValue">false</field>
+ <field name="mView">null</field>
+ <field name="currentSearchString">""</field>
+ <field name="ignoreInputEvent">false</field>
+ <field name="oninit">null</field>
+ <field name="mDefaultMatchFilled">false</field>
+ <field name="mFirstReturn">true</field>
+ <field name="mIsPasting">false</field>
+
+ <field name="mPasteController"><![CDATA[
+ ({
+ self: this,
+ kGlobalClipboard: Ci.nsIClipboard.kGlobalClipboard,
+ supportsCommand: function(aCommand) {
+ return aCommand == "cmd_paste";
+ },
+ isCommandEnabled: function(aCommand) {
+ return aCommand == "cmd_paste" &&
+ this.self.editor.isSelectionEditable &&
+ this.self.editor.canPaste(this.kGlobalClipboard);
+ },
+ doCommand: function(aCommand) {
+ if (aCommand == "cmd_paste") {
+ this.self.mIsPasting = true;
+ this.self.editor.paste(this.kGlobalClipboard);
+ this.self.mIsPasting = false;
+ }
+ },
+ onEvent: function() {}
+ })
+ ]]></field>
+
+ <field name="mMenuBarListener"><![CDATA[
+ ({
+ self: this,
+ handleEvent: function(aEvent) {
+ try {
+ this.self.finishAutoComplete(false, false, aEvent);
+ this.self.clearTimer();
+ this.self.closePopup();
+ } catch (e) {
+ window.top.removeEventListener("DOMMenuBarActive", this, true);
+ }
+ }
+ })
+ ]]></field>
+
+ <field name="mAutoCompleteObserver"><![CDATA[
+ ({
+ self: this,
+ onSearchResult: function(aSearch, aResult) {
+ for (var name in this.self.mSessions)
+ if (this.self.mSessions[name] == aSearch)
+ this.self.processResults(name, aResult);
+ }
+ })
+ ]]></field>
+
+ <field name="mInputElt"><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "input");
+ ]]></field>
+
+ <field name="mMenuAccessKey"><![CDATA[
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch)
+ .getIntPref("ui.key.menuAccessKey");
+ ]]></field>
+
+ <!-- =================== PUBLIC METHODS =================== -->
+
+ <method name="getErrorAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var obj = aIndex < 0 ? null : this.convertIndexToSession(aIndex);
+ return obj && this.mLastResults[obj.session] &&
+ this.mLastResults[obj.session].errorDescription;
+ ]]></body>
+ </method>
+
+ <!-- get a value from the autocomplete results as a string via an absolute index-->
+ <method name="getResultValueAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var obj = this.convertIndexToSession(aIndex);
+ return obj ? this.getSessionValueAt(obj.session, obj.index) : null;
+ ]]></body>
+ </method>
+
+ <!-- get a value from the autocomplete results as a string from a specific session -->
+ <method name="getSessionValueAt">
+ <parameter name="aSession"/>
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var result = this.mLastResults[aSession];
+ return result.errorDescription || result.getValueAt(aIndex);
+ ]]></body>
+ </method>
+
+ <!-- get the total number of results overall -->
+ <method name="getResultCount">
+ <body><![CDATA[
+ return this.view.rowCount;
+ ]]></body>
+ </method>
+
+ <!-- get the first session that has results -->
+ <method name="getDefaultSession">
+ <body><![CDATA[
+ for (var name in this.mLastResults) {
+ var results = this.mLastResults[name];
+ if (results && results.matchCount > 0 && !results.errorDescription)
+ return name;
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- empty the cached result data and empty the results popup -->
+ <method name="clearResults">
+ <parameter name="aInvalidate"/>
+ <body><![CDATA[
+ this.clearResultData();
+ this.clearResultElements(aInvalidate);
+ ]]></body>
+ </method>
+
+ <!-- =================== PRIVATE METHODS =================== -->
+
+ <!-- ::::::::::::: session searching ::::::::::::: -->
+
+ <!-- -->
+ <method name="callListener">
+ <parameter name="me"/>
+ <parameter name="aAction"/>
+ <body><![CDATA[
+ // bail if the binding was detached or the element removed from
+ // document during the timeout
+ if (!("startLookup" in me) || !me.ownerDocument || !me.parentNode)
+ return;
+
+ me.clearTimer();
+
+ if (me.disableAutoComplete)
+ return;
+
+ switch (aAction) {
+ case "startLookup":
+ me.startLookup();
+ break;
+
+ case "stopLookup":
+ me.stopLookup();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="startLookup">
+ <body><![CDATA[
+ var str = this.currentSearchString;
+ if (!str) {
+ this.clearResults(false);
+ this.closePopup();
+ return;
+ }
+
+ this.isSearching = true;
+ this.mFirstReturn = true;
+ this.mSessionReturns = this.sessionCount;
+ this.mFailureItems = 0;
+ this.mDefaultMatchFilled = false; // clear out our prefill state.
+
+ // Notify the input that the search is beginning.
+ this.onSearchBegin();
+
+ // tell each session to start searching...
+ for (var name in this.mSessions)
+ try {
+ this.mSessions[name].startSearch(str, this.searchParam, this.mLastResults[name], this.mAutoCompleteObserver);
+ } catch (e) {
+ --this.mSessionReturns;
+ this.searchFailed();
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="stopLookup">
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mSessions[name].stopSearch();
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processResults">
+ <parameter name="aSessionName"/>
+ <parameter name="aResults"/>
+ <body><![CDATA[
+ if (this.disableAutoComplete)
+ return;
+
+ const ACR = Ci.nsIAutoCompleteResult;
+ var status = aResults.searchResult;
+ if (status != ACR.RESULT_NOMATCH_ONGOING &&
+ status != ACR.RESULT_SUCCESS_ONGOING)
+ --this.mSessionReturns;
+
+ // check the many criteria for failure
+ if (aResults.errorDescription)
+ ++this.mFailureItems;
+ else if (status == ACR.RESULT_IGNORED ||
+ status == ACR.RESULT_FAILURE ||
+ status == ACR.RESULT_NOMATCH ||
+ status == ACR.RESULT_NOMATCH_ONGOING ||
+ aResults.matchCount == 0 ||
+ aResults.searchString != this.currentSearchString)
+ {
+ this.mLastResults[aSessionName] = null;
+ if (this.mFirstReturn)
+ this.clearResultElements(false);
+ this.mFirstReturn = false;
+ this.searchFailed();
+ return;
+ }
+
+ if (this.mFirstReturn) {
+ if (this.view.mTree)
+ this.view.mTree.beginUpdateBatch();
+ this.clearResultElements(false); // clear results, but don't repaint yet
+ }
+
+ // always call openPopup...we may not have opened it
+ // if a previous search session didn't return enough search results.
+ // it's smart and doesn't try to open itself multiple times...
+ // be sure to add our result elements before calling openPopup as we need
+ // to know the total # of results found so far.
+ this.addResultElements(aSessionName, aResults);
+
+ this.autoFillInput(aSessionName, aResults, false);
+ if (this.mFirstReturn && this.view.mTree)
+ this.view.mTree.endUpdateBatch();
+ this.openPopup();
+ this.mFirstReturn = false;
+
+ // if this is the last session to return...
+ if (this.mSessionReturns == 0)
+ this.postSearchCleanup();
+
+ if (this.mFinishAfterSearch)
+ this.finishAutoComplete(false, this.mFireAfterSearch, null);
+ ]]></body>
+ </method>
+
+ <!-- called each time a search fails, except when failure items need
+ to be displayed. If all searches have failed, clear the list
+ and close the popup -->
+ <method name="searchFailed">
+ <body><![CDATA[
+ // if all searches are done and they all failed...
+ if (this.mSessionReturns == 0 && this.getResultCount() == 0) {
+ if (this.minResultsForPopup == 0) {
+ this.clearResults(true); // clear data and repaint empty
+ this.openPopup();
+ } else {
+ this.closePopup();
+ }
+ }
+
+ // if it's the last session to return, time to clean up...
+ if (this.mSessionReturns == 0)
+ this.postSearchCleanup();
+ ]]></body>
+ </method>
+
+ <!-- does some stuff after a search is done (success or failure) -->
+ <method name="postSearchCleanup">
+ <body><![CDATA[
+ this.isSearching = false;
+
+ // figure out if there are no matches in all search sessions
+ var failed = true;
+ for (var name in this.mSessions) {
+ if (this.mLastResults[name])
+ failed = this.mLastResults[name].errorDescription ||
+ this.mLastResults[name].matchCount == 0;
+ if (!failed)
+ break;
+ }
+ this.noMatch = failed;
+
+ // if we have processed all of our searches, and none of them gave us a default index,
+ // then we should try to auto fill the input field with the first match.
+ // note: autoFillInput is smart enough to kick out if we've already prefilled something...
+ if (!this.noMatch) {
+ var defaultSession = this.getDefaultSession();
+ if (defaultSession)
+ this.autoFillInput(defaultSession, this.mLastResults[defaultSession], true);
+ }
+
+ // Notify the input that the search is complete.
+ this.onSearchComplete();
+ ]]></body>
+ </method>
+
+ <!-- when the focus exits the widget or user hits return,
+ determine what value to leave in the textbox -->
+ <method name="finishAutoComplete">
+ <parameter name="aForceComplete"/>
+ <parameter name="aFireTextCommand"/>
+ <parameter name="aTriggeringEvent"/>
+ <body><![CDATA[
+ this.mFinishAfterSearch = false;
+ this.mFireAfterSearch = false;
+ if (this.mNeedToFinish && !this.disableAutoComplete) {
+ // set textbox value to either override value, or default search result
+ var val = this.popup.overrideValue;
+ if (val) {
+ this.setTextValue(val);
+ this.mNeedToFinish = false;
+ } else if (this.mTransientValue ||
+ !(this.forceComplete ||
+ (aForceComplete &&
+ this.mDefaultMatchFilled &&
+ this.mNeedToComplete))) {
+ this.mNeedToFinish = false;
+ } else if (this.isWaiting) {
+ // if the user typed, the search results are out of date, so let
+ // the search finish, and tell it to come back here when it's done
+ this.mFinishAfterSearch = true;
+ this.mFireAfterSearch = aFireTextCommand;
+ return;
+ } else {
+ // we want to use the default item index for the first session which gave us a valid
+ // default item index...
+ for (var name in this.mLastResults) {
+ var results = this.mLastResults[name];
+ if (results && results.matchCount > 0 &&
+ !results.errorDescription && results.defaultIndex != -1)
+ {
+ val = results.getValueAt(results.defaultIndex);
+ this.setTextValue(val);
+ this.mDefaultMatchFilled = true;
+ this.mNeedToFinish = false;
+ break;
+ }
+ }
+
+ if (this.mNeedToFinish) {
+ // if a search is happening at this juncture, bail out of this function
+ // and let the search finish, and tell it to come back here when it's done
+ if (this.isSearching) {
+ this.mFinishAfterSearch = true;
+ this.mFireAfterSearch = aFireTextCommand;
+ return;
+ }
+
+ this.mNeedToFinish = false;
+ var defaultSession = this.getDefaultSession();
+ if (defaultSession)
+ {
+ // preselect the first one
+ var first = this.getSessionValueAt(defaultSession, 0);
+ this.setTextValue(first);
+ this.mDefaultMatchFilled = true;
+ }
+ }
+ }
+
+ this.stopLookup();
+
+ this.closePopup();
+ }
+
+ this.mNeedToComplete = false;
+ this.clearTimer();
+
+ if (aFireTextCommand)
+ this._fireEvent("textentered", this.userAction, aTriggeringEvent);
+ ]]></body>
+ </method>
+
+ <!-- when the user clicks an entry in the autocomplete popup -->
+ <method name="onResultClick">
+ <body><![CDATA[
+ // set textbox value to either override value, or the clicked result
+ var errItem = this.getErrorAt(this.popup.selectedIndex);
+ var val = this.popup.overrideValue;
+ if (val)
+ this.setTextValue(val);
+ else if (this.popup.selectedIndex != -1) {
+ if (errItem) {
+ this.setTextValue(this.currentSearchString);
+ this.mTransientValue = true;
+ } else {
+ this.setTextValue(this.getResultValueAt(
+ this.popup.selectedIndex));
+ }
+ }
+
+ this.mNeedToFinish = false;
+ this.mNeedToComplete = false;
+
+ this.closePopup();
+
+ this.currentSearchString = "";
+
+ if (errItem)
+ this._fireEvent("errorcommand", errItem);
+ this._fireEvent("textentered", "clicking");
+ ]]></body>
+ </method>
+
+ <!-- when the user hits escape, revert the previously typed value in the textbox -->
+ <method name="undoAutoComplete">
+ <body><![CDATA[
+ var val = this.currentSearchString;
+
+ var ok = this.onTextReverted();
+ if ((ok || ok == undefined) && val)
+ this.setTextValue(val);
+
+ this.userAction = "typing";
+
+ this.currentSearchString = this.value;
+ this.mNeedToComplete = false;
+ ]]></body>
+ </method>
+
+ <!-- convert an absolute result index into a session name/index pair -->
+ <method name="convertIndexToSession">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ for (var name in this.mLastRows) {
+ if (aIndex < this.mLastRows[name])
+ return { session: name, index: aIndex };
+ aIndex -= this.mLastRows[name];
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: user input handling ::::::::::::: -->
+
+ <!-- -->
+ <method name="processInput">
+ <body><![CDATA[
+ // stop current lookup in case it's async.
+ this.stopLookup();
+ // stop the queued up lookup on a timer
+ this.clearTimer();
+
+ if (this.disableAutoComplete)
+ return;
+
+ this.userAction = "typing";
+ this.mFinishAfterSearch = false;
+ this.mNeedToFinish = true;
+ this.mTransientValue = false;
+ this.mNeedToComplete = true;
+ var str = this.value;
+ this.currentSearchString = str;
+ this.popup.clearSelection();
+
+ var timeout = this.mIsPasting ? this.pasteTimeout : this.timeout;
+ this.mAutoCompleteTimer = setTimeout(this.callListener, timeout, this, "startLookup");
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ this.mLastKeyCode = aEvent.keyCode;
+
+ var killEvent = false;
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_TAB:
+ if (this.tabScrolling) {
+ // don't kill this event if alt-tab or ctrl-tab is hit
+ if (!aEvent.altKey && !aEvent.ctrlKey) {
+ killEvent = this.mMenuOpen;
+ if (killEvent)
+ this.keyNavigation(aEvent);
+ }
+ }
+ break;
+
+ case KeyEvent.DOM_VK_RETURN:
+
+ // if this is a failure item, save it for fireErrorCommand
+ var errItem = this.getErrorAt(this.popup.selectedIndex);
+
+ killEvent = this.mMenuOpen;
+ this.finishAutoComplete(true, true, aEvent);
+ this.closePopup();
+ if (errItem) {
+ this._fireEvent("errorcommand", errItem);
+ }
+ break;
+
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.clearTimer();
+ killEvent = this.mMenuOpen;
+ this.undoAutoComplete();
+ this.closePopup();
+ break;
+
+ case KeyEvent.DOM_VK_LEFT:
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_HOME:
+ case KeyEvent.DOM_VK_END:
+ this.finishAutoComplete(true, false, aEvent);
+ this.clearTimer();
+ this.closePopup();
+ break;
+
+ case KeyEvent.DOM_VK_DOWN:
+ if (!aEvent.altKey) {
+ this.clearTimer();
+ killEvent = this.keyNavigation(aEvent);
+ break;
+ }
+ // Alt+Down falls through to history popup toggling code
+
+ case KeyEvent.DOM_VK_F4:
+ if (!aEvent.ctrlKey && !aEvent.shiftKey && this.getAttribute("enablehistory") == "true") {
+ var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
+ if (historyPopup)
+ historyPopup.showPopup();
+ else
+ historyPopup.hidePopup();
+ }
+ break;
+ case KeyEvent.DOM_VK_PAGE_UP:
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ case KeyEvent.DOM_VK_UP:
+ if (!aEvent.ctrlKey && !aEvent.metaKey) {
+ this.clearTimer();
+ killEvent = this.keyNavigation(aEvent);
+ }
+ break;
+
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (!aEvent.ctrlKey && !aEvent.altKey && !aEvent.shiftKey &&
+ this.selectionStart == this.currentSearchString.length &&
+ this.selectionEnd == this.value.length &&
+ this.mDefaultMatchFilled) {
+ this.mDefaultMatchFilled = false;
+ this.value = this.currentSearchString;
+ }
+
+ if (!/Mac/.test(navigator.platform))
+ break;
+ case KeyEvent.DOM_VK_DELETE:
+ if (/Mac/.test(navigator.platform) && !aEvent.shiftKey)
+ break;
+
+ if (this.mMenuOpen && this.popup.selectedIndex != -1) {
+ var obj = this.convertIndexToSession(this.popup.selectedIndex);
+ if (obj) {
+ var result = this.mLastResults[obj.session];
+ if (!result.errorDescription) {
+ var count = result.matchCount;
+ result.removeValueAt(obj.index, true);
+ this.view.updateResults(this.popup.selectedIndex, result.matchCount - count);
+ killEvent = true;
+ }
+ }
+ }
+ break;
+ }
+
+ if (killEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+
+ return true;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processStartComposition">
+ <body><![CDATA[
+ this.finishAutoComplete(false, false, null);
+ this.clearTimer();
+ this.closePopup();
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="keyNavigation">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var k = aEvent.keyCode;
+ if (k == KeyEvent.DOM_VK_TAB ||
+ k == KeyEvent.DOM_VK_UP || k == KeyEvent.DOM_VK_DOWN ||
+ k == KeyEvent.DOM_VK_PAGE_UP || k == KeyEvent.DOM_VK_PAGE_DOWN)
+ {
+ if (!this.mMenuOpen) {
+ // Original xpfe style was to allow the up and down keys to have
+ // their default Mac action if the popup could not be opened.
+ // For compatibility for toolkit we now have to predict which
+ // keys have a default action that we can always allow to fire.
+ if (/Mac/.test(navigator.platform) &&
+ ((k == KeyEvent.DOM_VK_UP &&
+ (this.selectionStart != 0 ||
+ this.selectionEnd != 0)) ||
+ (k == KeyEvent.DOM_VK_DOWN &&
+ (this.selectionStart != this.value.length ||
+ this.selectionEnd != this.value.length))))
+ return false;
+ if (this.currentSearchString != this.value) {
+ this.processInput();
+ return true;
+ }
+ if (this.view.rowCount < this.minResultsForPopup)
+ return true; // used to be false, see above
+
+ this.mNeedToFinish = true;
+ this.openPopup();
+ return true;
+ }
+
+ this.userAction = "scrolling";
+ this.mNeedToComplete = false;
+
+ var reverse = k == KeyEvent.DOM_VK_TAB && aEvent.shiftKey ||
+ k == KeyEvent.DOM_VK_UP ||
+ k == KeyEvent.DOM_VK_PAGE_UP;
+ var page = k == KeyEvent.DOM_VK_PAGE_UP ||
+ k == KeyEvent.DOM_VK_PAGE_DOWN;
+ var selected = this.popup.selectBy(reverse, page);
+
+ // determine which value to place in the textbox
+ this.ignoreInputEvent = true;
+ if (selected != -1) {
+ if (this.getErrorAt(selected)) {
+ if (this.currentSearchString)
+ this.setTextValue(this.currentSearchString);
+ } else {
+ this.setTextValue(this.getResultValueAt(selected));
+ }
+ this.mTransientValue = true;
+ } else {
+ if (this.currentSearchString)
+ this.setTextValue(this.currentSearchString);
+ this.mTransientValue = false;
+ }
+
+ // move cursor to the end
+ this.mInputElt.setSelectionRange(this.value.length, this.value.length);
+ this.ignoreInputEvent = false;
+ }
+ return true;
+ ]]></body>
+ </method>
+
+ <!-- while the user is typing, fill the textbox with the "default" value
+ if one can be assumed, and select the end of the text -->
+ <method name="autoFillInput">
+ <parameter name="aSessionName"/>
+ <parameter name="aResults"/>
+ <parameter name="aUseFirstMatchIfNoDefault"/>
+ <body><![CDATA[
+ if (this.mInputElt.selectionEnd < this.currentSearchString.length ||
+ this.mDefaultMatchFilled)
+ return;
+
+ if (!this.mFinishAfterSearch &&
+ (this.autoFill || this.completeDefaultIndex) &&
+ this.mLastKeyCode != KeyEvent.DOM_VK_BACK_SPACE &&
+ this.mLastKeyCode != KeyEvent.DOM_VK_DELETE) {
+ var indexToUse = aResults.defaultIndex;
+ if (aUseFirstMatchIfNoDefault && indexToUse == -1)
+ indexToUse = 0;
+
+ if (indexToUse != -1) {
+ var resultValue = this.getSessionValueAt(aSessionName, indexToUse);
+ var match = resultValue.toLowerCase();
+ var entry = this.currentSearchString.toLowerCase();
+ this.ignoreInputEvent = true;
+ if (match.indexOf(entry) == 0) {
+ var endPoint = this.value.length;
+ this.setTextValue(this.value + resultValue.substr(endPoint));
+ this.mInputElt.setSelectionRange(endPoint, this.value.length);
+ } else {
+ if (this.completeDefaultIndex) {
+ this.setTextValue(this.value + " >> " + resultValue);
+ this.mInputElt.setSelectionRange(entry.length, this.value.length);
+ } else {
+ var postIndex = resultValue.indexOf(this.value);
+ if (postIndex >= 0) {
+ var startPt = this.value.length;
+ this.setTextValue(this.value +
+ resultValue.substr(startPt+postIndex));
+ this.mInputElt.setSelectionRange(startPt, this.value.length);
+ }
+ }
+ }
+ this.mNeedToComplete = true;
+ this.ignoreInputEvent = false;
+ this.mDefaultMatchFilled = true;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: popup and tree ::::::::::::: -->
+
+ <!-- -->
+ <method name="openPopup">
+ <body><![CDATA[
+ if (!this.mMenuOpen && this.focused &&
+ (this.getResultCount() >= this.minResultsForPopup ||
+ this.mFailureItems)) {
+ var w = this.boxObject.width;
+ if (w != this.popup.boxObject.width)
+ this.popup.setAttribute("width", w);
+ this.popup.showPopup(this, -1, -1, "popup", "bottomleft", "topleft");
+ this.mMenuOpen = true;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="closePopup">
+ <body><![CDATA[
+ if (this.popup && this.mMenuOpen) {
+ this.popup.hidePopup();
+ this.mMenuOpen = false;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="addResultElements">
+ <parameter name="aSession"/>
+ <parameter name="aResults"/>
+ <body><![CDATA[
+ var count = aResults.errorDescription ? 1 : aResults.matchCount;
+ if (this.focused && this.showPopup) {
+ var row = 0;
+ for (var name in this.mSessions) {
+ row += this.mLastRows[name];
+ if (name == aSession)
+ break;
+ }
+ this.view.updateResults(row, count - this.mLastRows[name]);
+ this.popup.adjustHeight();
+ }
+ this.mLastResults[aSession] = aResults;
+ this.mLastRows[aSession] = count;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearResultElements">
+ <parameter name="aInvalidate"/>
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mLastRows[name] = 0;
+ this.view.clearResults();
+ if (aInvalidate)
+ this.popup.adjustHeight();
+
+ this.noMatch = true;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="setTextValue">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.value = aValue;
+
+ // Completing a result should simulate the user typing the result,
+ // so fire an input event.
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("input", true, false, window, 0);
+ var oldIgnoreInput = this.ignoreInputEvent;
+ this.ignoreInputEvent = true;
+ this.dispatchEvent(evt);
+ this.ignoreInputEvent = oldIgnoreInput;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearResultData">
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mLastResults[name] = null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: miscellaneous ::::::::::::: -->
+
+ <!-- -->
+ <method name="ifSetAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ if (!this.hasAttribute(aAttr))
+ this.setAttribute(aAttr, aVal);
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearTimer">
+ <body><![CDATA[
+ if (this.mAutoCompleteTimer) {
+ clearTimeout(this.mAutoCompleteTimer);
+ this.mAutoCompleteTimer = 0;
+ }
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: event dispatching ::::::::::::: -->
+
+ <method name="_fireEvent">
+ <parameter name="aEventType"/>
+ <parameter name="aEventParam"/>
+ <parameter name="aTriggeringEvent"/>
+ <body>
+ <![CDATA[
+ var noCancel = true;
+ // handle any xml attribute event handlers
+ var handler = this.getAttribute("on"+aEventType);
+ if (handler) {
+ var fn = new Function("eventParam", "domEvent", handler);
+ var returned = fn.apply(this, [aEventParam, aTriggeringEvent]);
+ if (returned == false)
+ noCancel = false;
+ }
+
+ return noCancel;
+ ]]>
+ </body>
+ </method>
+
+ <!-- =================== TREE VIEW =================== -->
+
+ <field name="view"><![CDATA[
+ ({
+ mTextbox: this,
+ mTree: null,
+ mSelection: null,
+ mRowCount: 0,
+
+ clearResults: function()
+ {
+ var oldCount = this.mRowCount;
+ this.mRowCount = 0;
+
+ if (this.mTree) {
+ this.mTree.rowCountChanged(0, -oldCount);
+ this.mTree.scrollToRow(0);
+ }
+ },
+
+ updateResults: function(aRow, aCount)
+ {
+ this.mRowCount += aCount;
+
+ if (this.mTree)
+ this.mTree.rowCountChanged(aRow, aCount);
+ },
+
+ //////////////////////////////////////////////////////////
+ // nsIAutoCompleteController interface
+
+ // this is the only method required by the treebody mouseup handler
+ handleEnter: function(aIsPopupSelection) {
+ this.mTextbox.onResultClick();
+ },
+
+ //////////////////////////////////////////////////////////
+ // nsITreeView interface
+
+ get rowCount() {
+ return this.mRowCount;
+ },
+
+ get selection() {
+ return this.mSelection;
+ },
+
+ set selection(aVal) {
+ return this.mSelection = aVal;
+ },
+
+ setTree: function(aTree)
+ {
+ this.mTree = aTree;
+ },
+
+ getCellText: function(aRow, aCol)
+ {
+ for (var name in this.mTextbox.mSessions) {
+ if (aRow < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ switch (aCol.id) {
+ case "treecolAutoCompleteValue":
+ return result.errorDescription || result.getLabelAt(aRow);
+ case "treecolAutoCompleteComment":
+ if (!result.errorDescription)
+ return result.getCommentAt(aRow);
+ default:
+ return "";
+ }
+ }
+ aRow -= this.mTextbox.mLastRows[name];
+ }
+ return "";
+ },
+
+ getRowProperties: function(aIndex)
+ {
+ return "";
+ },
+
+ getCellProperties: function(aIndex, aCol)
+ {
+ // for the value column, append nsIAutoCompleteItem::className
+ // to the property list so that we can style this column
+ // using that property
+ if (aCol.id == "treecolAutoCompleteValue") {
+ for (var name in this.mTextbox.mSessions) {
+ if (aIndex < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ if (result.errorDescription)
+ return "";
+ return result.getStyleAt(aIndex);
+ }
+ aIndex -= this.mTextbox.mLastRows[name];
+ }
+ }
+ return "";
+ },
+
+ getColumnProperties: function(aCol)
+ {
+ return "";
+ },
+
+ getImageSrc: function(aRow, aCol)
+ {
+ if (aCol.id == "treecolAutoCompleteValue") {
+ for (var name in this.mTextbox.mSessions) {
+ if (aRow < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ if (result.errorDescription)
+ return "";
+ return result.getImageAt(aRow);
+ }
+ aRow -= this.mTextbox.mLastRows[name];
+ }
+ }
+ return "";
+ },
+
+ getParentIndex: function(aRowIndex) { },
+ hasNextSibling: function(aRowIndex, aAfterIndex) { },
+ getLevel: function(aIndex) {},
+ getProgressMode: function(aRow, aCol) {},
+ getCellValue: function(aRow, aCol) {},
+ isContainer: function(aIndex) {},
+ isContainerOpen: function(aIndex) {},
+ isContainerEmpty: function(aIndex) {},
+ isSeparator: function(aIndex) {},
+ isSorted: function() {},
+ toggleOpenState: function(aIndex) {},
+ selectionChanged: function() {},
+ cycleHeader: function(aCol) {},
+ cycleCell: function(aRow, aCol) {},
+ isEditable: function(aRow, aCol) {},
+ isSelectable: function(aRow, aCol) {},
+ setCellValue: function(aRow, aCol, aValue) {},
+ setCellText: function(aRow, aCol, aValue) {},
+ });
+ ]]></field>
+
+ </implementation>
+
+ <handlers>
+ <handler event="input"
+ action="if (!this.ignoreInputEvent) this.processInput();"/>
+
+ <handler event="keypress" phase="capturing"
+ action="return this.processKeyPress(event);"/>
+
+ <handler event="compositionstart" phase="capturing"
+ action="this.processStartComposition();"/>
+
+ <handler event="focus" phase="capturing"
+ action="this.userAction = 'typing';"/>
+
+ <handler event="blur" phase="capturing"
+ action="if ( !(this.ignoreBlurWhileSearching &amp;&amp; this.isSearching) ) {this.userAction = 'none'; this.finishAutoComplete(false, false, event);}"/>
+
+ <handler event="mousedown" phase="capturing"
+ action="if ( !this.mMenuOpen ) this.finishAutoComplete(false, false, event);"/>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top">
+ <xul:tree anonid="tree" class="autocomplete-tree plain" flex="1">
+ <xul:treecols anonid="treecols">
+ <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteValue" flex="2"/>
+ <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteComment" flex="1" hidden="true"/>
+ </xul:treecols>
+ <xul:treechildren anonid="treebody" class="autocomplete-treebody"/>
+ </xul:tree>
+ </content>
+
+ <implementation implements="nsIAutoCompletePopup">
+ <constructor><![CDATA[
+ if (this.textbox && this.textbox.view)
+ this.initialize();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ if (this.view)
+ this.tree.view = null;
+ ]]></destructor>
+
+ <field name="textbox">
+ document.getBindingParent(this);
+ </field>
+
+ <field name="tree">
+ document.getAnonymousElementByAttribute(this, "anonid", "tree");
+ </field>
+
+ <field name="treecols">
+ document.getAnonymousElementByAttribute(this, "anonid", "treecols");
+ </field>
+
+ <field name="treebody">
+ document.getAnonymousElementByAttribute(this, "anonid", "treebody");
+ </field>
+
+ <field name="view">
+ null
+ </field>
+
+ <!-- Setting tree.view doesn't always immediately create a selection,
+ so we ensure the selection by asking the tree for the view. Note:
+ this.view.selection is quicker if we know the selection exists. -->
+ <property name="selection" onget="return this.tree.view.selection;"/>
+
+ <property name="pageCount"
+ onget="return this.tree.treeBoxObject.getPageLength();"/>
+
+ <field name="maxRows">0</field>
+ <field name="mLastRows">0</field>
+
+ <method name="initialize">
+ <body><![CDATA[
+ this.showCommentColumn = this.textbox.showCommentColumn;
+ this.tree.view = this.textbox.view;
+ this.view = this.textbox.view;
+ this.maxRows = this.textbox.maxRows;
+ ]]></body>
+ </method>
+
+ <property name="showCommentColumn"
+ onget="return !this.treecols.lastChild.hidden;"
+ onset="this.treecols.lastChild.hidden = !val; return val;"/>
+
+ <method name="adjustHeight">
+ <body><![CDATA[
+ // detect the desired height of the tree
+ var bx = this.tree.treeBoxObject;
+ var view = this.view;
+ var rows = this.maxRows || 6;
+ if (!view.rowCount || (rows && view.rowCount < rows))
+ rows = view.rowCount;
+
+ var height = rows * bx.rowHeight;
+
+ if (height == 0)
+ this.tree.setAttribute("collapsed", "true");
+ else {
+ if (this.tree.hasAttribute("collapsed"))
+ this.tree.removeAttribute("collapsed");
+ this.tree.setAttribute("height", height);
+ }
+ ]]></body>
+ </method>
+
+ <method name="clearSelection">
+ <body>
+ this.selection.clearSelection();
+ </body>
+ </method>
+
+ <method name="getNextIndex">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <parameter name="aIndex"/>
+ <parameter name="aMaxRow"/>
+ <body><![CDATA[
+ if (aMaxRow < 0)
+ return -1;
+
+ if (aIndex == -1)
+ return aReverse ? aMaxRow : 0;
+ if (aIndex == (aReverse ? 0 : aMaxRow))
+ return -1;
+
+ var amount = aPage ? this.pageCount - 1 : 1;
+ aIndex = aReverse ? aIndex - amount : aIndex + amount;
+ if (aIndex > aMaxRow)
+ return aMaxRow;
+ if (aIndex < 0)
+ return 0;
+ return aIndex;
+ ]]></body>
+ </method>
+
+ <!-- =================== nsIAutoCompletePopup =================== -->
+
+ <field name="input">
+ null
+ </field>
+
+ <!-- This property is meant to be overriden by bindings extending
+ this one. When the user selects an item from the list by
+ hitting enter or clicking, this method can set the value
+ of the textbox to a different value if it wants to. -->
+ <property name="overrideValue" readonly="true" onget="return null;"/>
+
+ <property name="selectedIndex">
+ <getter>
+ if (!this.view || !this.selection.count)
+ return -1;
+ var start = {}, end = {};
+ this.view.selection.getRangeAt(0, start, end);
+ return start.value;
+ </getter>
+ <setter>
+ if (this.view) {
+ this.selection.select(val);
+ if (val >= 0) {
+ this.view.selection.currentIndex = -1;
+ this.tree.treeBoxObject.ensureRowIsVisible(val);
+ }
+ }
+ return val;
+ </setter>
+ </property>
+
+ <property name="popupOpen" onget="return !!this.input;" readonly="true"/>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ if (!this.input) {
+ this.tree.view = aInput.controller;
+ this.view = this.tree.view;
+ this.showCommentColumn = aInput.showCommentColumn;
+ this.maxRows = aInput.maxRows;
+ this.invalidate();
+
+ var viewer = aElement.ownerGlobal
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+ var rect = aElement.getBoundingClientRect();
+ var width = Math.round((rect.right - rect.left) * viewer.fullZoom);
+ this.setAttribute("width", width > 100 ? width : 100);
+ // Adjust the direction (which is not inherited) of the autocomplete
+ // popup list, based on the textbox direction. (Bug 707039)
+ this.style.direction = aElement.ownerGlobal
+ .getComputedStyle(aElement)
+ .direction;
+ this.popupBoxObject.setConsumeRollupEvent(aInput.consumeRollupEvent
+ ? PopupBoxObject.ROLLUP_CONSUME
+ : PopupBoxObject.ROLLUP_NO_CONSUME);
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ if (this.state != "closed")
+ this.input = aInput;
+ }
+ ]]></body>
+ </method>
+
+ <method name="closePopup">
+ <body>
+ this.hidePopup();
+ </body>
+ </method>
+
+ <method name="invalidate">
+ <body>
+ if (this.view)
+ this.adjustHeight();
+ this.tree.treeBoxObject.invalidate();
+ </body>
+ </method>
+
+ <method name="selectBy">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <body><![CDATA[
+ try {
+ return this.selectedIndex = this.getNextIndex(aReverse, aPage, this.selectedIndex, this.view.rowCount - 1);
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ return -1;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing">
+ if (this.textbox)
+ this.textbox.mMenuOpen = true;
+ </handler>
+
+ <handler event="popuphiding">
+ if (this.textbox)
+ this.textbox.mMenuOpen = false;
+ this.clearSelection();
+ this.input = null;
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treebody">
+ <implementation>
+ <field name="popup">document.getBindingParent(this);</field>
+
+ <field name="mLastMoveTime">Date.now()</field>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseout" action="this.popup.selectedIndex = -1;"/>
+
+ <handler event="mouseup"><![CDATA[
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != -1) {
+ this.popup.selectedIndex = rc;
+ this.popup.view.handleEnter(true);
+ }
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != -1 && rc != this.popup.selectedIndex)
+ this.popup.selectedIndex = rc;
+ this.mLastMoveTime = Date.now();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-history-popup"
+ extends="chrome://global/content/bindings/popup.xml#popup-scrollbars">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <implementation>
+ <method name="removeOpenAttribute">
+ <parameter name="parentNode"/>
+ <body><![CDATA[
+ parentNode.removeAttribute("open");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popuphiding"><![CDATA[
+ setTimeout(this.removeOpenAttribute, 0, this.parentNode);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
+
+ <implementation>
+ <method name="showPopup">
+ <body><![CDATA[
+ var textbox = document.getBindingParent(this);
+ var kids = textbox.getElementsByClassName("autocomplete-history-popup");
+ if (kids.item(0) && textbox.getAttribute("open") != "true") { // Open history popup
+ var w = textbox.boxObject.width;
+ if (w != kids[0].boxObject.width)
+ kids[0].width = w;
+ kids[0].showPopup(textbox, -1, -1, "popup", "bottomleft", "topleft");
+ textbox.setAttribute("open", "true");
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown"><![CDATA[
+ this.showPopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/autocomplete/jar.mn b/comm/suite/components/autocomplete/jar.mn
new file mode 100644
index 0000000000..a1d10068f4
--- /dev/null
+++ b/comm/suite/components/autocomplete/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+ content/global/autocomplete.xml (content/autocomplete.xml)
+
+comm.jar:
+ content/communicator/autocomplete.css (content/autocomplete.css)
diff --git a/comm/suite/components/autocomplete/moz.build b/comm/suite/components/autocomplete/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/autocomplete/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/bindings/datetimepicker.xml b/comm/suite/components/bindings/datetimepicker.xml
new file mode 100644
index 0000000000..7475b6c04e
--- /dev/null
+++ b/comm/suite/components/bindings/datetimepicker.xml
@@ -0,0 +1,1316 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+<!ENTITY % datetimepickerDTD SYSTEM
+ "chrome://communicator/locale/datetimepicker.dtd">
+ %datetimepickerDTD;
+]>
+
+<bindings id="timepickerBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="datetimepicker-base"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <content align="center">
+ <xul:hbox class="datetimepicker-input-box" align="center"
+ xbl:inherits="context,disabled,readonly">
+ <xul:moz-input-box class="textbox-input-box datetimepicker-input-subbox"
+ align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-one"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:moz-input-box>
+ <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/>
+ <xul:moz-input-box class="textbox-input-box datetimepicker-input-subbox"
+ align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-two"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:moz-input-box>
+ <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/>
+ <xul:moz-input-box class="textbox-input-box datetimepicker-input-subbox"
+ align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-three"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:moz-input-box>
+ <xul:moz-input-box class="textbox-input-box datetimepicker-input-subbox"
+ align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-ampm"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:moz-input-box>
+ </xul:hbox>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled"
+ onup="this.parentNode._increaseOrDecrease(1);"
+ ondown="this.parentNode._increaseOrDecrease(-1);"/>
+ </content>
+
+ <implementation>
+ <field name="_dateValue">null</field>
+ <field name="_fieldOne">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-one");
+ </field>
+ <field name="_fieldTwo">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-two");
+ </field>
+ <field name="_fieldThree">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-three");
+ </field>
+ <field name="_fieldAMPM">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-ampm");
+ </field>
+ <field name="_separatorFirst">
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
+ </field>
+ <field name="_separatorSecond">
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
+ </field>
+ <field name="_lastFocusedField">null</field>
+ <field name="_hasEntry">true</field>
+ <field name="_valueEntered">false</field>
+ <field name="attachedControl">null</field>
+
+ <property name="_currentField" readonly="true">
+ <getter>
+ var focusedInput = document.activeElement;
+ if (focusedInput == this._fieldOne ||
+ focusedInput == this._fieldTwo ||
+ focusedInput == this._fieldThree ||
+ focusedInput == this._fieldAMPM)
+ return focusedInput;
+ return this._lastFocusedField || this._fieldOne;
+ </getter>
+ </property>
+
+ <property name="dateValue" onget="return new Date(this._dateValue);">
+ <setter>
+ <![CDATA[
+ if (!(val instanceof Date))
+ throw "Invalid Date";
+
+ this._setValueNoSync(val);
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="readOnly" onset="if (val) this.setAttribute('readonly', 'true');
+ else this.removeAttribute('readonly'); return val;"
+ onget="return this.getAttribute('readonly') == 'true';"/>
+
+ <method name="_fireEvent">
+ <parameter name="aEventName"/>
+ <parameter name="aTarget"/>
+ <body>
+ var event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ return !aTarget.dispatchEvent(event);
+ </body>
+ </method>
+
+ <method name="_setValueOnChange">
+ <parameter name="aField"/>
+ <body>
+ <![CDATA[
+ if (!this._hasEntry)
+ return;
+
+ if (aField == this._fieldOne ||
+ aField == this._fieldTwo ||
+ aField == this._fieldThree) {
+ var value = Number(aField.value);
+ if (isNaN(value))
+ value = 0;
+
+ value = this._constrainValue(aField, value, true);
+ this._setFieldValue(aField, value);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_init">
+ <body/>
+ </method>
+
+ <constructor>
+ this._init();
+
+ var cval = this.getAttribute("value");
+ if (cval) {
+ try {
+ this.value = cval;
+ return;
+ } catch (ex) { }
+ }
+ this.dateValue = new Date();
+ </constructor>
+
+ <destructor>
+ if (this.attachedControl) {
+ this.attachedControl.attachedControl = null;
+ this.attachedControl = null;
+ }
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ var target = event.originalTarget;
+ if (target == this._fieldOne ||
+ target == this._fieldTwo ||
+ target == this._fieldThree ||
+ target == this._fieldAMPM)
+ this._lastFocusedField = target;
+ ]]>
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ if (this._hasEntry && event.charCode &&
+ this._currentField != this._fieldAMPM &&
+ !(event.altKey || event.ctrlKey || event.metaKey) &&
+ (event.charCode < 48 || event.charCode > 57))
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="keypress" keycode="VK_UP">
+ if (this._hasEntry)
+ this._increaseOrDecrease(1);
+ </handler>
+ <handler event="keypress" keycode="VK_DOWN">
+ if (this._hasEntry)
+ this._increaseOrDecrease(-1);
+ </handler>
+
+ <handler event="input">
+ this._valueEntered = true;
+ </handler>
+
+ <handler event="change">
+ this._setValueOnChange(event.originalTarget);
+ </handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="timepicker"
+#ifdef MOZ_SUITE
+ extends="chrome://communicator/content/bindings/datetimepicker.xml#datetimepicker-base">
+#else
+ extends="chrome://messenger/content/datetimepicker.xml#datetimepicker-base">
+#endif
+ <implementation>
+ <field name="is24HourClock">false</field>
+ <field name="hourLeadingZero">false</field>
+ <field name="minuteLeadingZero">true</field>
+ <field name="secondLeadingZero">true</field>
+ <field name="amIndicator">"AM"</field>
+ <field name="pmIndicator">"PM"</field>
+
+ <field name="hourField">null</field>
+ <field name="minuteField">null</field>
+ <field name="secondField">null</field>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ var minute = this._dateValue.getMinutes();
+ if (minute < 10)
+ minute = "0" + minute;
+
+ var second = this._dateValue.getSeconds();
+ if (second < 10)
+ second = "0" + second;
+ return this._dateValue.getHours() + ":" + minute + ":" + second;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var items = val.match(/^([0-9]{1,2})\:([0-9]{1,2})\:?([0-9]{1,2})?$/);
+ if (!items)
+ throw "Invalid Time";
+
+ var dt = this.dateValue;
+ dt.setHours(items[1]);
+ dt.setMinutes(items[2]);
+ dt.setSeconds(items[3] ? items[3] : 0);
+ this.dateValue = dt;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="hour" onget="return this._dateValue.getHours();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 23)
+ throw "Invalid Hour";
+ this._setFieldValue(this.hourField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="minute" onget="return this._dateValue.getMinutes();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 59)
+ throw "Invalid Minute";
+ this._setFieldValue(this.minuteField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="second" onget="return this._dateValue.getSeconds();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 59)
+ throw "Invalid Second";
+ this._setFieldValue(this.secondField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="isPM">
+ <getter>
+ <![CDATA[
+ return (this.hour >= 12);
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val) {
+ if (this.hour < 12)
+ this.hour += 12;
+ } else if (this.hour >= 12) {
+ this.hour -= 12;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="hideSeconds">
+ <getter>
+ return (this.getAttribute("hideseconds") == "true");
+ </getter>
+ <setter>
+ if (val)
+ this.setAttribute("hideseconds", "true");
+ else
+ this.removeAttribute("hideseconds");
+ if (this.secondField)
+ this.secondField.parentNode.collapsed = val;
+ this._separatorSecond.collapsed = val;
+ return val;
+ </setter>
+ </property>
+ <property name="increment">
+ <getter>
+ <![CDATA[
+ var increment = this.getAttribute("increment");
+ increment = Number(increment);
+ if (isNaN(increment) || increment <= 0 || increment >= 60)
+ return 1;
+ return increment;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val == "number")
+ this.setAttribute("increment", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI(this.hourField, this.hour);
+ this._updateUI(this.minuteField, this.minute);
+ this._updateUI(this.secondField, this.second);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecrease">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+
+ var field = this._currentField;
+ if (this._valueEntered)
+ this._setValueOnChange(field);
+
+ if (field == this._fieldAMPM) {
+ this.isPM = !this.isPM;
+ this._fireEvent("change", this);
+ } else {
+ var oldval;
+ var change = aDir;
+ if (field == this.hourField) {
+ oldval = this.hour;
+ } else if (field == this.minuteField) {
+ oldval = this.minute;
+ change *= this.increment;
+ } else if (field == this.secondField) {
+ oldval = this.second;
+ }
+
+ var newval = this._constrainValue(field, oldval + change, false);
+
+ if (field == this.hourField)
+ this.hour = newval;
+ else if (field == this.minuteField)
+ this.minute = newval;
+ else if (field == this.secondField)
+ this.second = newval;
+
+ if (oldval != newval)
+ this._fireEvent("change", this);
+ }
+ field.select();
+ ]]>
+ </body>
+ </method>
+ <method name="_setFieldValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (aField == this.hourField)
+ this._dateValue.setHours(aValue);
+ else if (aField == this.minuteField)
+ this._dateValue.setMinutes(aValue);
+ else if (aField == this.secondField)
+ this._dateValue.setSeconds(aValue);
+
+ this.setAttribute("value", this.value);
+ this._updateUI(aField, aValue);
+
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ this._valueEntered = false;
+
+ var prependZero = false;
+ if (aField == this.hourField) {
+ prependZero = this.hourLeadingZero;
+ if (!this.is24HourClock) {
+ if (aValue > 12)
+ aValue -= 12;
+ else if (aValue == 0)
+ aValue = 12;
+ this._fieldAMPM.value = this.isPM ? this.pmIndicator :
+ this.amIndicator;
+ }
+ } else if (aField == this.minuteField) {
+ prependZero = this.minuteLeadingZero;
+ } else if (aField == this.secondField) {
+ prependZero = this.secondLeadingZero;
+ }
+
+ if (prependZero && aValue < 10)
+ aField.value = "0" + aValue;
+ else
+ aField.value = aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_constrainValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aNoWrap"/>
+ <body>
+ <![CDATA[
+ // aNoWrap is true when the user entered a value, so just
+ // constrain within limits. If false, the value is being
+ // incremented or decremented, so wrap around values
+ var max = 60;
+ if (aField == this.hourField) {
+ max = 24;
+ // User input in the hour field should be adjusted as
+ // needed for 12-hour vs. 24-hour time.
+ if (aNoWrap && !this.is24HourClock) {
+ if (aValue && aValue < 12 && this.isPM)
+ aValue += 12;
+ else if (aValue == 12 && !this.isPM)
+ aValue = 0;
+ }
+ }
+ if (aValue < 0)
+ return aNoWrap ? 0 : max + aValue;
+ if (aValue >= max)
+ return aNoWrap ? max - 1 : aValue - max;
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_init">
+ <body>
+ <![CDATA[
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ this.hourField = this._fieldOne;
+ this.minuteField = this._fieldTwo;
+ this.secondField = this._fieldThree;
+
+ var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/;
+
+ // XXX TODO: The following hack should be fixed once Intl.Locale arrives in bug 1433303.
+ var locale = Services.locale.regionalPrefsLocales[0];
+ if (locale.includes("-u-"))
+ locale += "-ca-gregory-nu-latn";
+ else
+ locale += "-u-ca-gregory-nu-latn";
+ var dtf = new Services.intl.DateTimeFormat(locale, { timeStyle: "long" });
+
+ var pmTime = dtf.format(new Date(2000, 0, 1, 16, 7, 9));
+ var numberFields = pmTime.match(numberOrder);
+ if (numberFields) {
+ this._separatorFirst.value = numberFields[3];
+ this._separatorSecond.value = numberFields[5];
+ if (Number(numberFields[2]) > 12)
+ this.is24HourClock = true;
+ else
+ this.pmIndicator = numberFields[1] || numberFields[7];
+ }
+
+ var amTime = dtf.format(new Date(2000, 0, 1, 1, 7, 9));
+ numberFields = amTime.match(numberOrder);
+ if (numberFields) {
+ this.hourLeadingZero = (numberFields[2].length > 1);
+ this.minuteLeadingZero = (numberFields[4].length > 1);
+ this.secondLeadingZero = (numberFields[6].length > 1);
+
+ if (!this.is24HourClock) {
+ this.amIndicator = numberFields[1] || numberFields[7];
+ if (numberFields[1]) {
+ var mfield = this._fieldAMPM.parentNode;
+ var mcontainer = mfield.parentNode;
+ mcontainer.insertBefore(mfield, mcontainer.firstChild);
+ }
+ var size = (numberFields[1] || numberFields[7]).length;
+ if (this.pmIndicator.length > size)
+ size = this.pmIndicator.length;
+ this._fieldAMPM.size = size;
+ this._fieldAMPM.maxLength = size;
+ } else {
+ this._fieldAMPM.parentNode.collapsed = true;
+ }
+ }
+
+ this.hideSeconds = this.hideSeconds;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress">
+ <![CDATA[
+ // just allow any printable character to switch the AM/PM state
+ if (event.charCode && !this.disabled && !this.readOnly &&
+ this._currentField == this._fieldAMPM) {
+ this.isPM = !this.isPM;
+ this._fieldAMPM.select();
+ this._fireEvent("change", this);
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="datepicker"
+ extends="chrome://communicator/content/bindings/datetimepicker.xml#datetimepicker-base">
+ <implementation>
+ <field name="yearLeadingZero">false</field>
+ <field name="monthLeadingZero">true</field>
+ <field name="dateLeadingZero">true</field>
+
+ <field name="yearField"/>
+ <field name="monthField"/>
+ <field name="dateField"/>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ var month = this._dateValue.getMonth();
+ month = (month < 9) ? month = "0" + ++month : month + 1;
+
+ var date = this._dateValue.getDate();
+ if (date < 10)
+ date = "0" + date;
+ return this._dateValue.getFullYear() + "-" + month + "-" + date;
+ ]]>
+
+ </getter>
+ <setter>
+ <![CDATA[
+ var results = val.match(/^([0-9]{1,4})\-([0-9]{1,2})\-([0-9]{1,2})$/);
+ if (!results)
+ throw "Invalid Date";
+
+ this.dateValue = new Date(results[1] + "/" + results[2] + "/" + results[3]);
+ this.setAttribute("value", this.value);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="year" onget="return this._dateValue.getFullYear();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 1 || valnum > 9999)
+ throw "Invalid Year";
+ this._setFieldValue(this.yearField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="month" onget="return this._dateValue.getMonth();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 11)
+ throw "Invalid Month";
+ this._setFieldValue(this.monthField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="date" onget="return this._dateValue.getDate();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 1 || valnum > 31)
+ throw "Invalid Date";
+ this._setFieldValue(this.dateField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="open" onget="return false;" onset="return val;"/>
+
+ <property name="displayedMonth" onget="return this.month;"
+ onset="this.month = val; return val;"/>
+ <property name="displayedYear" onget="return this.year;"
+ onset="this.year = val; return val;"/>
+
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI(this.yearField, this.year);
+ this._updateUI(this.monthField, this.month);
+ this._updateUI(this.dateField, this.date);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecrease">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+
+ var field = this._currentField;
+ if (this._valueEntered)
+ this._setValueOnChange(field);
+
+ var oldval;
+ if (field == this.yearField)
+ oldval = this.year;
+ else if (field == this.monthField)
+ oldval = this.month;
+ else if (field == this.dateField)
+ oldval = this.date;
+
+ var newval = this._constrainValue(field, oldval + aDir, false);
+
+ if (field == this.yearField)
+ this.year = newval;
+ else if (field == this.monthField)
+ this.month = newval;
+ else if (field == this.dateField)
+ this.date = newval;
+
+ if (oldval != newval)
+ this._fireEvent("change", this);
+ field.select();
+ ]]>
+ </body>
+ </method>
+ <method name="_setFieldValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (aField == this.yearField) {
+ let oldDate = this.date;
+ this._dateValue.setFullYear(aValue);
+ if (oldDate != this.date) {
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ }
+ } else if (aField == this.monthField) {
+ let oldDate = this.date;
+ this._dateValue.setMonth(aValue);
+ if (oldDate != this.date) {
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ }
+ } else if (aField == this.dateField) {
+ this._dateValue.setDate(aValue);
+ }
+
+ this.setAttribute("value", this.value);
+ this._updateUI(aField, aValue);
+
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ this._valueEntered = false;
+
+ var prependZero = false;
+ if (aField == this.yearField) {
+ if (this.yearLeadingZero) {
+ aField.value = ("000" + aValue).slice(-4);
+ return;
+ }
+ } else if (aField == this.monthField) {
+ aValue++;
+ prependZero = this.monthLeadingZero;
+ } else if (aField == this.dateField) {
+ prependZero = this.dateLeadingZero;
+ }
+ if (prependZero && aValue < 10)
+ aField.value = "0" + aValue;
+ else
+ aField.value = aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_constrainValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aNoWrap"/>
+ <body>
+ <![CDATA[
+ // the month will be 1 to 12 if entered by the user, so subtract 1
+ if (aNoWrap && aField == this.monthField)
+ aValue--;
+
+ if (aField == this.dateField) {
+ if (aValue < 1)
+ return new Date(this.year, this.month + 1, 0).getDate();
+
+ var currentMonth = this.month;
+ var dt = new Date(this.year, currentMonth, aValue);
+ return (dt.getMonth() != currentMonth ? 1 : aValue);
+ }
+ var min = (aField == this.monthField) ? 0 : 1;
+ var max = (aField == this.monthField) ? 11 : 9999;
+ if (aValue < min)
+ return aNoWrap ? min : max;
+ if (aValue > max)
+ return aNoWrap ? max : min;
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_init">
+ <body>
+ <![CDATA[
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ // We'll default to YYYY/MM/DD to start.
+ var yfield = "input-one";
+ var mfield = "input-two";
+ var dfield = "input-three";
+ var twoDigitYear = false;
+ this.yearLeadingZero = true;
+ this.monthLeadingZero = true;
+ this.dateLeadingZero = true;
+
+ var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/;
+
+ // XXX TODO: The following hack should be fixed once Intl.Locale arrives in bug 1433303.
+ var locale = Services.locale.regionalPrefsLocales[0];
+ if (locale.includes("-u-"))
+ locale += "-ca-gregory-nu-latn";
+ else
+ locale += "-u-ca-gregory-nu-latn";
+ var dtf = new Services.intl.DateTimeFormat(locale, { dateStyle: "short" });
+
+ var dt = dtf.format(new Date(2002, 9, 4));
+ var numberFields = dt.match(numberOrder);
+ if (numberFields) {
+ this._separatorFirst.value = numberFields[3];
+ this._separatorSecond.value = numberFields[5];
+
+ var yi = 2, mi = 4, di = 6;
+
+ function fieldForNumber(i) {
+ if (i == 2)
+ return "input-one";
+ if (i == 4)
+ return "input-two";
+ return "input-three";
+ }
+
+ for (var i = 1; i < numberFields.length; i++) {
+ switch (Number(numberFields[i])) {
+ case 2:
+ twoDigitYear = true; // fall through
+ case 2002:
+ yi = i;
+ yfield = fieldForNumber(i);
+ break;
+ case 9:
+ case 10:
+ mi = i;
+ mfield = fieldForNumber(i);
+ break;
+ case 4:
+ di = i;
+ dfield = fieldForNumber(i);
+ break;
+ }
+ }
+
+ this.yearLeadingZero = (numberFields[yi].length > 1);
+ this.monthLeadingZero = (numberFields[mi].length > 1);
+ this.dateLeadingZero = (numberFields[di].length > 1);
+ }
+
+ this.yearField = document.getAnonymousElementByAttribute(this, "anonid", yfield);
+ if (!twoDigitYear)
+ this.yearField.parentNode.classList.add("datetimepicker-input-subbox", "datetimepicker-year");
+ this.monthField = document.getAnonymousElementByAttribute(this, "anonid", mfield);
+ this.dateField = document.getAnonymousElementByAttribute(this, "anonid", dfield);
+
+ this._fieldAMPM.parentNode.collapsed = true;
+ this.yearField.size = twoDigitYear ? 2 : 4;
+ this.yearField.maxLength = twoDigitYear ? 2 : 4;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ </binding>
+
+ <binding id="datepicker-grid"
+ extends="chrome://communicator/content/bindings/datetimepicker.xml#datepicker">
+ <content>
+ <vbox class="datepicker-mainbox"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox class="datepicker-monthbox" align="center">
+ <button class="datepicker-previous datepicker-button" type="repeat"
+ xbl:inherits="disabled"
+ oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(-1);"/>
+ <spacer flex="1"/>
+ <deck anonid="monthlabeldeck">
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ </deck>
+ <label anonid="yearlabel" class="datepicker-gridlabel"/>
+ <spacer flex="1"/>
+ <button class="datepicker-next datepicker-button" type="repeat"
+ xbl:inherits="disabled"
+ oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(1);"/>
+ </hbox>
+ <grid class="datepicker-grid" role="grid">
+ <columns>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ </columns>
+ <rows anonid="datebox">
+ <row anonid="dayofweekbox">
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ </rows>
+ </grid>
+ </vbox>
+ </content>
+
+ <implementation>
+ <field name="_hasEntry">false</field>
+ <field name="_weekStart">&firstdayofweek.default;</field>
+ <field name="_displayedDate">null</field>
+ <field name="_todayItem">null</field>
+
+ <field name="yearField">
+ document.getAnonymousElementByAttribute(this, "anonid", "yearlabel");
+ </field>
+ <field name="monthField">
+ document.getAnonymousElementByAttribute(this, "anonid", "monthlabeldeck");
+ </field>
+ <field name="dateField">
+ document.getAnonymousElementByAttribute(this, "anonid", "datebox");
+ </field>
+
+ <field name="_selectedItem">null</field>
+
+ <property name="selectedItem" onget="return this._selectedItem">
+ <setter>
+ <![CDATA[
+ if (!val.value)
+ return val;
+ if (val.parentNode.parentNode != this.dateField)
+ return val;
+
+ if (this._selectedItem)
+ this._selectedItem.removeAttribute("selected");
+ this._selectedItem = val;
+ val.setAttribute("selected", "true");
+ this._displayedDate.setDate(val.value);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="displayedMonth">
+ <getter>
+ return this._displayedDate.getMonth();
+ </getter>
+ <setter>
+ this._updateUI(this.monthField, val, true);
+ return val;
+ </setter>
+ </property>
+ <property name="displayedYear">
+ <getter>
+ return this._displayedDate.getFullYear();
+ </getter>
+ <setter>
+ this._updateUI(this.yearField, val, true);
+ return val;
+ </setter>
+ </property>
+
+ <method name="_init">
+ <body>
+ <![CDATA[
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ // XXX TODO: The following hack should be fixed once Intl.Locale arrives in bug 1433303.
+ var locale = Services.locale.regionalPrefsLocales[0];
+ if (locale.includes("-u-"))
+ locale += "-ca-gregory";
+ else
+ locale += "-u-ca-gregory";
+ var dtfMonth = new Services.intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"});
+ var dtfWeekday = new Services.intl.DateTimeFormat(locale, {weekday: "narrow"});
+
+ var monthLabel = this.monthField.firstChild;
+ var tempDate = new Date(Date.UTC(2005, 0, 1));
+ for (var month = 0; month < 12; month++) {
+ tempDate.setUTCMonth(month);
+ monthLabel.setAttribute("value", dtfMonth.format(tempDate));
+ monthLabel = monthLabel.nextSibling;
+ }
+
+ var fdow = Number(this.getAttribute("firstdayofweek"));
+ if (!isNaN(fdow) && fdow >= 0 && fdow <= 6)
+ this._weekStart = fdow;
+
+ var weekbox = document.getAnonymousElementByAttribute(this, "anonid", "dayofweekbox").childNodes;
+ var date = new Date();
+ date.setDate(date.getDate() - (date.getDay() - this._weekStart));
+ for (var i = 0; i < weekbox.length; i++) {
+ weekbox[i].value = dtfWeekday.format(date);
+ date.setDate(date.getDate() + 1);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI();
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aCheckMonth"/>
+ <body>
+ <![CDATA[
+ var date;
+ var currentMonth;
+ if (aCheckMonth) {
+ if (!this._displayedDate)
+ this._displayedDate = this.dateValue;
+
+ var expectedMonth = aValue;
+ if (aField == this.monthField) {
+ this._displayedDate.setMonth(aValue);
+ } else {
+ expectedMonth = this._displayedDate.getMonth();
+ this._displayedDate.setFullYear(aValue);
+ }
+
+ if (expectedMonth != -1 && expectedMonth != 12 &&
+ expectedMonth != this._displayedDate.getMonth()) {
+ // If the month isn't what was expected, then the month overflowed.
+ // Setting the date to 0 will go back to the last day of the right month.
+ this._displayedDate.setDate(0);
+ }
+
+ date = new Date(this._displayedDate);
+ currentMonth = this._displayedDate.getMonth();
+ } else {
+ var samemonth = (this._displayedDate &&
+ this._displayedDate.getMonth() == this.month &&
+ this._displayedDate.getFullYear() == this.year);
+ if (samemonth) {
+ var items = this.dateField.getElementsByAttribute("value", this.date);
+ if (items.length)
+ this.selectedItem = items[0];
+ return;
+ }
+
+ date = this.dateValue;
+ this._displayedDate = new Date(date);
+ currentMonth = this.month;
+ }
+
+ if (this._todayItem) {
+ this._todayItem.removeAttribute("today");
+ this._todayItem = null;
+ }
+
+ if (this._selectedItem) {
+ this._selectedItem.removeAttribute("selected");
+ this._selectedItem = null;
+ }
+
+ // Update the month and year title
+ this.monthField.selectedIndex = currentMonth;
+ this.yearField.setAttribute("value", date.getFullYear());
+
+ date.setDate(1);
+ var firstWeekday = (7 + date.getDay() - this._weekStart) % 7;
+ date.setDate(date.getDate() - firstWeekday);
+
+ var today = new Date();
+ var datebox = this.dateField;
+ for (var k = 1; k < datebox.childNodes.length; k++) {
+ var row = datebox.childNodes[k];
+ for (var i = 0; i < 7; i++) {
+ var item = row.childNodes[i];
+
+ if (currentMonth == date.getMonth()) {
+ item.value = date.getDate();
+
+ // highlight today
+ if (this._isSameDay(today, date)) {
+ this._todayItem = item;
+ item.setAttribute("today", "true");
+ }
+
+ // highlight the selected date
+ if (this._isSameDay(this._dateValue, date)) {
+ this._selectedItem = item;
+ item.setAttribute("selected", "true");
+ }
+ } else {
+ item.value = "";
+ }
+
+ date.setDate(date.getDate() + 1);
+ }
+ }
+
+ this._fireEvent("monthchange", this);
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecreaseDateFromEvent">
+ <parameter name="aEvent"/>
+ <parameter name="aDiff"/>
+ <body>
+ <![CDATA[
+ if (aEvent.originalTarget == this && !this.disabled && !this.readOnly) {
+ var newdate = this.dateValue;
+ newdate.setDate(newdate.getDate() + aDiff);
+ this.dateValue = newdate;
+ this._fireEvent("change", this);
+ }
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecreaseMonth">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (!this.disabled) {
+ var month = this._displayedDate ? this._displayedDate.getMonth() :
+ this.month;
+ this._updateUI(this.monthField, month + aDir, true);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_isSameDay">
+ <parameter name="aDate1"/>
+ <parameter name="aDate2"/>
+ <body>
+ <![CDATA[
+ return (aDate1 && aDate2 &&
+ aDate1.getDate() == aDate2.getDate() &&
+ aDate1.getMonth() == aDate2.getMonth() &&
+ aDate1.getFullYear() == aDate2.getFullYear());
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="click">
+ <![CDATA[
+ if (event.button != 0 || this.disabled || this.readOnly)
+ return;
+
+ var target = event.originalTarget;
+ if (target.classList.contains("datepicker-gridlabel") &&
+ target != this.selectedItem) {
+ this.selectedItem = target;
+ this._dateValue = new Date(this._displayedDate);
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ this._fireEvent("change", this);
+
+ if (this.attachedControl && "open" in this.attachedControl)
+ this.attachedControl.open = false; // close the popup
+ }
+ ]]>
+ </handler>
+ <handler event="MozMousePixelScroll" preventdefault="true"/>
+ <handler event="DOMMouseScroll" preventdefault="true">
+ <![CDATA[
+ this._increaseOrDecreaseMonth(event.detail < 0 ? -1 : 1);
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_LEFT"
+ action="this._increaseOrDecreaseDateFromEvent(event, -1);"/>
+ <handler event="keypress" keycode="VK_RIGHT"
+ action="this._increaseOrDecreaseDateFromEvent(event, 1);"/>
+ <handler event="keypress" keycode="VK_UP"
+ action="this._increaseOrDecreaseDateFromEvent(event, -7);"/>
+ <handler event="keypress" keycode="VK_DOWN"
+ action="this._increaseOrDecreaseDateFromEvent(event, 7);"/>
+ <handler event="keypress" keycode="VK_PAGE_UP" preventdefault="true"
+ action="this._increaseOrDecreaseMonth(-1);"/>
+ <handler event="keypress" keycode="VK_PAGE_DOWN" preventdefault="true"
+ action="this._increaseOrDecreaseMonth(1);"/>
+ </handlers>
+ </binding>
+
+ <binding id="datepicker-popup" display="xul:menu"
+ extends="chrome://communicator/content/bindings/datetimepicker.xml#datepicker">
+ <content align="center">
+ <xul:moz-input-box class="textbox-input-box datetimepicker-input-box"
+ align="center"
+ allowevents="true"
+ xbl:inherits="context,disabled,readonly">
+ <xul:hbox class="datetimepicker-input-subbox" align="baseline">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-one"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="datetimepicker-input-subbox" align="baseline">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-two"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-three"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:hbox class="datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-ampm"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ </xul:moz-input-box>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled" allowevents="true"
+ onup="this.parentNode._increaseOrDecrease(1);"
+ ondown="this.parentNode._increaseOrDecrease(-1);"/>
+ <xul:dropmarker class="datepicker-dropmarker" xbl:inherits="disabled"/>
+ <xul:panel onpopupshown="this.firstChild.focus();" level="top">
+ <xul:datepicker anonid="grid" type="grid" class="datepicker-popupgrid"
+ xbl:inherits="disabled,readonly,firstdayofweek"/>
+ </xul:panel>
+ </content>
+ <implementation>
+ <constructor>
+ var grid = document.getAnonymousElementByAttribute(this, "anonid", "grid");
+ this.attachedControl = grid;
+ grid.attachedControl = this;
+ grid._setValueNoSync(this._dateValue);
+ </constructor>
+ <property name="open" onget="return this.hasAttribute('open');">
+ <setter>
+ <![CDATA[
+ if (this.hasMenu())
+ this.openMenu(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="displayedMonth">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth;
+ </getter>
+ <setter>
+ document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth = val;
+ return val;
+ </setter>
+ </property>
+ <property name="displayedYear">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear;
+ </getter>
+ <setter>
+ document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear = val;
+ return val;
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/bindings/findbar.xml b/comm/suite/components/bindings/findbar.xml
new file mode 100644
index 0000000000..dde9c5ebd9
--- /dev/null
+++ b/comm/suite/components/bindings/findbar.xml
@@ -0,0 +1,162 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+ SeaMonkey Flexible Findbar
+
+ The binding implemented here mostly works like its toolkit ancestor,
+ except that it will not appear during a manually triggered type ahead find
+ if accessibility.typeaheadfind.usefindbar is false, and the automatic
+ typeahead find is controlled by the accessibility.typeaheadfind.autostart
+ preference instead of the accessibility.typeaheadfind preference.
+
+ This allows the in status bar type ahead find to be used in place of the
+ findbar implementation and allows the in status bar type ahead find
+ to only need to cache the accessibility.typeaheadfind preference branch.
+-->
+
+<!DOCTYPE bindings>
+
+<bindings id="findbarBindings"
+ xmlns="http://www.mozilla.org/xbl">
+
+ <binding id="findbar"
+ extends="chrome://global/content/bindings/findbar.xml#findbar">
+ <implementation>
+ <constructor><![CDATA[
+ var prefsvc =
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch);
+
+ prefsvc.removeObserver("accessibility.typeaheadfind",
+ this._observer);
+ prefsvc.addObserver("accessibility.typeaheadfind.autostart",
+ this._suiteObserver);
+ prefsvc.addObserver("accessibility.typeaheadfind.usefindbar",
+ this._suiteObserver);
+
+ this._findAsYouType =
+ prefsvc.getBoolPref("accessibility.typeaheadfind.autostart");
+ this._useFindbar =
+ prefsvc.getBoolPref("accessibility.typeaheadfind.usefindbar");
+ ]]></constructor>
+
+ <field name="_suiteObserver"><![CDATA[({
+ _self: this,
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIObserver) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ observe: function(aSubject, aTopic, aPrefName) {
+ if (aTopic != "nsPref:changed")
+ return;
+
+ var prefsvc =
+ aSubject.QueryInterface(Ci.nsIPrefBranch);
+
+ switch (aPrefName) {
+ case "accessibility.typeaheadfind.autostart":
+ this._self._findAsYouType = prefsvc.getBoolPref(aPrefName);
+ this._self._updateBrowserWithState();
+ break;
+ case "accessibility.typeaheadfind.usefindbar":
+ this._self._useFindbar = prefsvc.getBoolPref(aPrefName);
+ break;
+ }
+ }
+ })]]></field>
+
+ <!-- This is necessary because the destructor isn't called when
+ we are removed from a document that is not destroyed. This
+ needs to be explicitly called in this case -->
+ <method name="destroy">
+ <body><![CDATA[
+ if (this._destroyed)
+ return;
+ this._destroyed = true;
+
+ this.browser = null;
+
+ var prefsvc =
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch);
+ prefsvc.removeObserver("accessibility.typeaheadfind.linksonly",
+ this._observer);
+ prefsvc.removeObserver("accessibility.typeaheadfind.casesensitive",
+ this._observer);
+ prefsvc.removeObserver("findbar.entireword", this._observer);
+ prefsvc.removeObserver("findbar.highlightAll", this._observer);
+ prefsvc.removeObserver("findbar.modalHighlight", this._observer);
+
+ prefsvc.removeObserver("accessibility.typeaheadfind.usefindbar",
+ this._suiteObserver);
+ prefsvc.removeObserver("accessibility.typeaheadfind.autostart",
+ this._suiteObserver);
+
+ // Clear all timers that might still be running.
+ this._cancelTimers();
+ ]]></body>
+ </method>
+
+ <method name="_updateBrowserWithState">
+ <body><![CDATA[
+ window.messageManager.broadcastAsyncMessage("Findbar:UpdateState", {
+ findMode: this._findMode,
+ findAsYouType: this._findAsYouType,
+ });
+ ]]></body>
+ </method>
+
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ switch (aMessage.name) {
+ case "Findbar:Mouseup":
+ if (!this.hidden && this._findMode != this.FIND_NORMAL)
+ this.close();
+ break;
+
+ case "Findbar:Keypress":
+ if (this._useFindbar)
+ return this._onBrowserKeypress(aMessage.data.fakeEvent,
+ aMessage.data.shouldFastFind);
+ break;
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="startFastFind">
+ <parameter name="aMode"/>
+ <body><![CDATA[
+ if (this._findMode == aMode && this._quickFindTimeout) {
+ this._findField.select();
+ this._findField.focus();
+ return;
+ }
+
+ // Clear bar first, so that when openFindBar() calls setCaseSensitivity()
+ // it doesn't get confused by a lingering value
+ this._findField.value = "";
+
+ if (this._quickFindTimeout)
+ clearTimeout(this._quickFindTimeout);
+ this.open(aMode);
+ this._setFindCloseTimeout();
+ this._findField.select();
+ this._findField.focus();
+
+ this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/bindings/general.xml b/comm/suite/components/bindings/general.xml
new file mode 100644
index 0000000000..9df42b3548
--- /dev/null
+++ b/comm/suite/components/bindings/general.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="generalBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="statusbarpanel-iconic" display="xul:button" role="xul:button"
+ extends="chrome://communicator/content/bindings/generalBindings.xml#statusbarpanel">
+ <content>
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ </content>
+ </binding>
+
+ <binding id="statusbarpanel-iconic-text" display="xul:button" role="xul:button"
+ extends="chrome://communicator/content/bindings/generalBindings.xml#statusbarpanel">
+ <content>
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop"/>
+ </content>
+ </binding>
+
+ <binding id="statusbarpanel-backgroundbox" display="xul:button"
+ extends="chrome://communicator/content/bindings/general.xml#statusbarpanel-iconic-text">
+ <content>
+ <xul:hbox class="statusbarpanel-contentbox" xbl:inherits="dir">
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/bindings/generalBindings.xml b/comm/suite/components/bindings/generalBindings.xml
new file mode 100644
index 0000000000..385e1166a2
--- /dev/null
+++ b/comm/suite/components/bindings/generalBindings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="generalBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="menu-vertical"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:hbox flex="1" align="center">
+ <xul:vbox flex="1" align="center">
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ </xul:vbox>
+ <xul:dropmarker anonid="dropmarker" type="menu"
+ class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/bindings/jar.mn b/comm/suite/components/bindings/jar.mn
new file mode 100644
index 0000000000..6e231a38da
--- /dev/null
+++ b/comm/suite/components/bindings/jar.mn
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/customizeToolbar.css (../customizeToolbar.css)
+ content/communicator/customizeToolbar.js (../customizeToolbar.js)
+* content/communicator/customizeToolbar.xhtml (../customizeToolbar.xhtml)
+ content/communicator/bindings/datetimepicker.xml (datetimepicker.xml)
+ content/communicator/bindings/findbar.xml (findbar.xml)
+ content/communicator/bindings/general.xml (general.xml)
+ content/communicator/bindings/generalBindings.xml (generalBindings.xml)
+ content/communicator/bindings/notification.xml (notification.xml)
+ content/communicator/bindings/numberbox.xml (numberbox.xml)
+ content/communicator/bindings/preferences.xml (preferences.xml)
+ content/communicator/bindings/spinbuttons.xml (spinbuttons.xml)
+* content/communicator/bindings/textbox.xml (textbox.xml)
+ content/communicator/bindings/toolbar.xml (toolbar.xml)
+ content/communicator/bindings/toolbar-xpfe.xml (toolbar-xpfe.xml)
+* content/communicator/bindings/prefwindow.xml (prefwindow.xml)
diff --git a/comm/suite/components/bindings/moz.build b/comm/suite/components/bindings/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/bindings/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/bindings/notification.xml b/comm/suite/components/bindings/notification.xml
new file mode 100644
index 0000000000..8965df67cb
--- /dev/null
+++ b/comm/suite/components/bindings/notification.xml
@@ -0,0 +1,2423 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+<!ENTITY % commNotificationDTD SYSTEM "chrome://communicator/locale/notification.dtd">
+ %commNotificationDTD;
+<!ENTITY % globalNotificationDTD SYSTEM "chrome://global/locale/notification.dtd">
+ %globalNotificationDTD;
+]>
+
+<bindings id="browserNotificationBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="browser-notificationbox"
+ extends="chrome://global/content/bindings/notification.xml#notificationbox">
+ <implementation implements="nsIObserver, nsIFormSubmitObserver, nsIWebProgressListener, nsIWebProgressListener2, nsIDOMEventListener">
+ <field name="_stringBundle" readonly="true">
+ <![CDATA[
+ Services.strings.createBundle("chrome://communicator/locale/notification.properties");
+ ]]>
+ </field>
+
+ <field name="_brandStringBundle" readonly="true">
+ <![CDATA[
+ Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ ]]>
+ </field>
+
+ <field name="_placesBundle" readonly="true">
+ <![CDATA[
+ Services.strings.createBundle("chrome://communicator/locale/places/places.properties");
+ ]]>
+ </field>
+
+ <field name="wrappedJSObject">this</field>
+
+ <field name="_activeBrowser">null</field>
+
+ <property name="activeBrowser" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._activeBrowser) {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var browsers = this.getElementsByTagNameNS(XUL_NS, "browser");
+ for (var i = 0; this._activeBrowser = browsers.item(i); i++)
+ if (!this._activeBrowser.hidden)
+ break;
+ }
+ return this._activeBrowser;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_cwu">null</field>
+
+ <property name="contentWindowUtils" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._cwu)
+ this._cwu = this.activeBrowser.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return this._cwu;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="usePrivateBrowsing" readonly="true">
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing
+ </field>
+
+ <method name="onDocumentChange">
+ <body>
+ <![CDATA[
+ this.crashNotified = false;
+ if (this.popupCount) {
+ this.popupCount = 0;
+ this.notifyPopupCountChanged();
+ }
+ this.removeTransientNotifications();
+ ]]>
+ </body>
+ </method>
+
+ <method name="addProgressListener">
+ <body>
+ <![CDATA[
+ if (this.activeBrowser && !this._addedProgressListener) {
+ this.activeBrowser.webProgress
+ .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_SECURITY |
+ Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_REFRESH);
+ this._addedProgressListener = true;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="lastMessage">"EnterInsecureMessage"</field>
+ <field name="lastState">0</field>
+
+ <method name="onSecurityChange">
+ <parameter name="aWebProgress"/>
+ <parameter name="aRequest"/>
+ <parameter name="aState"/>
+ <body>
+ <![CDATA[
+ if (aState < 0)
+ aState = this.lastState;
+ const nsIWebProgressListener = Ci.nsIWebProgressListener;
+ var pref = "security.warn_leaving_secure";
+ var message = "EnterInsecureMessage";
+ var priority = this.PRIORITY_WARNING_LOW;
+ var pane = "ssl_pane";
+ var buttons = [];
+ if (aState & nsIWebProgressListener.STATE_IS_SECURE) {
+ pref = "security.warn_entering_secure";
+ message = "EnterSecureMessage";
+ priority = this.PRIORITY_INFO_LOW;
+ } else if (aState & nsIWebProgressListener.STATE_IS_BROKEN) {
+ pref = "security.warn_viewing_mixed";
+ message = "MixedContentMessage";
+ priority = this.PRIORITY_CRITICAL_LOW;
+ }
+
+ if (aState & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT &&
+ Services.prefs.getBoolPref("security.warn_mixed_active_content")) {
+ pref = "security.warn_mixed_active_content";
+ message = "MixedActiveContentMessage";
+ priority = this.PRIORITY_CRITICAL_LOW;
+ } else if (aState & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT &&
+ Services.prefs.getBoolPref("security.warn_mixed_active_content")) {
+ pref = "security.warn_mixed_active_content";
+ message = "BlockedActiveContentMessage";
+ priority = this.PRIORITY_INFO_LOW;
+ this.lastState = aState & ~nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ const nsIWebNavigation = Ci.nsIWebNavigation;
+ buttons = [{
+ label: this._stringBundle.GetStringFromName("SecurityKeepBlocking.label"),
+ accessKey: this._stringBundle.GetStringFromName("SecurityKeepBlocking.accesskey"),
+ callback: this.onSecurityChange.bind(this, null, null, -1)
+ }, {
+ label: this._stringBundle.GetStringFromName("SecurityUnblock.label"),
+ accessKey: this._stringBundle.GetStringFromName("SecurityUnblock.accesskey"),
+ callback: this.reloadPage.bind(this,
+ nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT |
+ nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE)
+ }];
+ } else if (aState & nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT &&
+ Services.prefs.getBoolPref("privacy.warn_tracking_content")) {
+ pref = "privacy.warn_tracking_content";
+ message = "TrackingContentMessage";
+ priority = this.PRIORITY_WARNING_LOW;
+ pane = "security_pane";
+ } else if (aState & nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT &&
+ Services.prefs.getBoolPref("privacy.warn_tracking_content")) {
+ pref = "privacy.warn_tracking_content";
+ message = "BlockedTrackingContentMessage";
+ priority = this.PRIORITY_INFO_LOW;
+ pane = "security_pane";
+ if (!this.usePrivateBrowsing) {
+ this.lastState = aState & ~nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT;
+ buttons = [{
+ label: this._stringBundle.GetStringFromName("SecurityKeepBlocking.label"),
+ accessKey: this._stringBundle.GetStringFromName("SecurityKeepBlocking.accesskey"),
+ callback: this.onSecurityChange.bind(this, null, null, -1)
+ }, {
+ label: this._stringBundle.GetStringFromName("SecurityUnblock.label"),
+ accessKey: this._stringBundle.GetStringFromName("SecurityUnblock.accesskey"),
+ callback: () => {
+ Services.perms.add(this.activeBrowser.currentURI,
+ "trackingprotection",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ this.reloadPage();
+ }
+ }];
+ }
+ } else if (aState & nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT &&
+ Services.prefs.getBoolPref("security.warn_mixed_display_content")) {
+ pref = "security.warn_mixed_display_content";
+ message = "MixedDisplayContentMessage";
+ priority = this.PRIORITY_WARNING_LOW;
+ } else if (aState & nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT &&
+ Services.prefs.getBoolPref("security.warn_mixed_display_content")) {
+ pref = "security.warn_mixed_display_content";
+ message = "BlockedDisplayContentMessage";
+ priority = this.PRIORITY_INFO_LOW;
+ }
+
+ if (this.lastMessage == message)
+ return false;
+
+ var box = this.getNotificationWithValue(this.lastMessage);
+ if (box)
+ box.close();
+
+ this.lastMessage = message;
+
+ if (!Services.prefs.getBoolPref(pref))
+ return true;
+
+ if ("goPreferences" in window) {
+ buttons.push({
+ label: this._stringBundle.GetStringFromName("SecurityPreferences.label"),
+ accessKey: this._stringBundle.GetStringFromName("SecurityPreferences.accesskey"),
+ callback: function() {
+ goPreferences(pane);
+ return true;
+ }
+ });
+ }
+ var text = this._stringBundle.GetStringFromName(message);
+ box = this.appendNotification(text, message, null, priority, buttons);
+ box.persistence = 1;
+ box.timeout = Date.now() + 20000; // 20 seconds
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="onLocationChange">
+ <parameter name="aWebProgress" />
+ <parameter name="aRequest" />
+ <parameter name="aLocation" />
+ <parameter name="aFlags" />
+ <body>
+ <![CDATA[
+ const nsIWebProgressListener = Ci.nsIWebProgressListener;
+ if (aWebProgress.DOMWindow == this.activeBrowser.contentWindow &&
+ !(aFlags & nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT))
+ this.onDocumentChange();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onRefreshAttempted">
+ <parameter name="aWebProgress" />
+ <parameter name="aURI" />
+ <parameter name="aDelay" />
+ <parameter name="aSameURI" />
+ <body>
+ <![CDATA[
+ if (Services.prefs.getBoolPref("accessibility.blockautorefresh")) {
+ let brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ let refreshButtonText =
+ this._stringBundle.GetStringFromName("refreshBlocked.goButton");
+ let refreshButtonAccesskey =
+ this._stringBundle.GetStringFromName("refreshBlocked.goButton.accesskey");
+ let message =
+ this._stringBundle.formatStringFromName(aSameURI ? "refreshBlocked.refreshLabel"
+ : "refreshBlocked.redirectLabel",
+ [brandShortName], 1);
+ let notification = this.getNotificationWithValue("refresh-blocked");
+ if (notification) {
+ notification.label = message;
+ } else {
+ let buttons = [{
+ label: refreshButtonText,
+ accessKey: refreshButtonAccesskey,
+ callback: function (aNotification, aButton) {
+ var refreshURI = aNotification.webProgress
+ .QueryInterface(Ci.nsIRefreshURI);
+ refreshURI.forceRefreshURI(aNotification.uri, null,
+ aNotification.delay, true);
+ }
+ }];
+ notification =
+ this.appendNotification(message, "refresh-blocked", null,
+ this.PRIORITY_INFO_MEDIUM, buttons);
+ }
+ // In the case of a meta refresh, the location has already
+ // changed. But in the case of an HTTP header refresh, the
+ // location changes synchronously after this call returns.
+ // Set the persistence to 1 temporarily to stop this from
+ // immediately clobbering the location bar (bug 516441),
+ // but set the persistence back to 0 as soon as possible.
+ setTimeout(function() { notification.persistence = 0; }, 0);
+ notification.persistence = 1;
+ notification.webProgress = aWebProgress;
+ notification.uri = aURI;
+ notification.delay = aDelay;
+ return false;
+ }
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="notify">
+ <parameter name="aFormElement"/>
+ <parameter name="aWindow"/>
+ <parameter name="aActionURI"/>
+ <parameter name="aCancel"/>
+ <body>
+ <![CDATA[
+ aCancel.value = false;
+ if (!aFormElement || !aWindow || !aActionURI)
+ return;
+
+ var browser = this.activeBrowser;
+
+ // inactive sidebar panel:
+ if (!browser || !browser.docShell || !browser.docShell.securityUI)
+ return;
+
+ // not our window:
+ if (aWindow.top != browser.contentWindow)
+ return;
+
+ // pref disabled:
+ if (!Services.prefs.getBoolPref("security.warn_submit_insecure"))
+ return;
+
+ // HTTPS uninteresting:
+ if (aActionURI.schemeIs("https"))
+ return;
+
+ // javascript doesn't hit the network:
+ if (aActionURI.schemeIs("javascript"))
+ return;
+
+ // PSM handles HTTPS source:
+ var uri;
+ try {
+ uri = aFormElement.nodePrincipal.URI;
+ } catch (e) {}
+ if (!uri)
+ uri = aFormElement.ownerDocument.documentURIObject;
+ if (uri.schemeIs("https"))
+ return;
+
+ var warn = { value: true };
+ var prompt = Services.prompt;
+ aCancel.value = prompt.confirmEx(
+ aWindow,
+ this._stringBundle.GetStringFromName("SecurityTitle"),
+ this._stringBundle.GetStringFromName("PostToInsecureFromInsecureMessage"),
+ prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0 +
+ prompt.BUTTON_TITLE_CANCEL * prompt.BUTTON_POS_1,
+ this._stringBundle.GetStringFromName("PostToInsecureContinue"),
+ null, null,
+ this._stringBundle.GetStringFromName("PostToInsecureFromInsecureShowAgain"),
+ warn) != 0;
+ if (!aCancel.value)
+ Services.prefs.setBoolPref("security.warn_submit_insecure", warn.value);
+ ]]>
+ </body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aSubject" />
+ <parameter name="aTopic" />
+ <parameter name="aData" />
+ <body>
+ <![CDATA[
+ var browser = this.activeBrowser;
+
+ // inactive sidebar panel:
+ if (!browser || !browser.docShell)
+ return;
+
+ switch (aTopic) {
+ case "indexedDB-permissions-prompt":
+ var requestor = aSubject.QueryInterface(Ci.nsIInterfaceRequestor);
+ var contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
+ if (contentWindow.top == browser.contentWindow)
+ this.promptIndexedDB(requestor, contentWindow, "permissions");
+ break;
+
+ case "indexedDB-quota-prompt":
+ var requestor = aSubject.QueryInterface(Ci.nsIInterfaceRequestor);
+ var contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
+ if (contentWindow.top == browser.contentWindow)
+ this.promptIndexedDB(requestor, contentWindow, "quota", aData);
+ break;
+
+ case "indexedDB-quota-cancel":
+ var requestor = aSubject.QueryInterface(Ci.nsIInterfaceRequestor);
+ var contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
+ if (contentWindow.top == browser.contentWindow)
+ this.cancelIndexedDB(requestor, contentWindow, "quota", aData);
+ break;
+
+ case "addon-install-blocked":
+ var installInfo = aSubject.wrappedJSObject;
+ if (installInfo.browser == browser)
+ this.addonInstallBlocked(installInfo);
+ break;
+
+ case "addon-install-complete":
+ var installInfo = aSubject.wrappedJSObject;
+ if (installInfo.browser == browser)
+ this.addonInstallComplete(installInfo);
+ break;
+
+ case "addon-install-disabled":
+ var installInfo = aSubject.wrappedJSObject;
+ if (installInfo.browser == browser)
+ this.addonInstallDisabled(installInfo);
+ break;
+
+ case "addon-install-failed":
+ var installInfo = aSubject.wrappedJSObject;
+ if (installInfo.browser == browser)
+ this.addonInstallFailed(installInfo);
+ break;
+
+ case "addon-install-started":
+ var installInfo = aSubject.wrappedJSObject;
+ if (installInfo.browser == browser)
+ this.addonInstallStarted(installInfo);
+ break;
+
+ case "offline-cache-update-completed":
+ var doc = browser.contentDocument;
+ if (!doc.documentElement)
+ break;
+
+ var manifest = doc.documentElement.getAttribute("manifest");
+ if (!manifest)
+ break;
+
+ try {
+ var manifestURI = Services.io.newURI(manifest,
+ doc.characterSet,
+ doc.documentURIObject);
+ var cacheUpdate =
+ aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate);
+ if (manifestURI.equals(cacheUpdate.manifestURI))
+ this.checkUsage(manifestURI);
+ } catch (e) {
+ alert(e);
+ }
+ break;
+
+ case "nsPref:changed":
+ if (aData == "privacy.popups.showBrowserMessage") {
+ if (Services.prefs.getBoolPref(aData))
+ return;
+
+ var popupNotification = this.getNotificationWithValue("popup-blocked");
+ if (popupNotification)
+ this.removeNotification(popupNotification);
+ }
+
+ if (aData == "dom.disable_open_during_load") {
+ // remove notifications when popup blocking has been turned off
+ if (Services.prefs.getBoolPref(aData) || !this.popupCount)
+ return;
+
+ var popupNotification = this.getNotificationWithValue("popup-blocked");
+ if (popupNotification)
+ this.removeNotification(popupNotification);
+ this.popupCount = 0;
+ this.notifyPopupCountChanged();
+ }
+ break;
+
+ case "perm-changed":
+ // If all permissions are cleared aSubject is null.
+ if (!aSubject)
+ return;
+
+ var permission = aSubject.QueryInterface(Ci.nsIPermission);
+ if (permission.type != "popup" || aData != "added" || !this.popupCount)
+ return;
+
+ if (permission.matchesURI(this.activeBrowser.currentURI, false)) {
+ var popupNotification = this.getNotificationWithValue("popup-blocked");
+ if (popupNotification)
+ this.removeNotification(popupNotification);
+ this.popupCount = 0;
+ this.notifyPopupCountChanged();
+ }
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="CrashSubmit">null</field>
+
+ <field name="crashNotified">false</field>
+
+ <method name="openURLPref">
+ <parameter name="aPref"/>
+ <body>
+ <![CDATA[
+ var url = Services.urlFormatter.formatURLPref(aPref);
+ var nsIBrowserDOMWindow = Ci.nsIBrowserDOMWindow;
+
+ var browserWin;
+ var whereToOpen = Services.prefs.getIntPref("browser.link.open_external");
+
+ if (whereToOpen != nsIBrowserDOMWindow.OPEN_NEWWINDOW) {
+ browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ if (!browserWin) {
+ var browserURL = "chrome://navigator/content/navigator.xul";
+ try {
+ browserURL = Services.prefs.getCharPref("browser.chromeURL");
+ } catch (ex) {}
+
+ window.openDialog(browserURL, "_blank", "chrome,all,dialog=no", url);
+ } else {
+ if (whereToOpen == nsIBrowserDOMWindow.OPEN_CURRENTWINDOW)
+ browserWin.loadURI(url);
+ else {
+ // new tab
+ var browser = browserWin.getBrowser();
+ var newTab = browser.addTab(url);
+ browser.selectedTab = newTab;
+ }
+ browserWin.content.focus();
+ }
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="makeNicePluginName">
+ <parameter name="aName"/>
+ <body>
+ <![CDATA[
+ // Clean up the plugin name by stripping off any trailing version
+ // numbers or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar"
+ // Do this by first stripping the numbers, etc. off the end, and
+ // then removing "Plugin" (and then trimming to get rid of any
+ // whitespace). Otherwise, something like "Java(TM) Plug-in
+ // 1.7.0_07" gets mangled.
+ var newName = aName.replace(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim();
+ return newName;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getPluginUI">
+ <parameter name="aPlugin"/>
+ <parameter name="aAnonId"/>
+ <body>
+ <![CDATA[
+ return aPlugin.ownerDocument.getAnonymousElementByAttribute(aPlugin, "anonid", aAnonId);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addLinkClickCallback">
+ <parameter name="linkNode"/>
+ <parameter name="callback"/>
+ <body>
+ <![CDATA[
+ // XXX just doing (callback)(arg) was giving a same-origin error. bug?
+
+ // We use event bubbling for the event listeners.
+ let callbackArgs = Array.from(arguments).slice(2);
+ linkNode.addEventListener("click",
+ function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+ if (aEvent.button != 0)
+ return;
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ if (callbackArgs.length == 0)
+ callbackArgs = [ aEvent ];
+ callback.apply(this, callbackArgs);
+ }.bind(this));
+
+ linkNode.addEventListener("keydown",
+ function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+ if (aEvent.keyCode != aEvent.DOM_VK_RETURN)
+ return;
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ if (callbackArgs.length == 0)
+ callbackArgs = [ aEvent ];
+ callback.apply(this, callbackArgs);
+ }.bind(this));
+ ]]>
+ </body>
+ </method>
+
+ <!-- Callback for user clicking "submit a report" link -->
+ <method name="submitReport">
+ <parameter name="pluginDumpID"/>
+ <parameter name="plugin"/>
+ <body>
+ <![CDATA[
+ var keyVals = {};
+ if (plugin) {
+ let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
+ if (userComment)
+ keyVals.PluginUserComment = userComment;
+ if (this.getPluginUI(plugin, "submitURLOptIn").checked)
+ keyVals.PluginContentURL = plugin.ownerDocument.URL;
+ }
+ this.CrashSubmit.submit(pluginDumpID, { extraExtraKeyVals: keyVals,
+ recordSubmission: true });
+ ]]>
+ </body>
+ </method>
+
+ <!-- Callback for user clicking a "reload page" link -->
+ <method name="reloadPage">
+ <parameter name="flags"/>
+ <body>
+ <![CDATA[
+ this.activeBrowser.reloadWithFlags(flags);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Callback for user clicking the help icon -->
+ <method name="openHelpPage">
+ <body>
+ <![CDATA[
+ //XXX Ratty need in app help here.
+ openHelp("plugin-crashed", "chrome://communicator/locale/help/suitehelp.rdf");
+ ]]>
+ </body>
+ </method>
+
+ <method name="showPluginCrashedNotification">
+ <parameter name="pluginDumpID"/>
+ <parameter name="messageString"/>
+ <body>
+ <![CDATA[
+ // If there's already an existing notification bar, don't do anything.
+ var notification = this.getNotificationWithValue("plugin-crashed");
+ if (notification)
+ return;
+
+ // Configure the notification bar
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ var reloadLabel = this._stringBundle.GetStringFromName("crashedpluginsMessage.reloadButton.label");
+ var reloadKey = this._stringBundle.GetStringFromName("crashedpluginsMessage.reloadButton.accesskey");
+ var submitLabel = this._stringBundle.GetStringFromName("crashedpluginsMessage.submitButton.label");
+ var submitKey = this._stringBundle.GetStringFromName("crashedpluginsMessage.submitButton.accesskey");
+
+ var buttons = [{
+ label: reloadLabel,
+ accessKey: reloadKey,
+ popup: null,
+ callback: this.reloadPage.bind(this)
+ }];
+
+ if (this.CrashSubmit && pluginDumpID) {
+ let submitButton = {
+ label: submitLabel,
+ accessKey: submitKey,
+ popup: null,
+ callback: this.submitReport.bind(this, pluginDumpID)
+ };
+ buttons.push(submitButton);
+ }
+
+ var notification = this.appendNotification(messageString, "plugin-crashed",
+ null, priority, buttons);
+
+ // Add the "learn more" link.
+ var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var link = notification.ownerDocument.createElementNS(XULNS, "label");
+ link.className = "text-link";
+ link.setAttribute("value", this._stringBundle.GetStringFromName("crashedpluginsMessage.learnMore"));
+ this.addLinkClickCallback(link, this.openHelpPage);
+ var description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
+ description.appendChild(link);
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (!aEvent.isTrusted)
+ return;
+ ]]>
+ </body>
+ </method>
+
+ <method name="playSoundForBlockedPopup">
+ <body>
+ <![CDATA[
+ const kCustomSound = 1;
+ var playSound = Services.prefs.getBoolPref("privacy.popups.sound_enabled");
+
+ if (playSound) {
+ var sound = Cc["@mozilla.org/sound;1"]
+ .createInstance(Ci.nsISound);
+
+ var soundType = Services.prefs.getIntPref("privacy.popups.sound_type");
+ if (soundType == kCustomSound) {
+ var soundUrlSpec = Services.prefs.getCharPref("privacy.popups.sound_url");
+ var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"]
+ .getService(Ci.nsIFileProtocolHandler);
+ var file = fileHandler.getFileFromURLSpec(soundUrlSpec);
+ if (file.exists()) {
+ var soundUrl = fileHandler.newFileURI(file);
+ sound.play(soundUrl);
+ return;
+ }
+ }
+
+ // Either a custom sound is selected which does not exist
+ // or the system beep was selected, so make the system beep.
+ sound.beep();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="popupCount">0</field>
+
+ <method name="notifyPopupCountChanged">
+ <body>
+ <![CDATA[
+ this.dispatchEvent(new Event("PopupCountChanged",
+ { bubbles: true, cancelable: true }));
+ ]]>
+ </body>
+ </method>
+
+ <method name="allowPopupsForSite">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ Services.perms.add(this.activeBrowser.currentURI, "popup",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ this.removeCurrentNotification();
+ ]]>
+ </body>
+ </method>
+
+ <method name="offlineAppRequested">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ var documentURI = aDocument.documentURIObject;
+ var pm = Services.perms;
+ if (pm.testExactPermission(documentURI, "offline-app") !=
+ Ci.nsIPermissionManager.UNKNOWN_ACTION)
+ return;
+
+ var host = documentURI.asciiHost;
+ var notificationName = "offline-app-requested-" + host;
+ var notification = this.getNotificationWithValue(notificationName);
+ if (notification)
+ notification.documents.push(aDocument);
+ else {
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("offlineApps.always"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.always.accesskey"),
+ callback: function() {
+ pm.add(documentURI, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
+ var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
+ .getService(Ci.nsIOfflineCacheUpdateService);
+ notification.documents.forEach(function(aDocument) {
+ if (!aDocument.documentElement)
+ return;
+ var manifest = aDocument.documentElement.getAttribute("manifest");
+ if (!manifest)
+ return;
+
+ try {
+ var manifestURI =
+ Services.io.newURI(manifest,
+ aDocument.characterSet,
+ aDocument.documentURIObject);
+ updateService.scheduleUpdate(manifestURI,
+ aDocument.documentURIObject,
+ window);
+ } catch (e) {
+ }
+ });
+ }
+ }, {
+ label: this._stringBundle.GetStringFromName("offlineApps.never"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.never.accesskey"),
+ callback: function() {
+ pm.add(documentURI, "offline-app", Ci.nsIPermissionManager.DENY_ACTION);
+ }
+ }, {
+ label: this._stringBundle.GetStringFromName("offlineApps.later"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.later.accesskey"),
+ callback: function() { /* no-op */ }
+ }];
+
+ var messageString = this._stringBundle.formatStringFromName(this.usePrivateBrowsing ?
+ "offlineApps.private" : "offlineApps.permissions", [host], 1);
+ var priority = this.PRIORITY_INFO_LOW;
+ notification = this.appendNotification(messageString, notificationName,
+ null, priority,
+ this.usePrivateBrowsing ?
+ null : buttons);
+ notification.documents = [aDocument];
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="checkUsage">
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ if (Services.perms.testExactPermission(aURI, "offline-app") ==
+ Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN)
+ return;
+
+ var host = aURI.asciiHost;
+ var usage = 0;
+
+ var cacheService = Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+ cacheService.getGroups().forEach(function(aGroup) {
+ var uri = Services.io.newURI(aGroup);
+ if (uri.asciiHost == host)
+ usage += cacheService.getActiveCache(aGroup).usage;
+ });
+ var warnQuota = Services.prefs.getIntPref("offline-apps.quota.warn");
+ if (usage < warnQuota * 1024)
+ return;
+
+ var message = this._stringBundle.formatStringFromName("offlineApps.quota", [host, warnQuota / 1024], 2);
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(message, "offline-app-usage", null,
+ priority, null);
+ Services.perms.add(aURI, "offline-app",
+ Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
+ ]]>
+ </body>
+ </method>
+
+ <method name="showRightsNotification">
+ <body>
+ <![CDATA[
+ var rightsBundle = Services.strings.createBundle("chrome://branding/locale/aboutRights.properties");
+ var buttonLabel = rightsBundle.GetStringFromName("buttonLabel");
+ var buttonAccessKey = rightsBundle.GetStringFromName("buttonAccessKey");
+ var productName = this._brandStringBundle.GetStringFromName("brandFullName");
+ var notifyRightsText = rightsBundle.formatStringFromName("notifyRightsText2", [productName], 1);
+
+ var buttons = [{
+ label: buttonLabel,
+ accessKey: buttonAccessKey,
+ popup: null,
+ callback: function (aNotificationBox, aButton) {
+ var browser = document.getBindingParent(aNotificationBox);
+ browser.addTab("about:rights", { focusNewTab: true });
+ }
+ }];
+ var box = this.appendNotification(notifyRightsText, "about-rights",
+ null, this.PRIORITY_INFO_LOW,
+ buttons);
+ box.persistence = 3; // arbitrary number, just so bar sticks around for a bit
+ ]]>
+ </body>
+ </method>
+
+ <method name="showPlacesLockedWarning">
+ <body>
+ <![CDATA[
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var message = this._placesBundle.formatStringFromName("lockPrompt.text", [brandShortName], 1);
+ var buttons = [{
+ label: this._placesBundle.GetStringFromName("lockPromptInfoButton.label"),
+ accessKey: this._placesBundle.GetStringFromName("lockPromptInfoButton.accesskey"),
+ popup: null,
+ callback: function() {
+ openHelp("places-locked", "chrome://communicator/locale/help/suitehelp.rdf");
+ }
+ }];
+ var box = this.appendNotification(message, "places-locked", null,
+ this.PRIORITY_CRITICAL_MEDIUM,
+ buttons);
+ box.persistence = -1; // until user closes it
+ ]]>
+ </body>
+ </method>
+
+ <method name="showUpdateWarning">
+ <body>
+ <![CDATA[
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var message = this._stringBundle.formatStringFromName("updatePrompt.text", [brandShortName], 1);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("updatePromptCheckButton.label"),
+ accessKey: this._stringBundle.GetStringFromName("updatePromptCheckButton.accesskey"),
+ popup: null,
+ callback: function() {
+ Cc["@mozilla.org/updates/update-prompt;1"]
+ .createInstance(Ci.nsIUpdatePrompt)
+ .checkForUpdates();
+ }
+ }];
+ var box = this.appendNotification(message, "update-warning", null,
+ this.PRIORITY_CRITICAL_MEDIUM,
+ buttons);
+ box.persistence = -1; // until user closes it
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeNotifications">
+ <parameter name="aNotifications"/>
+ <body>
+ <![CDATA[
+ aNotifications.forEach(function(value) {
+ var box = this.getNotificationWithValue(value);
+ if (box)
+ this.removeNotification(box);
+ }, this);
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeInstallRequest">
+ <parameter name="aHost"/>
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.formatStringFromName("lwthemeInstallRequest.message", [aHost], 1);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton.accesskey"),
+ popup: null,
+ callback: aCallback
+ }];
+ var box = this.appendNotification(message,
+ "lwtheme-install-request", null,
+ this.PRIORITY_INFO_MEDIUM,
+ buttons);
+ box.persistence = 1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeInstallNotification">
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.GetStringFromName("lwthemeInstallNotification.message");
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton.accesskey"),
+ callback: aCallback
+ }, {
+ label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton.accesskey"),
+ callback: function() {
+ window.toEM("addons://list/theme");
+ }
+ }];
+ var box = this.appendNotification(message,
+ "lwtheme-install-notification",
+ null, this.PRIORITY_INFO_MEDIUM,
+ buttons);
+ box.persistence = 1;
+ box.timeout = Date.now() + 20000; // 20 seconds
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeNeedsRestart">
+ <parameter name="aNewThemeName"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.formatStringFromName("lwthemeNeedsRestart.message", [aNewThemeName], 1);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton.accesskey"),
+ popup: null,
+ callback: function() {
+ BrowserUtils.restartApplication();
+ }
+ }];
+ var box = this.appendNotification(message,
+ "lwtheme-install-notification",
+ null,this.PRIORITY_INFO_MEDIUM,
+ buttons);
+ box.persistence = 1;
+ box.timeout = Date.now() + 20000; // 20 seconds
+ ]]>
+ </body>
+ </method>
+
+ <method name="promptIndexedDB">
+ <parameter name="aRequestor"/>
+ <parameter name="aWindow"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ var host = aWindow.document.documentURIObject.asciiHost;
+ var property = this.usePrivateBrowsing ? "offlineApps.private" :
+ "offlineApps." + aTopic;
+ var message = this._stringBundle.formatStringFromName(property,
+ [host, aData], 2);
+ var observer = aRequestor.getInterface(Ci.nsIObserver);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("offlineApps.always"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.always.accesskey"),
+ popup: null,
+ callback: function allowIndexedDB() {
+ clearTimeout(box.timeout);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ }
+ }, {
+ label: this._stringBundle.GetStringFromName("offlineApps.never"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.never.accesskey"),
+ popup: null,
+ callback: function denyIndexedDB() {
+ clearTimeout(box.timeout);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.DENY_ACTION);
+ }
+ }, {
+ label: this._stringBundle.GetStringFromName("offlineApps.later"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.later.accesskey"),
+ popup: null,
+ callback: function laterIndexedDB() {
+ clearTimeout(box.timeout);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ }
+ }];
+ var box = this.appendNotification(message,
+ "indexedDB-" + aTopic + "-prompt",
+ null, this.PRIORITY_INFO_LOW,
+ this.usePrivateBrowsing ?
+ null : buttons);
+ box.timeout = setTimeout(function() {
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ if (box.parentNode)
+ box.parentNode.removeNotification(box);
+ }, 300000); // 5 minutes
+ ]]>
+ </body>
+ </method>
+
+ <method name="cancelIndexedDB">
+ <parameter name="aRequestor"/>
+ <parameter name="aWindow"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ var popupNotification = this.getNotificationWithValue("indexedDB-" + aTopic + "-prompt");
+ if (popupNotification) {
+ clearTimeout(popupNotification.timeout);
+ this.removeNotification(popupNotification);
+ }
+
+ var observer = aRequestor.getInterface(Ci.nsIObserver);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallBlocked">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var host;
+ try {
+ // this fails with nsSimpleURIs like data: URIs
+ host = installInfo.originatingURI.host;
+ } catch (ex) {
+ host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
+ }
+ var notificationName = "addon-install-blocked";
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var messageString = this._stringBundle.formatStringFromName("xpinstallPromptWarning",
+ [brandShortName, host], 2);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton"),
+ accessKey: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton.accesskey"),
+ popup: null,
+ callback: function allowInstall() {
+ installInfo.install();
+ return false;
+ }
+ }];
+
+ if (!this.getNotificationWithValue(notificationName)) {
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(messageString, notificationName,
+ null, priority, buttons);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallCancelled">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+ var notificationName = "addon-install-cancelled";
+ var messageString = this._stringBundle.GetStringFromName("addonDownloadCancelled");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("addonDownloadRestartButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonDownloadRestartButton.accesskey"),
+ popup: null,
+ callback: function() {
+ var weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]
+ .getService(Ci.amIWebInstallListener);
+ if (weblistener.onWebInstallRequested(installInfo.browser, installInfo.originatingURI,
+ [aInstall], 1)) {
+ aInstall.install();
+ }
+ }
+ }];
+ var priority = this.PRIORITY_INFO_MEDIUM;
+ this.appendNotification(messageString, notificationName,
+ null, priority, buttons);
+
+ installInfo.installs.every(function(aInstall) {
+ aInstall.cancel();
+ });
+ return true; // the downloading notification closed automatically
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallComplete">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+
+ var notificationName = "addon-install-complete"
+ var addonNotification = this.getNotificationWithValue(notificationName);
+ if (addonNotification)
+ this.removeNotification(addonNotification);
+
+ var buttons = [];
+ var messageString;
+ if (installInfo.installs.some(install =>
+ install.addon.pendingOperations &
+ tmp.AddonManager.PENDING_INSTALL)) {
+ messageString = this._stringBundle.GetStringFromName("addonsInstalledNeedsRestart");
+ buttons.push({
+ label: this._stringBundle.GetStringFromName("addonInstallRestartButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonInstallRestartButton.accesskey"),
+ callback: function () {
+ BrowserUtils.restartApplication();
+ }
+ });
+ } else {
+ messageString = this._stringBundle.GetStringFromName("addonsInstalled");
+ }
+
+ if ("toEM" in window) {
+ buttons.push({
+ label: this._stringBundle.GetStringFromName("addonInstallManageButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonInstallManageButton.accesskey"),
+ callback: function() {
+ window.toEM("addons://list/extension");
+ }
+ });
+ }
+
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString)
+ .replace("#1", installInfo.installs[0].name)
+ .replace("#2", installInfo.installs.length)
+ .replace("#3", brandShortName);
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(messageString, notificationName,
+ null, priority, buttons);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallDisabled">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var messageString;
+ var buttons;
+
+ var notificationName = "addon-install-disabled";
+ if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
+ messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessageLocked");
+ buttons = [];
+ } else {
+ messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessage");
+ buttons = [{
+ label: this._stringBundle.GetStringFromName("xpinstallDisabledButton"),
+ accessKey: this._stringBundle.GetStringFromName("xpinstallDisabledButton.accesskey"),
+ popup: null,
+ callback: function editPrefs() {
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ return false;
+ }
+ }];
+ }
+
+ if (!this.getNotificationWithValue(notificationName)) {
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(messageString, notificationName,
+ null, priority, buttons);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallFailed">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var notificationName = "addon-install-failed";
+ if (!this.getNotificationWithValue(notificationName)) {
+ var host;
+ try {
+ // this fails with nsSimpleURIs like data: URIs
+ host = installInfo.originatingURI.host;
+ } catch (ex) {
+ host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
+ }
+
+ var error = "addonErrorIncompatible";
+ var name = installInfo.installs[0].name;
+ installInfo.installs.some(function(install) {
+ if (install.error) {
+ name = install.name;
+ error = "addonError" + install.error;
+ return true;
+ }
+ if (install.addon.blocklistState ==
+ Ci.nsIBlocklistService.STATE_BLOCKED) {
+ name = install.name;
+ error = "addonErrorBlocklisted";
+ }
+ return false;
+ });
+
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var version = Services.appinfo.version;
+ var messageString = this._stringBundle.GetStringFromName(error)
+ .replace("#1", name)
+ .replace("#2", host)
+ .replace("#3", brandShortName)
+ .replace("#4", version);
+
+ var priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(messageString, notificationName,
+ null, priority, []);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallStarted">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ if (installInfo.installs.every(function(aInstall) {
+ return aInstall.state == tmp.AddonManager.STATE_DOWNLOADED;
+ }))
+ return;
+
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+ var notificationName = "addon-install-started";
+ var messageString = this._stringBundle.GetStringFromName("addonDownloading");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
+ var buttons = [{
+ label: this._stringBundle.GetStringFromName("addonDownloadCancelButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonDownloadCancelButton.accesskey"),
+ popup: null,
+ callback: this.addonInstallCancelled.bind(this, installInfo)
+ }];
+ var priority = this.PRIORITY_INFO_MEDIUM;
+ var box = this.appendNotification(messageString, notificationName,
+ null, priority, buttons);
+ box.installInfo = installInfo;
+ installInfo.installs.forEach(function(aInstall) {
+ aInstall.addListener(box);
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="ignoreSafeBrowsingWarning">
+ <parameter name="aReason"/>
+ <parameter name="aBlockedInfo"/>
+
+ <body>
+ <![CDATA[
+ var uri = this.activeBrowser.currentURI;
+ var flag = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER;
+ this.activeBrowser.loadURIWithFlags(uri.asciiSpec, flag,
+ null, null, null);
+
+ Services.perms.add(uri, "safe-browsing",
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+ var title, label, accessKey, reportName, buttons;
+
+ switch (aReason) {
+ case "phishing":
+ title = "safebrowsing.deceptiveSite";
+ label = "safebrowsing.notADeceptiveSiteButton.label";
+ accessKey = "safebrowsing.notADeceptiveSiteButton.accessKey";
+ reportName = "PhishMistake";
+ break;
+ case "malware":
+ title = "safebrowsing.reportedAttackSite";
+ label = "safebrowsing.notAnAttackButton.label";
+ accessKey = "safebrowsing.notAnAttackButton.accessKey";
+ reportName = "MalwareMistake";
+ break;
+ case "unwanted":
+ title = "safebrowsing.reportedUnwantedSite";
+ break;
+ // No notifications for unknown reasons.
+ default:
+ return;
+ }
+
+ title = this._stringBundle.GetStringFromName(title);
+
+ buttons = [{
+ label: this._stringBundle.GetStringFromName("safebrowsing.getMeOutOfHereButton.label"),
+ accessKey: this._stringBundle.GetStringFromName("safebrowsing.getMeOutOfHereButton.accessKey"),
+ callback: getMeOutOfHere
+ }]
+
+ if (reportName) {
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/SafeBrowsing.jsm", tmp);
+ var reportUrl = tmp.SafeBrowsing.getReportURL(reportName, aBlockedInfo);
+
+ // There's no button if we can not get report url, for example
+ // if the provider of blockedInfo is not Google.
+ if (reportUrl) {
+ buttons.push({
+ label: this._stringBundle.GetStringFromName(label),
+ accessKey: this._stringBundle.GetStringFromName(accessKey),
+ callback() { openUILinkIn(reportUrl, "tabfocused"); }
+ });
+ }
+ }
+
+ var type = "blocked-badware-page";
+ var notification = this.getNotificationWithValue(type);
+ if (notification)
+ this.removeNotification(notification);
+
+ var box = this.appendNotification(title, type, null,
+ this.PRIORITY_CRITICAL_HIGH,
+ buttons);
+
+ // Persist the notification until the user removes so it
+ // doesn't get removed on redirects.
+ box.persistence = -1;
+ ]]>
+ </body>
+ </method>
+ <constructor>
+ <![CDATA[
+ var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
+
+ Services.obs.addObserver(this, "indexedDB-permissions-prompt");
+ Services.obs.addObserver(this, "indexedDB-quota-prompt");
+ Services.obs.addObserver(this, "indexedDB-quota-cancel");
+ Services.obs.addObserver(this, "addon-install-blocked");
+ Services.obs.addObserver(this, "addon-install-complete");
+ Services.obs.addObserver(this, "addon-install-disabled");
+ Services.obs.addObserver(this, "addon-install-failed");
+ Services.obs.addObserver(this, "addon-install-started");
+ Services.obs.addObserver(this, "offline-cache-update-completed");
+ Services.obs.addObserver(this, "perm-changed");
+ Services.obs.addObserver(this, "formsubmit");
+
+ Services.prefs.addObserver("privacy.popups.showBrowserMessage", this);
+ Services.prefs.addObserver("dom.disable_open_during_load", this);
+
+ this.addProgressListener();
+
+ if (AppConstants.MOZ_CRASHREPORTER)
+ ChromeUtils.import("resource://gre/modules/CrashSubmit.jsm", this);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ this.destroy();
+ ]]>
+ </destructor>
+
+ <field name="mDestroyed">false</field>
+
+ <!-- This is necessary because the destructor doesn't always get called when
+ we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
+ <method name="destroy">
+ <body>
+ <![CDATA[
+ if (this.mDestroyed)
+ return;
+ this.mDestroyed = true;
+
+ if (this._addedProgressListener) {
+ this.activeBrowser.webProgress.removeProgressListener(this);
+ this._addedProgressListener = false;
+ }
+
+ this._activeBrowser = null;
+ try {
+ Services.obs.removeObserver(this, "indexedDB-permissions-prompt");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "indexedDB-quota-prompt");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "indexedDB-quota-cancel");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "addon-install-blocked");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "addon-install-complete");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "addon-install-disabled");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "addon-install-failed");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "addon-install-started");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "offline-cache-update-completed");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "perm-changed");
+ } catch (ex) {}
+ try {
+ Services.obs.removeObserver(this, "formsubmit");
+ } catch (ex) {}
+
+ try {
+ Services.prefs.removeObserver("privacy.popups.showBrowserMessage", this);
+ } catch (ex) {}
+ try {
+ Services.prefs.removeObserver("dom.disable_open_during_load", this);
+ } catch (ex) {}
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="DOMContentLoaded" phase="capturing">
+ <![CDATA[
+ if (/^about:neterror\?e=netOffline/.test(event.target.documentURI))
+ event.target.addEventListener("click", function tryAgain(event) {
+ if (event.target.id == "errorTryAgain")
+ Services.io.offline = false;
+ }, true);
+ ]]>
+ </handler>
+
+ <handler event="DOMUpdatePageReport" phase="capturing">
+ <![CDATA[
+ var browser = this.activeBrowser;
+ if (!browser.blockedPopups || browser.blockedPopups.reported != false)
+ return;
+
+ // this.popupCount can be 0, while browser.blockedPopups has not been cleared.
+ if (!this.popupCount && browser.blockedPopups.length > 1) {
+ this.popupCount = browser.blockedPopups.length;
+ } else {
+ this.popupCount++;
+ }
+ this.playSoundForBlockedPopup();
+ this.notifyPopupCountChanged();
+
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+ if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage"))
+ {
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var message = this._stringBundle.GetStringFromName("popupWarning.message");
+ message = tmp.PluralForm.get(this.popupCount, message)
+ .replace("#1", brandShortName)
+ .replace("#2", this.popupCount);
+
+ var notification = this.getNotificationWithValue("popup-blocked");
+ if (notification) {
+ notification.label = message;
+ } else {
+ var popupButtonText = this._stringBundle.GetStringFromName("popupWarningButton");
+ var popupButtonAccesskey = this._stringBundle.GetStringFromName("popupWarningButton.accesskey");
+ var buttons = [{
+ label: popupButtonText,
+ accessKey: popupButtonAccesskey,
+ popup: "popupNotificationMenu",
+ callback: null
+ }];
+
+ const priority = this.PRIORITY_WARNING_MEDIUM;
+ this.appendNotification(message, "popup-blocked",
+ null, priority, buttons);
+ }
+ }
+ ]]>
+ </handler>
+
+ <handler event="PluginCrashed" phase="capturing">
+ <![CDATA[
+ // Ensure the plugin and event are of the right type.
+ var plugin = event.target;
+ var detail = event.detail;
+ if (!(detail instanceof Ci.nsIPropertyBag2))
+ return;
+
+ var submittedReport = detail.getPropertyAsBool("submittedCrashReport");
+ var doPrompt = true; // XXX followup for .getPropertyAsBool("doPrompt");
+ var submitReports = true; // XXX followup for .getPropertyAsBool("submitReports");
+ var pluginName = detail.getPropertyAsAString("pluginName");
+ var pluginFilename = detail.getPropertyAsAString("pluginFilename");
+ var pluginDumpID = detail.getPropertyAsAString("pluginDumpID");
+
+ // Remap the plugin name to a more user-presentable form.
+ pluginName = this.makeNicePluginName(pluginName);
+
+ // Force a style flush, so that we ensure our binding is attached.
+ plugin.clientTop;
+
+ // Configure the crashed-plugin placeholder.
+ var overlay = this.getPluginUI(plugin, "main");
+
+ var status;
+ var statusDiv = this.getPluginUI(plugin, "submitStatus");
+
+ if (this.CrashSubmit) {
+ // Determine which message to show regarding crash reports.
+ if (submittedReport) { // submitReports && !doPrompt, handled in observer
+ status = "submitted";
+ }
+ else if (!submitReports && !doPrompt) {
+ status = "noSubmit";
+ }
+ else { // doPrompt
+ status = "please";
+ this.getPluginUI(plugin, "submitButton").addEventListener("click",
+ function (event) {
+ if (event.button != 0 || !event.isTrusted)
+ return;
+ this.submitReport(pluginDumpID, plugin);
+ Services.prefs.setBoolPref("dom.ipc.plugins.reportCrashURL", optInCB.checked);
+ }.bind(this));
+ let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
+ optInCB.checked = Services.prefs.getBoolPref("dom.ipc.plugins.reportCrashURL");
+ }
+
+ // If we're showing the link to manually trigger report submission, we'll
+ // want to be able to update all the instances of the UI for this crash to
+ // show an updated message when a report is submitted.
+ if (doPrompt) {
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+ observe: function(subject, topic, data) {
+ let propertyBag = subject;
+ if (!(propertyBag instanceof Ci.nsIPropertyBag2))
+ return;
+ // Ignore notifications for other crashes.
+ if (propertyBag.get("minidumpID") != pluginDumpID)
+ return;
+ statusDiv.setAttribute("status", data);
+ },
+
+ handleEvent : function(event) {
+ // Not expected to be called, just here for the closure.
+ }
+ }
+
+ // Use a weak reference, so we don't have to remove it...
+ Services.obs.addObserver(observer, "crash-report-status", true);
+ // ...alas, now we need something to hold a strong reference to prevent
+ // it from being GC. But I don't want to manually manage the reference's
+ // lifetime (which should be no greater than the page).
+ // Clever solution? Use a closure with an event listener on the statusDiv.
+ // When it goes away, so do the listener references and the closure.
+ statusDiv.addEventListener("mozCleverClosureHack", observer);
+ }
+ }
+
+ // If we don't have a minidumpID, we can't (or didn't) submit anything.
+ // This can happen if the plugin is killed from the task manager.
+ if (!pluginDumpID) {
+ status = "noReport";
+ }
+
+ statusDiv.setAttribute("status", status);
+
+ var helpIcon = this.getPluginUI(plugin, "helpIcon");
+ this.addLinkClickCallback(helpIcon, this.openHelpPage);
+
+ var messageString = this._stringBundle.formatStringFromName("crashedpluginsMessage.title", [pluginName], 1);
+ var crashText = this.getPluginUI(plugin, "crashedText");
+ crashText.textContent = messageString;
+
+ var link = this.getPluginUI(plugin, "reloadLink");
+ this.addLinkClickCallback(link, this.reloadPage);
+
+ overlay.classList.add("visible");
+ // If a previous plugin on the page was too small and resulted in
+ // adding a notification bar, then remove it because this plugin
+ // instance it big enough to serve as in-content notification.
+ var notification = this.getNotificationWithValue("plugin-crashed");
+ if (notification)
+ this.removeNotification(notification, true);
+ this.crashNotified = true;
+ ]]>
+ </handler>
+
+ <handler event="MozApplicationManifest" phase="capturing">
+ <![CDATA[
+ if (!Services.prefs.getBoolPref("browser.offline-apps.notify"))
+ return;
+
+ try {
+ if (Services.prefs.getBoolPref("offline-apps.allow_by_default"))
+ return;
+ } catch (e) {
+ }
+
+ this.offlineAppRequested(event.originalTarget);
+ ]]>
+ </handler>
+
+ <handler event="pageshow" phase="capturing">
+ <![CDATA[
+ // |event.persisted| is true when the page is loaded from the
+ // BF cache, so this code reshows the notification if necessary.
+ if (!event.persisted)
+ return;
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="popup-notification"
+ extends="chrome://communicator/content/bindings/notification.xml#browser-notificationbox">
+ <implementation>
+ <method name="onLocationChange">
+ <parameter name="aWebProgress" />
+ <parameter name="aRequest" />
+ <parameter name="aLocation" />
+ <parameter name="aFlags" />
+ <body>
+ <![CDATA[
+ const nsIWebProgressListener = Ci.nsIWebProgressListener;
+ if (aWebProgress.DOMWindow == this.activeBrowser.contentWindow &&
+ !(aFlags & nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
+ this.onDocumentChange();
+ PopupNotifications.locationChange(this.activeBrowser);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeNotifications">
+ <parameter name="aNotifications"/>
+ <body>
+ <![CDATA[
+ aNotifications.forEach(function(value) {
+ var notification = PopupNotifications.getNotification(value);
+ if (notification)
+ PopupNotifications.remove(notification);
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeInstallRequest">
+ <parameter name="aHost"/>
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.formatStringFromName("lwthemeInstallRequest.message", [aHost], 1);
+ var action = {
+ label: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton.accesskey"),
+ callback: aCallback
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "lwtheme-install-request", message,
+ "addons-notification-icon", action);
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeInstallNotification">
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.GetStringFromName("lwthemeInstallNotification.message");
+ var mainAction = {
+ label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton.accesskey"),
+ callback: aCallback
+ };
+ var secondaryActions = [{
+ label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton.accesskey"),
+ callback: function() {
+ window.toEM("addons://list/theme");
+ }
+ }];
+ var options = {
+ timeout: Date.now() + 20000 // 20 seconds
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "lwtheme-install-notification", message,
+ "addons-notification-icon", mainAction,
+ secondaryActions, options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="lwthemeNeedsRestart">
+ <parameter name="aNewThemeName"/>
+ <body>
+ <![CDATA[
+ var message = this._stringBundle.formatStringFromName("lwthemeNeedsRestart.message", [aNewThemeName], 1);
+ var action = {
+ label: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton"),
+ accessKey: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton.accesskey"),
+ callback: function() {
+ BrowserUtils.restartApplication();
+ }
+ };
+ var options = {
+ timeout: Date.now() + 20000 // 20 seconds
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "lwtheme-install-notification", message,
+ "addons-notification-icon", action, null,
+ options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="promptIndexedDB">
+ <parameter name="aRequestor"/>
+ <parameter name="aWindow"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ var host = aWindow.document.documentURIObject.asciiHost;
+ var property = this.usePrivateBrowsing ? "offlineApps.private" :
+ "offlineApps." + aTopic;
+ var message = this._stringBundle.formatStringFromName(property,
+ [host, aData], 2);
+ var observer = aRequestor.getInterface(Ci.nsIObserver);
+ var mainAction = {
+ label: this._stringBundle.GetStringFromName("offlineApps.always"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.always.accesskey"),
+ callback: function allowIndexedDB() {
+ clearTimeout(notification.timeout);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ }
+ };
+ var secondaryActions = [{
+ label: this._stringBundle.GetStringFromName("offlineApps.never"),
+ accessKey: this._stringBundle.GetStringFromName("offlineApps.never.accesskey"),
+ callback: function denyIndexedDB() {
+ clearTimeout(notification.timeout);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.DENY_ACTION);
+ }
+ }];
+ function notificationTimedOut() {
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ notification.remove();
+ }
+ var options = {
+ eventCallback: function(state) {
+ if (notification) {
+ // Always clear the timeout up front. If the doorhanger was
+ // temporarily dismissed, we'll set a new 30 second timeout
+ // to automatically cancel the request. If the doorhanger
+ // gets redisplayed we don't want it to time out unless it
+ // gets dismissed again. And if the doorhanger gets removed
+ // then we aren't interested in it any more.
+ clearTimeout(timeout);
+ if (state == "dismissed")
+ timeout = setTimeout(notificationTimedOut, 30000);
+ }
+ }
+ };
+ var notification =
+ PopupNotifications.show(this.activeBrowser,
+ "indexedDB-" + aTopic + "-prompt",
+ message, "indexedDB-notification-icon",
+ this.usePrivateBrowsing ? null : mainAction,
+ secondaryActions, options);
+ var timeout = setTimeout(notificationTimedOut, 300000); // 5 minutes
+ ]]>
+ </body>
+ </method>
+
+ <method name="cancelIndexedDB">
+ <parameter name="aRequestor"/>
+ <parameter name="aWindow"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ var popupNotification = PopupNotifications.getNotification("indexedDB-" + aTopic + "-prompt", this.activeBrowser);
+ if (popupNotification)
+ popupNotification.remove(); // eventCallback clears the timeout
+
+ var observer = aRequestor.getInterface(Ci.nsIObserver);
+ observer.observe(null, "indexedDB-" + aTopic + "-response",
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallBlocked">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var host;
+ try {
+ // this fails with nsSimpleURIs like data: URIs
+ host = installInfo.originatingURI.host;
+ } catch (ex) {
+ host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
+ }
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var messageString = this._stringBundle.formatStringFromName("xpinstallPromptWarning",
+ [brandShortName, host], 2);
+ var action = {
+ label: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton"),
+ accessKey: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton.accesskey"),
+ callback: function allowInstall() {
+ installInfo.install();
+ return false;
+ }
+ };
+
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ timeout: Date.now() + 30000
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-blocked", messageString,
+ "addons-notification-icon", action,
+ null, options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallCancelled">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+ var messageString = this._stringBundle.GetStringFromName("addonDownloadCancelled");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
+ var action = {
+ label: this._stringBundle.GetStringFromName("addonDownloadRestartButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonDownloadRestartButton.accesskey"),
+ popup: null,
+ callback: function() {
+ var weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]
+ .getService(Ci.amIWebInstallListener);
+ if (weblistener.onWebInstallRequested(installInfo.browser, installInfo.originatingURI,
+ [aInstall], 1)) {
+ aInstall.install();
+ }
+ }
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-cancelled", messageString,
+ "addons-notification-icon", action);
+
+ installInfo.installs.every(function(aInstall) {
+ aInstall.cancel();
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallComplete">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+
+ var messageString;
+ var mainAction = null;
+ var secondaryActions = null;
+
+ if ("toEM" in window) {
+ mainAction = {
+ label: this._stringBundle.GetStringFromName("addonInstallManageButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonInstallManageButton.accesskey"),
+ callback: function() {
+ window.toEM("addons://list/extension");
+ }
+ };
+ }
+
+ if (installInfo.installs.some(install =>
+ install.addon.pendingOperations &
+ tmp.AddonManager.PENDING_INSTALL)) {
+ messageString = this._stringBundle.GetStringFromName("addonsInstalledNeedsRestart");
+ if (mainAction)
+ secondaryActions = [mainAction];
+ mainAction = {
+ label: this._stringBundle.GetStringFromName("addonInstallRestartButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonInstallRestartButton.accesskey"),
+ callback: function () {
+ BrowserUtils.restartApplication();
+ }
+ };
+ } else {
+ messageString = this._stringBundle.GetStringFromName("addonsInstalled");
+ }
+
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString)
+ .replace("#1", installInfo.installs[0].name)
+ .replace("#2", installInfo.installs.length)
+ .replace("#3", brandShortName);
+
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ timeout: Date.now() + 30000
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-complete", messageString,
+ "addons-notification-icon", mainAction,
+ secondaryActions, options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallDisabled">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var messageString;
+ var action = null;
+
+ if (Services.prefs.prefIsLocked("xpinstall.enabled"))
+ messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessageLocked");
+ else {
+ messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessage");
+ action = {
+ label: this._stringBundle.GetStringFromName("xpinstallDisabledButton"),
+ accessKey: this._stringBundle.GetStringFromName("xpinstallDisabledButton.accesskey"),
+ callback: function editPrefs() {
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ }
+ };
+ }
+
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ timeout: Date.now() + 30000
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-disabled", messageString,
+ "addons-notification-icon", action,
+ null, options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallFailed">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var host;
+ try {
+ // this fails with nsSimpleURIs like data: URIs
+ host = installInfo.originatingURI.host;
+ } catch (ex) {
+ host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
+ }
+
+ var error = "addonErrorIncompatible";
+ var name = installInfo.installs[0].name;
+ installInfo.installs.some(function(install) {
+ if (install.error) {
+ name = install.name;
+ error = "addonError" + install.error;
+ return true;
+ }
+ if (install.addon.blocklistState ==
+ Ci.nsIBlocklistService.STATE_BLOCKED) {
+ name = install.name;
+ error = "addonErrorBlocklisted";
+ }
+ return false;
+ });
+
+ var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
+ var version = Services.appinfo.version;
+ var messageString = this._stringBundle.GetStringFromName(error)
+ .replace("#1", name)
+ .replace("#2", host)
+ .replace("#3", brandShortName)
+ .replace("#4", version);
+
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ timeout: Date.now() + 30000
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-failed", messageString,
+ "addons-notification-icon", null,
+ null, options);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addonInstallStarted">
+ <parameter name="installInfo"/>
+ <body>
+ <![CDATA[
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ if (installInfo.installs.every(function(aInstall) {
+ return aInstall.state == tmp.AddonManager.STATE_DOWNLOADED;
+ }))
+ return;
+
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm", tmp);
+ var messageString = this._stringBundle.GetStringFromName("addonDownloading");
+ messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
+ var action = {
+ label: this._stringBundle.GetStringFromName("addonDownloadCancelButton"),
+ accessKey: this._stringBundle.GetStringFromName("addonDownloadCancelButton.accesskey"),
+ callback: this.addonInstallCancelled.bind(this, installInfo)
+ };
+ var options = {
+ installInfo: installInfo
+ };
+ PopupNotifications.show(this.activeBrowser,
+ "addon-install-started", messageString,
+ "addons-notification-icon", action,
+ null, options);
+ ]]>
+ </body>
+ </method>
+ <constructor>
+ <![CDATA[
+ ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="addon-progress-notification"
+ extends="chrome://global/content/bindings/notification.xml#notification">
+ <content>
+ <xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type">
+ <xul:hbox anonid="details" align="center" flex="1"
+ oncommand="this.parentNode.parentNode._doButtonCommand(event);">
+ <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
+ <xul:description anonid="messageText" class="messageText" xbl:inherits="xbl:text=label"/>
+ <xul:progressmeter mode="undetermined" xbl:inherits="mode,value=progress"/>
+ <xul:label flex="1" xbl:inherits="value=status"/>
+ <children/>
+ </xul:hbox>
+ <xul:toolbarbutton ondblclick="event.stopPropagation();"
+ class="messageCloseButton tabbable"
+ xbl:inherits="hidden=hideclose"
+ tooltiptext="&closeNotification.tooltip;"
+ oncommand="document.getBindingParent(this).close();"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <destructor>
+ <![CDATA[
+ this.installInfo.installs.forEach(function(aInstall) {
+ aInstall.removeListener(this);
+ }, this);
+ ]]>
+ </destructor>
+
+ <method name="updateProgress">
+ <body>
+ <![CDATA[
+ var count = 0;
+ var progress = 0;
+ var max = 0;
+
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ this.installInfo.installs.forEach(function(aInstall) {
+ if (aInstall.maxProgress < 0)
+ max = -1;
+ else if (max >= 0)
+ max += aInstall.maxProgress;
+ progress += aInstall.progress;
+ if (aInstall.state < tmp.AddonManager.STATE_DOWNLOADED)
+ count++;
+ });
+
+ if (max < 0)
+ this.setAttribute("mode", "undetermined");
+ else {
+ this.setAttribute("mode", "determined");
+ this.setAttribute("progress", progress * 100 / max);
+ }
+
+ var now = Date.now();
+ if (!this.startTime) {
+ this.startTime = now;
+ this.lastUpdate = now - 750;
+ this.lastSeconds = null;
+ }
+
+ if (progress == max || now - this.lastUpdate >= 750) {
+ this.lastUpdate = now;
+ var elapsed = (now - this.startTime) / 1000;
+ var rate = elapsed && progress / elapsed;
+ ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", tmp);
+ var status;
+ [status, this.lastSeconds] = tmp.DownloadUtils.getDownloadStatus(progress, max, rate, this.lastSeconds);
+ this.setAttribute("status", status);
+ }
+
+ if (!count)
+ this.close();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadProgress">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadFailed">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadCancelled">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadEnded">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="sidebar-notification"
+ extends="chrome://global/content/bindings/notification.xml#notification">
+ <content>
+ <xul:vbox class="notification-inner outset" flex="1" xbl:inherits="type">
+ <xul:hbox align="center">
+ <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
+ <xul:arrowscrollbox orient="horizontal" flex="1" pack="end"
+ oncommand="document.getBindingParent(this)._doButtonCommand(event);">
+ <children/>
+ </xul:arrowscrollbox>
+ <xul:toolbarbutton ondblclick="event.stopPropagation();"
+ class="messageCloseButton tabbable"
+ xbl:inherits="hidden=hideclose"
+ tooltiptext="&closeNotification.tooltip;"
+ oncommand="document.getBindingParent(this).close();"/>
+ </xul:hbox>
+ <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
+ </xul:vbox>
+ </content>
+ </binding>
+
+ <binding id="sidebar-addon-progress-notification"
+ extends="chrome://communicator/content/bindings/notification.xml#addon-progress-notification">
+ <content>
+ <xul:vbox class="notification-inner outset" flex="1" xbl:inherits="type">
+ <xul:hbox align="center">
+ <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
+ <xul:arrowscrollbox orient="horizontal" flex="1" pack="end"
+ oncommand="document.getBindingParent(this)._doButtonCommand(event);">
+ <children/>
+ </xul:arrowscrollbox>
+ <xul:toolbarbutton ondblclick="event.stopPropagation();"
+ class="messageCloseButton tabbable"
+ xbl:inherits="hidden=hideclose"
+ tooltiptext="&closeNotification.tooltip;"
+ oncommand="document.getBindingParent(this).close();"/>
+ </xul:hbox>
+ <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
+ <xul:progressmeter mode="undetermined" xbl:inherits="mode,value=progress"/>
+ <xul:label xbl:inherits="value=status"/>
+ </xul:vbox>
+ </content>
+ </binding>
+
+ <binding id="addon-progress-popup-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
+ <content align="start">
+ <xul:image class="popup-notification-icon" xbl:inherits="popupid"/>
+ <xul:vbox flex="1">
+ <xul:description class="popup-notification-description addon-progress-description"
+ xbl:inherits="xbl:text=label"/>
+ <xul:hbox class="popup-notification-button-container" align="center">
+ <xul:progressmeter mode="undetermined"
+ xbl:inherits="mode,value=progress"/>
+ <xul:spacer flex="1"/>
+ <xul:button anonid="button"
+ class="popup-notification-menubutton"
+ type="menu-button"
+ xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
+ <xul:menupopup anonid="menupopup"
+ xbl:inherits="oncommand=menucommand">
+ <children/>
+ <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
+ label="&closeNotificationItem.label;"
+ xbl:inherits="oncommand=closeitemcommand"/>
+ </xul:menupopup>
+ </xul:button>
+ </xul:hbox>
+ <xul:label xbl:inherits="xbl:text=status"/>
+ </xul:vbox>
+ <xul:vbox pack="start">
+ <xul:toolbarbutton anonid="closebutton"
+ class="messageCloseButton close-icon popup-notification-closebutton tabbable"
+ xbl:inherits="oncommand=closebuttoncommand"
+ tooltiptext="&closeNotification.tooltip;"/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.installInfo = this.notification.options.installInfo;
+ this.installInfo.installs.forEach(function(aInstall) {
+ aInstall.addListener(this);
+ }, this);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ this.installInfo.installs.forEach(function(aInstall) {
+ aInstall.removeListener(this);
+ }, this);
+ ]]>
+ </destructor>
+
+ <method name="updateProgress">
+ <body>
+ <![CDATA[
+ var count = 0;
+ var progress = 0;
+ var max = 0;
+
+ var tmp = {};
+ ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ this.installInfo.installs.forEach(function(aInstall) {
+ if (aInstall.maxProgress < 0)
+ max = -1;
+ else if (max >= 0)
+ max += aInstall.maxProgress;
+ progress += aInstall.progress;
+ if (aInstall.state < tmp.AddonManager.STATE_DOWNLOADED)
+ count++;
+ });
+
+ if (max < 0)
+ this.setAttribute("mode", "undetermined");
+ else {
+ this.setAttribute("mode", "determined");
+ this.setAttribute("progress", progress * 100 / max);
+ }
+
+ var now = Date.now();
+ if (!this.startTime) {
+ this.startTime = now;
+ this.lastUpdate = now - 750;
+ this.lastSeconds = null;
+ }
+
+ if (progress == max || now - this.lastUpdate >= 750) {
+ this.lastUpdate = now;
+ var elapsed = (now - this.startTime) / 1000;
+ var rate = elapsed && progress / elapsed;
+ ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", tmp);
+ var status;
+ [status, this.lastSeconds] = tmp.DownloadUtils.getDownloadStatus(progress, max, rate, this.lastSeconds);
+ this.setAttribute("status", status);
+ }
+
+ if (!count)
+ PopupNotifications.remove(this.notification);
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadProgress">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadFailed">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadCancelled">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+
+ <method name="onDownloadEnded">
+ <body>
+ <![CDATA[
+ this.updateProgress();
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="center-item">
+ <content>
+ <xul:vbox flex="1" class="center-item-box"
+ xbl:inherits="warn,showseparator,padbottom">
+ <xul:hbox align="center">
+ <xul:image class="center-item-icon"
+ xbl:inherits="src=itemicon"/>
+ <xul:description class="center-item-label"
+ xbl:inherits="xbl:text=itemtext"/>
+ <xul:spacer flex="1"/>
+ <xul:button class="popup-notification-menubutton center-item-button"
+ oncommand="document.getBindingParent(this).runCallback();"
+ xbl:inherits="label=buttonlabel"/>
+ </xul:hbox>
+ <xul:hbox align="center" class="center-item-warning">
+ <xul:image class="center-item-warning-icon"/>
+ <xul:label class="center-item-warning-description" xbl:inherits="xbl:text=warningText"/>
+ <xul:label xbl:inherits="href=updateLink" value="&checkForUpdates;" class="text-link"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <resources>
+ <stylesheet src="chrome://global/skin/notification.css"/>
+ </resources>
+ <implementation>
+ <field name="action"></field>
+ <method name="runCallback">
+ <body><![CDATA[
+ let action = this.action;
+ action.callback();
+ let cas = action.popupnotification.notification.options.centerActions;
+ cas.splice(cas.indexOf(action), 1);
+ PopupNotifications._dismiss();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/bindings/numberbox.xml b/comm/suite/components/bindings/numberbox.xml
new file mode 100644
index 0000000000..8f18bcbcdd
--- /dev/null
+++ b/comm/suite/components/bindings/numberbox.xml
@@ -0,0 +1,217 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="numberboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="numberbox" extends="chrome://messenger/content/textbox.xml#textbox">
+
+ <content>
+ <xul:moz-input-box class="textbox-input-box numberbox-input-box" flex="1"
+ xbl:inherits="context,disabled,focused">
+ <html:input class="numberbox-input textbox-input" anonid="input"
+ xbl:inherits="value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
+ </xul:moz-input-box>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled,hidden=hidespinbuttons"/>
+ </content>
+
+ <implementation>
+ <field name="_valueEntered">false</field>
+ <field name="_spinButtons">null</field>
+ <field name="_value">0</field>
+
+ <property name="spinButtons" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._spinButtons)
+ this._spinButtons = document.getAnonymousElementByAttribute(this, "anonid", "buttons");
+ return this._spinButtons;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="value" onget="return '' + this.valueNumber"
+ onset="return this.valueNumber = val;"/>
+
+ <property name="valueNumber">
+ <getter>
+ if (this._valueEntered) {
+ var newval = this.inputField.value;
+ this._validateValue(newval);
+ }
+ return this._value;
+ </getter>
+ <setter>
+ this._validateValue(val);
+ return val;
+ </setter>
+ </property>
+ <property name="min">
+ <getter>
+ var min = this.getAttribute("min");
+ return min ? Number(min) : 0;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val == "number") {
+ this.setAttribute("min", val);
+ if (this.valueNumber < val)
+ this._validateValue(val);
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="max">
+ <getter>
+ var max = this.getAttribute("max");
+ return max ? Number(max) : Infinity;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val != "number")
+ return val;
+ var min = this.min;
+ if (val < min)
+ val = min;
+ this.setAttribute("max", val);
+ if (this.valueNumber > val)
+ this._validateValue(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="_modifyUp">
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+ var oldval = this.valueNumber;
+ var newval = this._validateValue(this.valueNumber + 1);
+ this.inputField.select();
+ if (oldval != newval)
+ this._fireChange();
+ ]]>
+ </body>
+ </method>
+ <method name="_modifyDown">
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+ var oldval = this.valueNumber;
+ var newval = this._validateValue(this.valueNumber - 1);
+ this.inputField.select();
+ if (oldval != newval)
+ this._fireChange();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_enableDisableButtons">
+ <body>
+ <![CDATA[
+ var buttons = this.spinButtons;
+ if (this.disabled || this.readOnly) {
+ buttons.decreaseDisabled = buttons.increaseDisabled = true;
+ } else {
+ buttons.decreaseDisabled = (this.valueNumber <= this.min);
+ buttons.increaseDisabled = (this.valueNumber >= this.max);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_validateValue">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ aValue = Number(aValue) || 0;
+ aValue = Math.round(aValue);
+
+ var min = this.min;
+ var max = this.max;
+ if (aValue < min)
+ aValue = min;
+ else if (aValue > max)
+ aValue = max;
+
+ this._valueEntered = false;
+ this._value = Number(aValue);
+ this.inputField.value = aValue;
+
+ this._enableDisableButtons();
+
+ return aValue;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_fireChange">
+ <body>
+ var evt = document.createEvent("Events");
+ evt.initEvent("change", true, true);
+ this.dispatchEvent(evt);
+ </body>
+ </method>
+
+ <constructor><![CDATA[
+ if (this.max < this.min)
+ this.max = this.min;
+
+ var value = this.inputField.value || 0;
+ this._validateValue(value);
+ ]]></constructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="input" phase="capturing">
+ this._valueEntered = true;
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ if (!event.ctrlKey && !event.metaKey && !event.altKey && event.charCode) {
+ if (event.charCode == 45 && this.min < 0)
+ return;
+
+ if (event.charCode < 48 || event.charCode > 57)
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+
+ <handler event="keypress" keycode="VK_UP">
+ this._modifyUp();
+ </handler>
+
+ <handler event="keypress" keycode="VK_DOWN">
+ this._modifyDown();
+ </handler>
+
+ <handler event="up" preventdefault="true">
+ this._modifyUp();
+ </handler>
+
+ <handler event="down" preventdefault="true">
+ this._modifyDown();
+ </handler>
+
+ <handler event="change">
+ if (event.originalTarget == this.inputField) {
+ var newval = this.inputField.value;
+ this._validateValue(newval);
+ }
+ </handler>
+ </handlers>
+
+ </binding>
+</bindings>
diff --git a/comm/suite/components/bindings/preferences.xml b/comm/suite/components/bindings/preferences.xml
new file mode 100644
index 0000000000..8c5872ee76
--- /dev/null
+++ b/comm/suite/components/bindings/preferences.xml
@@ -0,0 +1,817 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE bindings [
+ <!ENTITY % preferencesDTD SYSTEM "chrome://communicator/locale/pref/preferences.dtd">
+ %preferencesDTD;
+ <!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
+ %globalKeysDTD;
+]>
+
+<bindings id="preferencesBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+#
+# = Preferences Window Framework
+#
+# The syntax for use looks something like:
+#
+# <prefwindow>
+# <prefpane id="prefPaneA">
+# <preferences>
+# <preference id="preference1" name="app.preference1" type="bool" onchange="foo();"/>
+# <preference id="preference2" name="app.preference2" type="bool" useDefault="true"/>
+# </preferences>
+# <checkbox label="Preference" preference="preference1"/>
+# </prefpane>
+# </prefwindow>
+#
+
+ <binding id="preferences">
+ <implementation implements="nsIObserver">
+ <method name="_constructAfterChildren">
+ <body>
+ <![CDATA[
+ // This method will be called after the last of the child
+ // <preference> elements is constructed. Its purpose is to propagate
+ // the values to the associated form elements. Sometimes the code for
+ // some <preference> initializers depend on other <preference> elements
+ // being initialized so we wait and call updateElements on all of them
+ // once the last one has been constructed. See bugs 997570 and 992185.
+
+ var elements = this.getElementsByTagName("preference");
+ for (let element of elements) {
+ element.updateElements();
+ }
+
+ this._constructAfterChildrenCalled = true;
+ ]]>
+ </body>
+ </method>
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ for (var i = 0; i < this.childNodes.length; ++i) {
+ var preference = this.childNodes[i];
+ if (preference.name == aData) {
+ preference.value = preference.valueFromPreferences;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="fireChangedEvent">
+ <parameter name="aPreference"/>
+ <body>
+ <![CDATA[
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("change", true, true);
+ aPreference.dispatchEvent(event);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="service">
+ Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
+ </field>
+ <field name="rootBranch">
+ Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ </field>
+ <field name="defaultBranch">
+ this.service.getDefaultBranch("");
+ </field>
+ <field name="rootBranchInternal">
+ Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ </field>
+ <property name="type" readonly="true">
+ <getter>
+ <![CDATA[
+ return document.documentElement.type || "";
+ ]]>
+ </getter>
+ </property>
+ <property name="instantApply" readonly="true">
+ <getter>
+ <![CDATA[
+ var doc = document.documentElement;
+ return this.type == "child" ? doc.instantApply
+ : doc.instantApply || this.rootBranch.getBoolPref("browser.preferences.instantApply");
+ ]]>
+ </getter>
+ </property>
+
+ <!-- We want to call _constructAfterChildren after all child
+ <preference> elements have been constructed. To do this, we get
+ and store the node list of all child <preference> elements in the
+ constructor, and maintain a count which is incremented in the
+ constructor of <preference>. _constructAfterChildren is called
+ when the count matches the length of the list. -->
+ <field name="_constructedChildrenCount">0</field>
+ <field name="_preferenceChildren">null</field>
+ <!-- Some <preference> elements are added dynamically after
+ _constructAfterChildren has already been called - we want to
+ avoid looping over all of them again in this case so we remember
+ if we already called it. -->
+ <field name="_constructAfterChildrenCalled">false</field>
+ <constructor>
+ <![CDATA[
+ this._preferenceChildren = this.getElementsByTagName("preference");
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="preference">
+ <implementation>
+ <constructor>
+ <![CDATA[
+ // if the element has been inserted without the name attribute set,
+ // we have nothing to do here
+ if (!this.name)
+ return;
+
+ this.preferences.rootBranchInternal
+ .addObserver(this.name, this.preferences);
+ // In non-instant apply mode, we must try and use the last saved state
+ // from any previous opens of a child dialog instead of the value from
+ // preferences, to pick up any edits a user may have made.
+
+ var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+ if (this.preferences.type == "child" &&
+ !this.instantApply && window.opener &&
+ secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
+ var pdoc = window.opener.document;
+
+ // Try to find a preference element for the same preference.
+ var preference = null;
+ var parentPreferences = pdoc.getElementsByTagName("preferences");
+ for (var k = 0; (k < parentPreferences.length && !preference); ++k) {
+ var parentPrefs = parentPreferences[k]
+ .getElementsByAttribute("name", this.name);
+ for (var l = 0; (l < parentPrefs.length && !preference); ++l) {
+ if (parentPrefs[l].localName == "preference")
+ preference = parentPrefs[l];
+ }
+ }
+
+ // Don't use the value setter here, we don't want updateElements to be prematurely fired.
+ this._value = preference ? preference.value : this.valueFromPreferences;
+ } else {
+ this._value = this.valueFromPreferences;
+ }
+ if (this.preferences._constructAfterChildrenCalled) {
+ // This <preference> was added after _constructAfterChildren() was already called.
+ // We can directly call updateElements().
+ this.updateElements();
+ } else {
+ this.preferences._constructedChildrenCount++;
+ if (this.preferences._constructedChildrenCount ==
+ this.preferences._preferenceChildren.length) {
+ // This is the last <preference>, time to updateElements() on all of them.
+ this.preferences._constructAfterChildren();
+ }
+ }
+
+ this.dispatchEvent(new CustomEvent("bindingattached", { bubbles: false }));
+ ]]>
+ </constructor>
+ <destructor>
+ this.preferences.rootBranchInternal
+ .removeObserver(this.name, this.preferences);
+ </destructor>
+ <field name="_constructed">false</field>
+ <property name="instantApply">
+ <getter>
+ if (this.getAttribute("instantApply") == "false")
+ return false;
+ return this.getAttribute("instantApply") == "true" || this.preferences.instantApply;
+ </getter>
+ </property>
+
+ <property name="preferences" onget="return this.parentNode"/>
+ <property name="name" onget="return this.getAttribute('name');">
+ <setter>
+ if (val == this.name)
+ return val;
+
+ this.preferences.rootBranchInternal
+ .removeObserver(this.name, this.preferences);
+ this.setAttribute("name", val);
+ this.preferences.rootBranchInternal
+ .addObserver(val, this.preferences);
+
+ return val;
+ </setter>
+ </property>
+ <property name="type" onget="return this.getAttribute('type');"
+ onset="this.setAttribute('type', val); return val;"/>
+ <property name="inverted" onget="return this.getAttribute('inverted') == 'true';"
+ onset="this.setAttribute('inverted', val); return val;"/>
+ <property name="readonly" onget="return this.getAttribute('readonly') == 'true';"
+ onset="this.setAttribute('readonly', val); return val;"/>
+
+ <field name="_value">null</field>
+ <method name="_setValue">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (this.value !== aValue) {
+ this._value = aValue;
+ if (this.instantApply)
+ this.valueFromPreferences = aValue;
+ this.preferences.fireChangedEvent(this);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <property name="value" onget="return this._value" onset="return this._setValue(val);"/>
+
+ <property name="locked">
+ <getter>
+ return this.preferences.rootBranch.prefIsLocked(this.name);
+ </getter>
+ </property>
+
+ <property name="disabled">
+ <getter>
+ return this.getAttribute("disabled") == "true";
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("disabled", "true");
+ else
+ this.removeAttribute("disabled");
+
+ if (!this.id)
+ return val;
+
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i) {
+ elements[i].disabled = val;
+
+ var labels = document.getElementsByAttribute("control", elements[i].id);
+ for (var j = 0; j < labels.length; ++j)
+ labels[j].disabled = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="tabIndex">
+ <getter>
+ return parseInt(this.getAttribute("tabindex"));
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("tabindex", val);
+ else
+ this.removeAttribute("tabindex");
+
+ if (!this.id)
+ return val;
+
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i) {
+ elements[i].tabIndex = val;
+
+ var labels = document.getElementsByAttribute("control", elements[i].id);
+ for (var j = 0; j < labels.length; ++j)
+ labels[j].tabIndex = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="hasUserValue">
+ <getter>
+ <![CDATA[
+ return this.preferences.rootBranch.prefHasUserValue(this.name) &&
+ this.value !== undefined;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="reset">
+ <body>
+ // defer reset until preference update
+ this.value = undefined;
+ </body>
+ </method>
+
+ <field name="_useDefault">false</field>
+ <property name="defaultValue">
+ <getter>
+ <![CDATA[
+ this._useDefault = true;
+ var val = this.valueFromPreferences;
+ this._useDefault = false;
+ return val;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="_branch">
+ <getter>
+ return this._useDefault ? this.preferences.defaultBranch : this.preferences.rootBranch;
+ </getter>
+ </property>
+
+ <field name="batching">false</field>
+
+ <method name="_reportUnknownType">
+ <body>
+ <![CDATA[
+ var consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+ var msg = "<preference> with id='" + this.id + "' and name='" +
+ this.name + "' has unknown type '" + this.type + "'.";
+ consoleService.logStringMessage(msg);
+ ]]>
+ </body>
+ </method>
+
+ <property name="valueFromPreferences">
+ <getter>
+ <![CDATA[
+ try {
+ // Force a resync of value with preferences.
+ switch (this.type) {
+ case "int":
+ return this._branch.getIntPref(this.name);
+ case "bool":
+ var val = this._branch.getBoolPref(this.name);
+ return this.inverted ? !val : val;
+ case "wstring":
+ return this._branch
+ .getComplexValue(this.name, Ci.nsIPrefLocalizedString)
+ .data;
+ case "string":
+ case "unichar":
+ return this._branch.getStringPref(this.name);
+ case "fontname":
+ var family = this._branch.getStringPref(this.name);
+ var fontEnumerator = Cc["@mozilla.org/gfx/fontenumerator;1"]
+ .createInstance(Ci.nsIFontEnumerator);
+ return fontEnumerator.getStandardFamilyName(family);
+ case "file":
+ var f = this._branch
+ .getComplexValue(this.name, Ci.nsIFile);
+ return f;
+ default:
+ this._reportUnknownType();
+ }
+ } catch (e) { }
+ return null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ // Exit early if nothing to do.
+ if (this.readonly || this.valueFromPreferences == val)
+ return val;
+
+ // The special value undefined means 'reset preference to default'.
+ if (val === undefined) {
+ this.preferences.rootBranch.clearUserPref(this.name);
+ return val;
+ }
+
+ // Force a resync of preferences with value.
+ switch (this.type) {
+ case "int":
+ this.preferences.rootBranch.setIntPref(this.name, val);
+ break;
+ case "bool":
+ this.preferences.rootBranch.setBoolPref(this.name, this.inverted ? !val : val);
+ break;
+ case "wstring":
+ var pls = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = val;
+ this.preferences.rootBranch
+ .setComplexValue(this.name, Ci.nsIPrefLocalizedString, pls);
+ break;
+ case "string":
+ case "unichar":
+ case "fontname":
+ this.preferences.rootBranch.setStringPref(this.name, val);
+ break;
+ case "file":
+ var lf;
+ if (typeof(val) == "string") {
+ lf = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsIFile);
+ lf.persistentDescriptor = val;
+ if (!lf.exists())
+ lf.initWithPath(val);
+ } else {
+ lf = val.QueryInterface(Ci.nsIFile);
+ }
+ this.preferences.rootBranch
+ .setComplexValue(this.name, Ci.nsIFile, lf);
+ break;
+ default:
+ this._reportUnknownType();
+ }
+ if (!this.batching)
+ this.preferences.service.savePrefFile(null);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="setElementValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (this.locked)
+ aElement.disabled = true;
+
+ if (!this.isElementEditable(aElement))
+ return;
+
+ var rv = undefined;
+ if (aElement.hasAttribute("onsyncfrompreference")) {
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("syncfrompreference", true, true);
+ var f = new Function("event",
+ aElement.getAttribute("onsyncfrompreference"));
+ rv = f.call(aElement, event);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ var val = rv;
+ if (val === undefined)
+ val = this.instantApply ? this.valueFromPreferences : this.value;
+ // if the preference is marked for reset, show default value in UI
+ if (val === undefined)
+ val = this.defaultValue;
+
+ /**
+ * Initialize a UI element property with a value. Handles the case
+ * where an element has not yet had a XBL binding attached for it and
+ * the property setter does not yet exist by setting the same attribute
+ * on the XUL element using DOM apis and assuming the element's
+ * constructor or property getters appropriately handle this state.
+ */
+ function setValue(element, attribute, value) {
+ if (attribute in element)
+ element[attribute] = value;
+ else
+ element.setAttribute(attribute, value);
+ }
+ if (aElement.localName == "checkbox") {
+ setValue(aElement, "checked", val);
+ } else if (aElement.localName == "colorpicker") {
+ setValue(aElement, "color", val);
+ } else if (aElement.localName == "textbox") {
+ // XXXmano Bug 303998: Avoid a caret placement issue if either the
+ // preference observer or its setter calls updateElements as a result
+ // of the input event handler.
+ if (aElement.value !== val)
+ setValue(aElement, "value", val);
+ } else {
+ setValue(aElement, "value", val);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getElementValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (aElement.hasAttribute("onsynctopreference")) {
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("synctopreference", true, true);
+ var f = new Function("event",
+ aElement.getAttribute("onsynctopreference"));
+ var rv = f.call(aElement, event);
+ if (rv !== undefined)
+ return rv;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ /**
+ * Read the value of an attribute from an element, assuming the
+ * attribute is a property on the element's node API. If the property
+ * is not present in the API, then assume its value is contained in
+ * an attribute, as is the case before a binding has been attached.
+ */
+ function getValue(element, attribute) {
+ if (attribute in element)
+ return element[attribute];
+ return element.getAttribute(attribute);
+ }
+ if (aElement.localName == "checkbox")
+ var value = getValue(aElement, "checked");
+ else if (aElement.localName == "colorpicker")
+ value = getValue(aElement, "color");
+ else
+ value = getValue(aElement, "value");
+
+ switch (this.type) {
+ case "int":
+ return parseInt(value, 10) || 0;
+ case "bool":
+ return typeof(value) == "boolean" ? value : value == "true";
+ }
+ return value;
+ ]]>
+ </body>
+ </method>
+
+ <method name="isElementEditable">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ switch (aElement.localName) {
+ case "checkbox":
+ case "colorpicker":
+ case "radiogroup":
+ case "textbox":
+ case "richlistitem":
+ case "richlistbox":
+ case "menulist":
+ return true;
+ }
+ return aElement.getAttribute("preference-editable") == "true";
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateElements">
+ <body>
+ <![CDATA[
+ if (!this.id)
+ return;
+
+ // This "change" event handler tracks changes made to preferences by
+ // sources other than the user in this window.
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i)
+ this.setElementValue(elements[i]);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="change">
+ this.updateElements();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="prefpane">
+ <implementation>
+ <method name="writePreferences">
+ <parameter name="aFlushToDisk"/>
+ <body>
+ <![CDATA[
+ // Write all values to preferences.
+ if (this._deferredValueUpdateElements.size) {
+ this._finalizeDeferredElements();
+ }
+
+ var preferences = this.preferences;
+ for (var i = 0; i < preferences.length; ++i) {
+ var preference = preferences[i];
+ preference.batching = true;
+ preference.valueFromPreferences = preference.value;
+ preference.batching = false;
+ }
+ if (aFlushToDisk) {
+ var psvc = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+ psvc.savePrefFile(null);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="src"
+ onget="return this.getAttribute('src');"
+ onset="this.setAttribute('src', val); return val;"/>
+ <property name="selected"
+ onget="return this.getAttribute('selected') == 'true';"
+ onset="this.setAttribute('selected', val); return val;"/>
+ <property name="image"
+ onget="return this.getAttribute('image');"
+ onset="this.setAttribute('image', val); return val;"/>
+ <property name="label"
+ onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label', val); return val;"/>
+
+ <property name="preferenceElements"
+ onget="return this.getElementsByAttribute('preference', '*');"/>
+ <property name="preferences"
+ onget="return this.getElementsByTagName('preference');"/>
+
+ <property name="helpTopic">
+ <getter>
+ <![CDATA[
+ // if there are tabs, and the selected tab provides a helpTopic, return that
+ var box = this.getElementsByTagName("tabbox");
+ if (box[0]) {
+ var tab = box[0].selectedTab;
+ if (tab && tab.hasAttribute("helpTopic"))
+ return tab.getAttribute("helpTopic");
+ }
+
+ // otherwise, return the helpTopic of the current panel
+ return this.getAttribute("helpTopic");
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_loaded">false</field>
+ <property name="loaded"
+ onget="return !this.src ? true : this._loaded;"
+ onset="this._loaded = val; return val;"/>
+
+ <method name="preferenceForElement">
+ <parameter name="aElement"/>
+ <body>
+ return document.getElementById(aElement.getAttribute("preference"));
+ </body>
+ </method>
+
+ <method name="getPreferenceElement">
+ <parameter name="aStartElement"/>
+ <body>
+ <![CDATA[
+ var temp = aStartElement;
+ while (temp && temp.nodeType == Node.ELEMENT_NODE &&
+ !temp.hasAttribute("preference"))
+ temp = temp.parentNode;
+ return temp && temp.nodeType == Node.ELEMENT_NODE ?
+ temp : aStartElement;
+ ]]>
+ </body>
+ </method>
+
+ <property name="DeferredTask" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ ChromeUtils.import("resource://gre/modules/DeferredTask.jsm", module);
+ Object.defineProperty(this, "DeferredTask", {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: module.DeferredTask,
+ });
+ return module.DeferredTask;
+ ]]></getter>
+ </property>
+ <method name="_deferredValueUpdate">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ delete aElement._deferredValueUpdateTask;
+ let preference = document.getElementById(aElement.getAttribute("preference"));
+ let prefVal = preference.getElementValue(aElement);
+ preference.value = prefVal;
+ this._deferredValueUpdateElements.delete(aElement);
+ ]]>
+ </body>
+ </method>
+ <field name="_deferredValueUpdateElements">
+ new Set();
+ </field>
+ <method name="_finalizeDeferredElements">
+ <body>
+ <![CDATA[
+ for (let el of this._deferredValueUpdateElements) {
+ if (el._deferredValueUpdateTask) {
+ el._deferredValueUpdateTask.finalize();
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="userChangedValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ let element = this.getPreferenceElement(aElement);
+ if (element.hasAttribute("preference")) {
+ if (element.getAttribute("delayprefsave") != "true") {
+ var preference = document.getElementById(element.getAttribute("preference"));
+ var prefVal = preference.getElementValue(element);
+ preference.value = prefVal;
+ } else {
+ if (!element._deferredValueUpdateTask) {
+ element._deferredValueUpdateTask = new this.DeferredTask(this._deferredValueUpdate.bind(this, element), 1000);
+ this._deferredValueUpdateElements.add(element);
+ } else {
+ // Each time the preference is changed, restart the delay.
+ element._deferredValueUpdateTask.disarm();
+ }
+ element._deferredValueUpdateTask.arm();
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="contentHeight">
+ <getter>
+ var targetHeight = parseInt(window.getComputedStyle(this).height);
+ targetHeight += parseInt(window.getComputedStyle(this).marginTop);
+ targetHeight += parseInt(window.getComputedStyle(this).marginBottom);
+ return targetHeight;
+ </getter>
+ </property>
+ </implementation>
+ <handlers>
+ <handler event="command">
+ // This "command" event handler tracks changes made to preferences by
+ // the user in this window.
+ if (event.sourceEvent)
+ event = event.sourceEvent;
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="select">
+ // This "select" event handler tracks changes made to colorpicker
+ // preferences by the user in this window.
+ if (event.target.localName == "colorpicker")
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="change">
+ // This "change" event handler tracks changes made to preferences by
+ // the user in this window.
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="input">
+ // This "input" event handler tracks changes made to preferences by
+ // the user in this window.
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="paneload">
+ <![CDATA[
+ // Initialize all values from preferences.
+ var elements = this.preferenceElements;
+ for (var i = 0; i < elements.length; ++i) {
+ try {
+ var preference = this.preferenceForElement(elements[i]);
+ preference.setElementValue(elements[i]);
+ } catch (e) {
+ dump("*** No preference found for " + elements[i].getAttribute("preference") + "\n");
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="panebutton" role="listitem"
+ extends="chrome://global/content/bindings/radio.xml#radio">
+ <content>
+ <xul:image class="paneButtonIcon" xbl:inherits="src"/>
+ <xul:label class="paneButtonLabel" xbl:inherits="value=label"/>
+ </content>
+ </binding>
+
+</bindings>
+
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#
+# This is PrefWindow 6. The Code Could Well Be Ready, Are You?
+#
+# Historical References:
+# PrefWindow V (February 1, 2003)
+# PrefWindow IV (April 24, 2000)
+# PrefWindow III (January 6, 2000)
+# PrefWindow II (???)
+# PrefWindow I (June 4, 1999)
+#
diff --git a/comm/suite/components/bindings/prefwindow.xml b/comm/suite/components/bindings/prefwindow.xml
new file mode 100644
index 0000000000..64173b6c76
--- /dev/null
+++ b/comm/suite/components/bindings/prefwindow.xml
@@ -0,0 +1,548 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+ SeaMonkey Extended Preferences Window Framework
+
+ The binding implemented here mostly works like its toolkit ancestor, with
+ one important difference: the <prefwindow> recognizes the first <tree> in
+ its content and assumes that this is the navigation tree:
+
+ <prefwindow>
+ <tree>
+ ...
+ <treeitem id="prefTreeItemA" prefpane="prefPaneA">
+ ...
+ </tree>
+ <prefpane id="prefPaneB">...</prefpane>
+ <prefpane id="prefPaneA">
+ </prefwindow>
+
+ The <tree> structure defines the hierarchical layout of the preference
+ window's navigation tree. A <treeitem>'s "prefpane" attribute references
+ one of the <prefpane>s given on the <prefwindow>'s main level.
+ All <prefpane>s not referenced by a <treeitem> will be appended to the
+ navigation tree's top level. <treeitem>s can be nested as needed, but
+ <treeitem>s without a related <prefpane> will be hidden.
+
+ Furthermore, if the <prefwindow> has attribute "autopanes" set to "true",
+ non-existing <prefpane>s will be generated automatically from certain
+ attributes of the <treeitem>:
+ - "url" must contain the <prefpane>'s url
+ - "prefpane" should contain the <prefpane>'s desired id,
+ otherwise its url will be used as id
+ - "helpTopic" may contain an index into SeaMonkey's help
+
+ Unlike in XPFE, where preferences panels were loaded into a separate
+ iframe, <prefpane>s are an integral part of the <prefwindow> document,
+ by virtue of loadOverlay. Hence <script>s will be loaded into the
+ <prefwindow> scope and possibly clash. To avoid this, <prefpane>s should
+ specify a "script" attribute with a whitespace delimited list of scripts
+ to load into the <prefpane>'s context. The subscriptloader will take care
+ of any internal scoping, so no this.* fest is necessary inside the script.
+
+ <prefwindow> users who want to share the very same file between SeaMonkey
+ and other toolkit apps should hide the <tree> (set <tree>.hidden=true);
+ this binding will then unhide the <tree> if necessary, ie more than just
+ one <prefpane> exists.
+ Also, the <tree> will get the class "prefnavtree" added, so that it may be
+ prestyled by the SeaMonkey themes.
+ Setting <prefwindow xpfe="false"> will enforce the application of just the
+ basic toolkit <prefwindow> even in SeaMonkey. The same "xpfe" attribute
+ exists for <prefpane>, too.
+-->
+
+<!DOCTYPE bindings [
+ <!ENTITY % dtdPrefs SYSTEM "chrome://communicator/locale/pref/preferences.dtd"> %dtdPrefs;
+ <!ENTITY % dtdGlobalPrefs SYSTEM "chrome://global/locale/preferences.dtd"> %dtdGlobalPrefs;
+]>
+
+<bindings id="prefwindowBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="prefwindow"
+ extends="chrome://communicator/content/bindings/preferences.xml#prefwindow">
+ <resources>
+ <stylesheet src="chrome://communicator/skin/preferences.css"/>
+ </resources>
+
+ <!-- The only difference between the following <content> and its toolkit
+ ancestor is the help button and the 'navTrees' <vbox> before the 'paneDeck'! -->
+ <content dlgbuttons="accept,cancel" persist="lastSelected screenX screenY"
+ closebuttonlabel="&preferencesCloseButton.label;"
+ closebuttonaccesskey="&preferencesCloseButton.accesskey;"
+ role="dialog">
+ <xul:radiogroup anonid="selector" orient="horizontal" class="paneSelector chromeclass-toolbar"
+ role="listbox"/> <!-- Expose to accessibility APIs as a listbox -->
+ <xul:hbox flex="1" class="paneDeckContainer">
+ <xul:vbox anonid="navTrees">
+ <children includes="tree"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:dialogheader anonid="paneHeader" hidden="true"/>
+ <xul:deck anonid="paneDeck" flex="1">
+ <children includes="prefpane"/>
+ </xul:deck>
+ </xul:vbox>
+ </xul:hbox>
+ <xul:hbox anonid="dlg-buttons" class="prefWindow-dlgbuttons" pack="end">
+#ifdef XP_UNIX
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/>
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1"/>
+ <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/>
+ <xul:button dlgtype="accept" class="dialog-button" icon="accept"/>
+#else
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1"/>
+ <xul:button dlgtype="accept" class="dialog-button" icon="accept"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/>
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+#endif
+ </xul:hbox>
+ <xul:hbox>
+ <children/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ // grab the first child tree and try to tie it to the prefpanes
+ var tree = this.getElementsByTagName('tree')[0];
+ this.initNavigationTree(tree);
+ // hide the toolkit pref strip if we have a tree
+ if (this._navigationTree)
+ this._selector.hidden = true;
+ ]]>
+ </constructor>
+
+ <field name="_navigationTree">null</field>
+
+ <!-- <prefwindow> users can call this method to exchange the <tree> -->
+ <method name="initNavigationTree">
+ <parameter name="aTreeElement"/>
+ <body>
+ <![CDATA[
+ this._navigationTree = null;
+ if (!aTreeElement)
+ return;
+
+ // don't grab trees in prefpanes etc.
+ if (aTreeElement.parentNode != this)
+ return;
+
+ // autogenerate <prefpane>s from <treecell>.url if requested
+ var autopanes = (this.getAttribute('autopanes') == 'true');
+ if (!autopanes)
+ {
+ // without autopanes, we can return early: don't bother
+ // with a navigation tree if we only have one prefpane
+ aTreeElement.hidden = (this.preferencePanes.length < 2);
+ if (aTreeElement.hidden)
+ return;
+ }
+
+ // ensure that we have a tree body
+ if (!aTreeElement.getElementsByTagName('treechildren').length)
+ aTreeElement.appendChild(document.createElement('treechildren'));
+
+ // ensure that we have a tree column
+ if (!aTreeElement.getElementsByTagName('treecol').length)
+ {
+ var navcols = document.createElement('treecols');
+ var navcol = document.createElement('treecol');
+ navcol.setAttribute('id', 'navtreecol');
+ navcol.setAttribute('primary', true);
+ navcol.setAttribute('flex', 1);
+ navcol.setAttribute('hideheader', true);
+ navcols.appendChild(navcol);
+ aTreeElement.appendChild(navcols);
+ aTreeElement.setAttribute('hidecolumnpicker', true);
+ }
+
+ // add the class "prefnavtree", so that themes can set defaults
+ aTreeElement.className += ' prefnavtree';
+
+ // Do some magic with the treeitem ingredient:
+ // - if it has a label attribute but no treerow child,
+ // generate a treerow with a treecell child with that label
+ // - if it has a prefpane attribute, tie it to that panel
+ // - if still no panel found and a url attribute is present,
+ // autogenerate the prefpane and connect to it
+ var treeitems = aTreeElement.getElementsByTagName('treeitem');
+ for (var i = 0; i < treeitems.length; ++i)
+ {
+ var node = treeitems[i];
+ var label = node.getAttribute('label');
+ if (label)
+ {
+ // autocreate the treecell?
+ var row = node.firstChild;
+ while (row && row.nodeName != 'treerow')
+ row = row.nextSibling;
+ if (!row)
+ {
+ var itemrow = document.createElement('treerow');
+ var itemcell = document.createElement('treecell');
+ itemcell.setAttribute('label', label);
+ itemrow.appendChild(itemcell);
+ node.appendChild(itemrow);
+ }
+ }
+ var paneID = node.getAttribute('prefpane');
+ var pane = paneID && document.getElementById(paneID);
+ if (!pane && autopanes)
+ {
+ // if we have a url, create a <prefpane> for it
+ var paneURL = node.getAttribute('url');
+ if (paneURL)
+ {
+ // reuse paneID if present, else use the url as id
+ pane = document.createElement('prefpane');
+ pane.setAttribute('id', paneID || paneURL);
+ pane.setAttribute('src', paneURL);
+ pane.setAttribute('label', label || paneID || paneURL);
+ var helpTopic = node.getAttribute('helpTopic');
+ if (helpTopic)
+ {
+ pane.setAttribute('helpURI', 'chrome://communicator/locale/help/suitehelp.rdf');
+ pane.setAttribute('helpTopic', helpTopic);
+ }
+ // add pane to prefwindow
+ this.appendChild(pane);
+ }
+ }
+ node.prefpane = pane;
+ if (pane)
+ pane.preftreeitem = node;
+ // hide unused treeitems
+ node.hidden = !pane;
+ }
+
+ // now that the number of <prefpane>s is known, try to return early:
+ // don't bother with a navigation tree if we only have one prefpane
+ aTreeElement.hidden = (this.preferencePanes.length < 2);
+ if (aTreeElement.hidden)
+ return;
+ this._navigationTree = aTreeElement;
+
+ // append any still unreferenced <prefpane>s to the tree's top level
+ for (var j = 0; j < this.preferencePanes.length; ++j)
+ {
+ // toolkit believes in fancy pane resizing - we don't
+ var lostpane = this.preferencePanes[j];
+ lostpane.setAttribute('flex', 1);
+
+ if (!("preftreeitem" in lostpane))
+ {
+ var treebody = this._navigationTree
+ .getElementsByTagName('treechildren')[0];
+ var treeitem = document.createElement('treeitem');
+ var treerow = document.createElement('treerow');
+ var treecell = document.createElement('treecell');
+ var label = lostpane.getAttribute('label');
+ if (!label)
+ label = lostpane.getAttribute('id');
+ treecell.setAttribute('label', label);
+ treerow.appendChild(treecell);
+ treeitem.appendChild(treerow);
+ treebody.appendChild(treeitem);
+ treeitem.prefpane = lostpane;
+ lostpane.preftreeitem = treeitem;
+ }
+ }
+
+ // Some parts of the toolkit base binding's initialization code (like
+ // panel select events) "fire" before we get here. Thus, we may need
+ // to sync the tree manually now (again), if we added any panels or
+ // if toolkit failed to select one.
+ // (This is a loose copy from the toolkit ctor.)
+ var lastPane = this.lastSelected &&
+ document.getElementById(this.lastSelected);
+ if (!lastPane)
+ this.lastSelected = "";
+ if ("arguments" in window && window.arguments[0])
+ {
+ var initialPane = document.getElementById(window.arguments[0]);
+ if (initialPane && initialPane.nodeName == "prefpane")
+ {
+ this.currentPane = initialPane;
+ this.lastSelected = initialPane.id;
+ }
+ }
+ else if (lastPane)
+ this.currentPane = lastPane;
+ try
+ {
+ this.showPane(this.currentPane); // may need to load it first
+ this.syncTreeWithPane(this.currentPane, true);
+ }
+ catch (e)
+ {
+ dump('***** broken prefpane: ' + this.currentPane.id + '\n' + e + '\n');
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- don't do any fancy animations -->
+ <property name="_shouldAnimate" onget="return false;"/>
+
+ <method name="setPaneTitle">
+ <parameter name="aPaneElement"/>
+ <body>
+#ifndef XP_MACOSX
+ <![CDATA[
+ // show pane title, if given
+ var paneHeader = document.getAnonymousElementByAttribute(this, 'anonid', 'paneHeader');
+ var paneHeaderLabel = '';
+ if (aPaneElement)
+ paneHeaderLabel = aPaneElement.getAttribute('label');
+ paneHeader.hidden = !paneHeaderLabel;
+ if (!paneHeader.hidden)
+ paneHeader.setAttribute('title', paneHeaderLabel);
+ ]]>
+#endif
+ </body>
+ </method>
+
+ <method name="syncPaneWithTree">
+ <parameter name="aTreeIndex"/>
+ <body>
+ <![CDATA[
+ var pane = null;
+ if ((this._navigationTree) && (aTreeIndex >= 0))
+ {
+ // load the prefpane associated with this treeitem
+ var treeitem = this._navigationTree.contentView
+ .getItemAtIndex(aTreeIndex);
+ if ('prefpane' in treeitem)
+ {
+ pane = treeitem.prefpane;
+ if (pane && (this.currentPane != pane))
+ {
+ try
+ {
+ this.showPane(pane); // may need to load it first
+ }
+ catch (e)
+ {
+ dump('***** broken prefpane: ' + pane.id + '\n' + e + '\n');
+ pane = null;
+ }
+ }
+ }
+ }
+ // don't show broken panels
+ this._paneDeck.hidden = (pane == null);
+ this.setPaneTitle(pane);
+ ]]>
+ </body>
+ </method>
+
+ <method name="syncTreeWithPane">
+ <parameter name="aPane"/>
+ <parameter name="aExpand"/>
+ <body>
+ <![CDATA[
+ if (this._navigationTree && aPane)
+ {
+ if ('preftreeitem' in aPane)
+ {
+ // make sure the treeitem is visible
+ var container = aPane.preftreeitem;
+ if (!aExpand)
+ container = container.parentNode.parentNode;
+ while (container != this._navigationTree)
+ {
+ container.setAttribute('open', true);
+ container = container.parentNode.parentNode;
+ }
+
+ // mark selected pane in navigation tree
+ var index = this._navigationTree.contentView
+ .getIndexOfItem(aPane.preftreeitem);
+ this._navigationTree.view.selection.select(index);
+ }
+ }
+ this.setPaneTitle(aPane);
+ if (this.getAttribute("overflow") != "auto")
+ {
+ if (this.scrollHeight > window.innerHeight)
+ window.innerHeight = this.scrollHeight;
+ if (this.scrollWidth > window.innerWidth)
+ window.innerWidth = this.scrollWidth;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- copied from contextHelp.js
+ Locate existing help window for this helpFileURI. -->
+ <method name="locateHelpWindow">
+ <parameter name="helpFileURI"/>
+ <body>
+ <![CDATA[
+ const iterator = Services.wm.getEnumerator("suite:help");
+ var topWindow = null;
+ var aWindow;
+
+ // Loop through help windows looking for one with selected helpFileURI
+ while (iterator.hasMoreElements())
+ {
+ aWindow = iterator.getNext();
+ if (aWindow.closed)
+ continue;
+ if (aWindow.getHelpFileURI() == helpFileURI)
+ topWindow = aWindow;
+ }
+ return topWindow;
+ ]]>
+ </body>
+ </method>
+
+ <!-- copied from contextHelp.js
+ Opens up the Help Viewer with the specified topic and helpFileURI. -->
+ <method name="openHelp">
+ <parameter name="topic"/>
+ <parameter name="helpFileURI"/>
+ <body>
+ <![CDATA[
+ // Empty help windows are not helpful...
+ if (!helpFileURI)
+ return;
+
+ // Try to find previously opened help.
+ var topWindow = this.locateHelpWindow(helpFileURI);
+ if (topWindow)
+ {
+ // Open topic in existing window.
+ topWindow.focus();
+ topWindow.displayTopic(topic);
+ }
+ else
+ {
+ // Open topic in new window.
+ const params = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+ params.SetNumberStrings(2);
+ params.SetString(0, helpFileURI);
+ params.SetString(1, topic);
+ Services.ww.openWindow(null,
+ "chrome://help/content/help.xul",
+ "_blank",
+ "chrome,all,alwaysRaised,dialog=no",
+ params);
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="dialoghelp">
+ <![CDATA[
+ this.openHelp(this.currentPane.helpTopic, this.currentPane.getAttribute("helpURI"));
+ ]]>
+ </handler>
+ <handler event="select">
+ <![CDATA[
+ // navigation tree select or deck change?
+ var target = event.originalTarget;
+ if (target == this._navigationTree)
+ {
+ this.syncPaneWithTree(target.currentIndex);
+ }
+ else if (target == this._paneDeck)
+ {
+ // deck.selectedIndex is a string!
+ var pane = this.preferencePanes[Number(target.selectedIndex)];
+ this.syncTreeWithPane(pane, false);
+ }
+ ]]>
+ </handler>
+
+ <handler event="paneload">
+ <![CDATA[
+ // panes may load asynchronously,
+ // so we have to "late-sync" those to our navigation tree
+ this.syncTreeWithPane(event.originalTarget, false);
+ ]]>
+ </handler>
+
+ <handler event="keypress" key="&focusSearch.key;" modifiers="accel">
+ <![CDATA[
+ var searchBox = this.currentPane.getElementsByAttribute("type", "search")[0];
+ if (searchBox)
+ {
+ searchBox.focus();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="prefpane"
+ extends="chrome://communicator/content/bindings/preferences.xml#prefpane">
+ <resources>
+ <stylesheet src="chrome://communicator/skin/preferences.css"/>
+ </resources>
+
+ <handlers>
+ <handler event="paneload">
+ <![CDATA[
+ // Since all <prefpane>s now share the same global document, their
+ // <script>s might clash. Thus we expect the "script" attribute to
+ // contain a whitespace delimited list of script files to be loaded
+ // into the <prefpane>'s context.
+
+ // list of scripts to load
+ var scripts = this.getAttribute('script').match(/\S+/g);
+ if (!scripts)
+ return;
+ var count = scripts.length;
+ for (var i = 0; i < count; ++i)
+ {
+ var script = scripts[i];
+ if (script)
+ {
+ try
+ {
+ Services.scriptloader.loadSubScript(script, this);
+ }
+ catch (e)
+ {
+ let errorStr =
+ "prefpane.paneload: loadSubScript(" + script + ") failed:\n" +
+ (e.fileName ? "at " + e.fileName + " : " + e.lineNumber + "\n"
+ : "") +
+ e + " - " + e.stack + "\n";
+ dump(errorStr);
+ Cu.reportError(errorStr);
+ }
+ }
+ }
+
+ // if we have a Startup method, call it
+ if ('Startup' in this)
+ this.Startup();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/bindings/spinbuttons.xml b/comm/suite/components/bindings/spinbuttons.xml
new file mode 100644
index 0000000000..90c249c9b2
--- /dev/null
+++ b/comm/suite/components/bindings/spinbuttons.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="spinbuttonsBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="spinbuttons"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <content>
+ <xul:vbox class="spinbuttons-box" flex="1">
+ <xul:button anonid="increaseButton" type="repeat" flex="1"
+ class="spinbuttons-button spinbuttons-up"
+ xbl:inherits="disabled,disabled=increasedisabled"/>
+ <xul:button anonid="decreaseButton" type="repeat" flex="1"
+ class="spinbuttons-button spinbuttons-down"
+ xbl:inherits="disabled,disabled=decreasedisabled"/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <property name="_increaseButton" readonly="true">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "increaseButton");
+ </getter>
+ </property>
+ <property name="_decreaseButton" readonly="true">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "decreaseButton");
+ </getter>
+ </property>
+
+ <property name="increaseDisabled"
+ onget="return this._increaseButton.getAttribute('disabled') == 'true';"
+ onset="if (val) this._increaseButton.setAttribute('disabled', 'true');
+ else this._increaseButton.removeAttribute('disabled'); return val;"/>
+ <property name="decreaseDisabled"
+ onget="return this._decreaseButton.getAttribute('disabled') == 'true';"
+ onset="if (val) this._decreaseButton.setAttribute('disabled', 'true');
+ else this._decreaseButton.removeAttribute('disabled'); return val;"/>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown">
+ <![CDATA[
+ // on the Mac, the native theme draws the spinbutton as a single widget
+ // so a state attribute is set based on where the mouse button was pressed
+ if (event.originalTarget == this._increaseButton)
+ this.setAttribute("state", "up");
+ else if (event.originalTarget == this._decreaseButton)
+ this.setAttribute("state", "down");
+ ]]>
+ </handler>
+
+ <handler event="mouseup">
+ this.removeAttribute("state");
+ </handler>
+ <handler event="mouseout">
+ this.removeAttribute("state");
+ </handler>
+
+ <handler event="command">
+ <![CDATA[
+ var eventname;
+ if (event.originalTarget == this._increaseButton)
+ eventname = "up";
+ else if (event.originalTarget == this._decreaseButton)
+ eventname = "down";
+
+ var evt = document.createEvent("Events");
+ evt.initEvent(eventname, true, true);
+ var cancel = this.dispatchEvent(evt);
+
+ if (this.hasAttribute("on" + eventname)) {
+ var fn = new Function("event", this.getAttribute("on" + eventname));
+ if (!fn.call(this, event))
+ cancel = true;
+ }
+
+ return !cancel;
+ ]]>
+ </handler>
+
+ </handlers>
+ </binding>
+
+</bindings> \ No newline at end of file
diff --git a/comm/suite/components/bindings/textbox.xml b/comm/suite/components/bindings/textbox.xml
new file mode 100644
index 0000000000..74893c29a8
--- /dev/null
+++ b/comm/suite/components/bindings/textbox.xml
@@ -0,0 +1,251 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This files relies on these specific Chrome/XBL globals -->
+<!-- globals ChromeWindow -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
+ %textcontextDTD;
+]>
+
+<bindings id="textboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="textbox">
+
+ <content>
+ <children/>
+ <xul:moz-input-box class="textbox-input-box" flex="1"
+ xbl:inherits="context,spellcheck">
+ <html:input class="textbox-input" anonid="input"
+ xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,noinitialfocus,mozactionhint,spellcheck"/>
+ </xul:moz-input-box>
+ </content>
+
+ <implementation implements="nsIDOMXULLabeledControlElement">
+ <!-- nsIDOMXULLabeledControlElement -->
+ <field name="crop">""</field>
+ <field name="image">""</field>
+ <field name="command">""</field>
+ <field name="accessKey">""</field>
+
+ <field name="mInputField">null</field>
+ <field name="mIgnoreClick">false</field>
+ <field name="mIgnoreFocus">false</field>
+ <field name="mEditor">null</field>
+
+ <property name="inputField" readonly="true">
+ <getter><![CDATA[
+ if (!this.mInputField)
+ this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
+ return this.mInputField;
+ ]]></getter>
+ </property>
+
+ <property name="value" onset="this.inputField.value = val; return val;"
+ onget="return this.inputField.value;"/>
+ <property name="defaultValue" onset="this.inputField.defaultValue = val; return val;"
+ onget="return this.inputField.defaultValue;"/>
+ <property name="label" onset="this.setAttribute('label', val); return val;"
+ onget="return this.getAttribute('label') ||
+ (this.labelElement ? this.labelElement.value :
+ this.placeholder);"/>
+ <property name="placeholder" onset="this.inputField.placeholder = val; return val;"
+ onget="return this.inputField.placeholder;"/>
+ <property name="emptyText" onset="this.placeholder = val; return val;"
+ onget="return this.placeholder;"/>
+ <property name="type" onset="if (val) this.setAttribute('type', val);
+ else this.removeAttribute('type'); return val;"
+ onget="return this.getAttribute('type');"/>
+ <property name="maxLength" onset="this.inputField.maxLength = val; return val;"
+ onget="return this.inputField.maxLength;"/>
+ <property name="disabled" onset="this.inputField.disabled = val;
+ if (val) this.setAttribute('disabled', 'true');
+ else this.removeAttribute('disabled'); return val;"
+ onget="return this.inputField.disabled;"/>
+ <property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex'));"
+ onset="this.inputField.tabIndex = val;
+ if (val) this.setAttribute('tabindex', val);
+ else this.removeAttribute('tabindex'); return val;"/>
+ <property name="size" onset="this.inputField.size = val; return val;"
+ onget="return this.inputField.size;"/>
+ <property name="readOnly" onset="this.inputField.readOnly = val;
+ if (val) this.setAttribute('readonly', 'true');
+ else this.removeAttribute('readonly'); return val;"
+ onget="return this.inputField.readOnly;"/>
+ <property name="clickSelectsAll"
+ onget="return this.getAttribute('clickSelectsAll') == 'true';"
+ onset="if (val) this.setAttribute('clickSelectsAll', 'true');
+ else this.removeAttribute('clickSelectsAll'); return val;" />
+
+ <property name="editor" readonly="true">
+ <getter><![CDATA[
+ if (!this.mEditor) {
+ this.mEditor = this.inputField.editor;
+ }
+ return this.mEditor;
+ ]]></getter>
+ </property>
+
+ <method name="reset">
+ <body><![CDATA[
+ this.value = this.defaultValue;
+ if (!this.editor) {
+ return false;
+ }
+ this.editor.clearUndoRedo();
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="select">
+ <body>
+ this.inputField.select();
+ </body>
+ </method>
+
+ <property name="controllers" readonly="true" onget="return this.inputField.controllers"/>
+ <property name="textLength" readonly="true"
+ onget="return this.inputField.textLength;"/>
+ <property name="selectionStart" onset="this.inputField.selectionStart = val; return val;"
+ onget="return this.inputField.selectionStart;"/>
+ <property name="selectionEnd" onset="this.inputField.selectionEnd = val; return val;"
+ onget="return this.inputField.selectionEnd;"/>
+
+ <method name="setSelectionRange">
+ <parameter name="aSelectionStart"/>
+ <parameter name="aSelectionEnd"/>
+ <body>
+ this.inputField.setSelectionRange(aSelectionStart, aSelectionEnd);
+ </body>
+ </method>
+
+ <method name="_setNewlineHandling">
+ <body><![CDATA[
+ var str = this.getAttribute("newlines");
+ if (str && this.editor) {
+ for (let x in Ci.nsIEditor) {
+ if (/^eNewlines/.test(x)) {
+ if (str == RegExp.rightContext.toLowerCase()) {
+ this.editor.newlineHandling = Ci.nsIEditor[x];
+ break;
+ }
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_maybeSelectAll">
+ <body><![CDATA[
+ if (!this.mIgnoreClick && this.clickSelectsAll &&
+ document.activeElement == this.inputField &&
+ this.inputField.selectionStart == this.inputField.selectionEnd)
+ this.editor.selectAll();
+ ]]></body>
+ </method>
+
+ <constructor><![CDATA[
+ var str = this.boxObject.getProperty("value");
+ if (str) {
+ this.inputField.value = str;
+ this.boxObject.removeProperty("value");
+ }
+
+ this._setNewlineHandling();
+
+ if (this.hasAttribute("emptytext"))
+ this.placeholder = this.getAttribute("emptytext");
+ ]]></constructor>
+
+ <destructor>
+ <![CDATA[
+ var field = this.inputField;
+ if (field && field.value)
+ this.boxObject.setProperty("value", field.value);
+ this.mInputField = null;
+ ]]>
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ if (this.hasAttribute("focused"))
+ return;
+
+ switch (event.originalTarget) {
+ case this:
+ // Forward focus to actual HTML input
+ this.inputField.focus();
+ break;
+ case this.inputField:
+ if (this.mIgnoreFocus) {
+ this.mIgnoreFocus = false;
+ } else if (this.clickSelectsAll) {
+ try {
+ if (!this.editor || !this.editor.composing)
+ this.editor.selectAll();
+ } catch (e) {}
+ }
+ break;
+ default:
+ // Allow other children (e.g. URL bar buttons) to get focus
+ return;
+ }
+ this.setAttribute("focused", "true");
+ ]]>
+ </handler>
+
+ <handler event="blur" phase="capturing">
+ <![CDATA[
+ this.removeAttribute("focused");
+
+ // don't trigger clickSelectsAll when switching application windows
+ if (window == window.top &&
+ window.isChromeWindow &&
+ document.activeElement == this.inputField)
+ this.mIgnoreFocus = true;
+ ]]>
+ </handler>
+
+ <handler event="mousedown">
+ <![CDATA[
+ this.mIgnoreClick = this.hasAttribute("focused");
+
+ if (!this.mIgnoreClick) {
+ this.mIgnoreFocus = true;
+ this.inputField.setSelectionRange(0, 0);
+ if (event.originalTarget == this ||
+ event.originalTarget == this.inputField.parentNode)
+ this.inputField.focus();
+ }
+ ]]>
+ </handler>
+
+ <handler event="click" action="this._maybeSelectAll();"/>
+
+#ifndef XP_WIN
+ <handler event="contextmenu">
+ // Only care about context clicks on the textbox itself.
+ if (event.target != this)
+ return;
+
+ if (!event.button) // context menu opened via keyboard shortcut
+ return;
+ this._maybeSelectAll();
+ // see bug 576135 comment 4
+ let box = this.inputField.parentNode;
+ box._doPopupItemEnabling(box.menupopup);
+ </handler>
+#endif
+ </handlers>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/bindings/toolbar-xpfe.xml b/comm/suite/components/bindings/toolbar-xpfe.xml
new file mode 100644
index 0000000000..d489a2eb73
--- /dev/null
+++ b/comm/suite/components/bindings/toolbar-xpfe.xml
@@ -0,0 +1,333 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="toolbarBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- With the move to the new toolkit, SeaMonkey needs to overwrite certain
+ bindings if it wants to keep its distinctive likeness. The bindings
+ found here reimplement the XPFE toolbar behaviour by providing toolbar
+ bindings in in the chrome://communicator/ domain that are based upon the
+ new toolkit's toolbar bindings in the chrome://global/ domain.
+ The now hidden new toolkit bindings are accessible via a set xpfe="false"
+ attribute, though.
+ -->
+
+ <binding id="grippytoolbox" extends="chrome://communicator/content/bindings/toolbar.xml#toolbox">
+ <content orient="vertical">
+ <xul:vbox flex="1" class="toolbar-internal-box">
+ <children/>
+ </xul:vbox>
+ <xul:hbox tbattr="collapsed-tray-holder" class="collapsed-tray-holder" moz-collapsed="true" xbl:inherits="collapsed=inFullscreen">
+ <xul:hbox tbattr="collapsed-tray" class="collapsed-tray"/>
+ <xul:spacer flex="1" class="collapsed-tray-spacer"/>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <field name="palette">
+ this.getElementsByTagName("toolbarpalette").item(0);
+ </field>
+
+ <constructor>
+ <![CDATA[
+ var set = this.toolbarset;
+ if (!set)
+ return;
+ var toolbars = this.getElementsByAttribute("customindex", "*");
+ for (let i = 0; i < toolbars.length; ++i) {
+ let bar = toolbars[i];
+ let name = bar.getAttribute("toolbarname").replace(" ", "_");
+ if (name) {
+ let attrs = ["mode", "iconsize", "labelalign", "hidden",
+ "collapsed", "moz-collapsed"];
+ for (let j = 0; j < attrs.length; j++) {
+ let attr = set.getAttribute(name + attrs[j]);
+ if (attr)
+ bar.setAttribute(attrs[j], attr);
+ }
+ bar.setAttribute("grippytooltiptext", name);
+ }
+ }
+ ]]>
+ </constructor>
+
+ <method name="collapseToolbar">
+ <parameter name="toolbar"/>
+ <body>
+ <![CDATA[
+ try {
+ this.createCollapsedGrippy(toolbar);
+ toolbar.setAttribute("collapsed", "true");
+ document.persist(toolbar.id, "collapsed");
+ toolbar.removeAttribute("moz-collapsed");
+ document.persist(toolbar.id, "moz-collapsed");
+ if (toolbar.hasAttribute("customindex"))
+ this.persistCustomCollapse(toolbar, "true");
+ }
+ catch(e) {
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="expandToolbar">
+ <parameter name="aGrippyID"/>
+ <body>
+ <![CDATA[
+ var idString = aGrippyID.substring("moz_tb_collapsed_".length, aGrippyID.length);
+ var toolbar = document.getElementById(idString);
+ toolbar.setAttribute("collapsed", "false");
+ var collapsedTray = document.getAnonymousElementByAttribute(this, "tbattr", "collapsed-tray");
+ var collapsedToolbar = document.getElementById("moz_tb_collapsed_" + toolbar.id);
+ collapsedToolbar.remove();
+ if (!collapsedTray.hasChildNodes())
+ document.getAnonymousElementByAttribute(this, "tbattr", "collapsed-tray-holder").setAttribute("moz-collapsed", "true");
+ document.persist(toolbar.id, "collapsed");
+ if (toolbar.hasAttribute("customindex"))
+ this.persistCustomCollapse(toolbar, "false");
+ ]]>
+ </body>
+ </method>
+
+ <method name="createCollapsedGrippy">
+ <parameter name="aToolbar"/>
+ <body>
+ <![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var existingGrippy = document.getAnonymousElementByAttribute(this, "id", "moz_tb_collapsed_" + aToolbar.id);
+ if (!existingGrippy) {
+ var grippy = document.getAnonymousElementByAttribute(aToolbar, "tbattr", "toolbar-grippy");
+ var boxObject = grippy.boxObject.QueryInterface(Ci.nsIBoxObject);
+ var collapsedGrippy = document.createElementNS(XUL_NS, "toolbargrippy");
+ if (collapsedGrippy) {
+ var width = boxObject.height > 20 ? boxObject.height : 23;
+ var height = boxObject.width > 10 ? boxObject.width : 12;
+ var styleString = "width: " + width + "px; height: " + height + "px;";
+ collapsedGrippy.setAttribute("style", styleString);
+ collapsedGrippy.setAttribute("tooltiptext", aToolbar.getAttribute("grippytooltiptext"));
+ collapsedGrippy.setAttribute("id", "moz_tb_collapsed_" + aToolbar.id);
+ collapsedGrippy.setAttribute("moz_grippy_collapsed", "true");
+ collapsedGrippy.setAttribute("tbgrippy-collapsed", "true");
+ var collapsedTrayHolder = document.getAnonymousElementByAttribute(this, "tbattr", "collapsed-tray-holder");
+ if (collapsedTrayHolder.getAttribute("moz-collapsed") == "true")
+ collapsedTrayHolder.removeAttribute("moz-collapsed");
+ document.getAnonymousElementByAttribute(this, "tbattr", "collapsed-tray").appendChild(collapsedGrippy);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="persistCustomCollapse">
+ <parameter name="toolbar"/>
+ <parameter name="collapsed"/>
+ <body>
+ <![CDATA[
+ var attr = toolbar.getAttribute("toolbarname").replace(" ", "_") + "collapsed";
+ this.toolbarset.setAttribute(attr, collapsed);
+ document.persist(this.toolbarset.id, attr);
+ var attr = toolbar.getAttribute("toolbarname").replace(" ", "_") + "moz-collapsed";
+ this.toolbarset.removeAttribute(attr);
+ document.persist(this.toolbarset.id, attr);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="grippytoolbar" extends="chrome://communicator/content/bindings/toolbar.xml#toolbar">
+ <content>
+ <xul:hbox flex="1" class="toolbar-box box-inherit">
+ <xul:toolbargrippy xbl:inherits="last-toolbar,hidden=grippyhidden,collapsed=inFullscreen"
+ tbattr="toolbar-grippy"
+ class="toolbar-grippy"/>
+ <xul:hbox flex="1" class="toolbar-holder box-inherit"
+ xbl:inherits="collapsed,last-toolbar,orient,align,pack">
+ <children/>
+ </xul:hbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ if (Services.prefs.getBoolPref("browser.toolbars.grippyhidden")) {
+ this.setAttribute("grippyhidden", "true");
+ }
+
+ if (this.getAttribute("moz-collapsed") == "true" &&
+ this.parentNode.localName == "toolbox")
+ this.parentNode.collapseToolbar(this);
+ else if (this.getAttribute("collapsed") == "true" &&
+ this.parentNode.localName == "toolbox")
+ this.parentNode.createCollapsedGrippy(this);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="grippytoolbar-primary" extends="chrome://communicator/content/bindings/toolbar-xpfe.xml#grippytoolbar">
+ <implementation implements="nsIObserver">
+ <constructor>
+ <![CDATA[
+ this.prefs.addObserver(this.domain, this);
+ if (this.prefs.getIntPref(this.domain) != 2)
+ this.observe(this.prefs, "nsPref:changed", this.domain);
+ ]]>
+ </constructor>
+
+ <destructor>
+ this.prefs.removeObserver(this.domain, this);
+ </destructor>
+
+ <field name="domain" readonly="true">
+ "browser.chrome.toolbar_style"
+ </field>
+
+ <field name="prefs" readonly="true">
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ </field>
+
+ <method name="observe">
+ <parameter name="subject"/>
+ <parameter name="topic"/>
+ <parameter name="name"/>
+ <body>
+ <![CDATA[
+ if (topic == "nsPref:changed" && name == this.domain) {
+ const styles = ["icons", "text", "full"];
+ const style = styles[this.prefs.getIntPref(name)];
+ this.parentNode.setAttribute("mode", style); // toolbox
+ if (!this.hasAttribute("customizable") ||
+ !this.hasAttribute("ignoremodepref"))
+ this.setAttribute("mode", style);
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="grippytoolbar-drag"
+ extends="chrome://communicator/content/bindings/toolbar-xpfe.xml#grippytoolbar">
+ <implementation>
+ <field name="_dragBindingAlive">true</field>
+ <constructor>
+ <![CDATA[
+ if (!this._draggableStarted) {
+ this._draggableStarted = true;
+ try {
+ let tmp = {};
+ ChromeUtils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
+ let draggableThis = new tmp.WindowDraggingElement(this);
+ draggableThis.mouseDownCheck = function(e) {
+ // Don't move while customizing.
+ return this._dragBindingAlive &&
+ this.getAttribute("customizing") != "true";
+ }
+ } catch (e) {}
+ }
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="grippytoolbar-menubar"
+ extends="chrome://communicator/content/bindings/toolbar-xpfe.xml#grippytoolbar"
+ display="xul:menubar"/>
+
+ <binding id="grippymenubar" extends="chrome://communicator/content/bindings/toolbar.xml#menubar">
+ <content>
+ <xul:hbox flex="1" class="toolbar-box">
+ <xul:toolbargrippy xbl:inherits="last-toolbar,hidden=grippyhidden"
+ tbattr="toolbar-grippy" class="toolbar-grippy"/>
+ <xul:hbox flex="1" class="toolbar-holder" xbl:inherits="collapsed,last-toolbar">
+ <children/>
+ </xul:hbox>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <constructor>
+ <![CDATA[
+ if (Services.prefs.getBoolPref("browser.toolbars.grippyhidden")) {
+ this.setAttribute("grippyhidden", "true");
+ }
+
+ if (this.getAttribute("moz-collapsed") == "true" &&
+ this.parentNode.localName == "toolbox")
+ this.parentNode.collapseToolbar(this);
+ else if (this.getAttribute("collapsed") == "true" &&
+ this.parentNode.localName == "toolbox")
+ this.parentNode.createCollapsedGrippy(this);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="toolbargrippy" display="xul:button"
+ extends="chrome://communicator/content/bindings/toolbar.xml#toolbar-base">
+ <content>
+ <xul:image class="toolbargrippy-arrow"/>
+ <xul:spacer class="toolbargrippy-texture" flex="1"/>
+ </content>
+
+ <implementation>
+ <property name="collapsed">
+ <getter>
+ return this.hasAttribute("moz_grippy_collapsed");
+ </getter>
+ <setter>
+ if (val)
+ this.setAttribute("moz_grippy_collapsed", "true");
+ else
+ this.removeAttribute("moz_grippy_collapsed");
+ return val;
+ </setter>
+ </property>
+
+ <method name="returnNode">
+ <parameter name="aNodeA"/>
+ <parameter name="aNodeB"/>
+ <body>
+ <![CDATA[
+ var node = this.parentNode;
+ while (node && node.localName != "window" &&
+ (node.localName != aNodeA && (node.localName != aNodeB))) {
+ node = node.parentNode;
+ }
+ return node;
+ ]]>
+ </body>
+ </method>
+
+ <method name="grippyTriggered">
+ <body>
+ <![CDATA[
+ var toolbox = this.returnNode("toolbox");
+ var toolbar = this.returnNode("toolbar", "menubar");
+ if (this.collapsed)
+ toolbox.expandToolbar(this.id);
+ else
+ toolbox.collapseToolbar(toolbar);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ this.grippyTriggered();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
+
diff --git a/comm/suite/components/bindings/toolbar.xml b/comm/suite/components/bindings/toolbar.xml
new file mode 100644
index 0000000000..cf36b421ad
--- /dev/null
+++ b/comm/suite/components/bindings/toolbar.xml
@@ -0,0 +1,579 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="toolbarBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="toolbox">
+ <implementation>
+ <field name="palette">
+ null
+ </field>
+
+ <field name="toolbarset">
+ null
+ </field>
+
+ <field name="customToolbarCount">
+ 0
+ </field>
+
+ <field name="externalToolbars">
+ []
+ </field>
+
+ <!-- Set by customizeToolbar.js -->
+ <property name="customizing">
+ <getter><![CDATA[
+ return this.getAttribute("customizing") == "true";
+ ]]></getter>
+ <setter><![CDATA[
+ if (val)
+ this.setAttribute("customizing", "true");
+ else
+ this.removeAttribute("customizing");
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ // Look to see if there is a toolbarset.
+ this.toolbarset = this.firstChild;
+ while (this.toolbarset && this.toolbarset.localName != "toolbarset")
+ this.toolbarset = this.toolbarset.nextSibling;
+
+ if (this.toolbarset) {
+ // Create each toolbar described by the toolbarset.
+ var index = 0;
+ while (this.toolbarset.hasAttribute("toolbar" + (++index))) {
+ var toolbarInfo = this.toolbarset.getAttribute("toolbar" + index);
+ var infoSplit = toolbarInfo.split(":");
+ this.appendCustomToolbar(infoSplit[0], infoSplit[1]);
+ }
+ }
+ ]]>
+ </constructor>
+
+ <method name="appendCustomToolbar">
+ <parameter name="aName"/>
+ <parameter name="aCurrentSet"/>
+ <body>
+ <![CDATA[
+ if (!this.toolbarset)
+ return null;
+ var toolbar = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "toolbar");
+ toolbar.id = "__customToolbar_" + aName.replace(" ", "_");
+ toolbar.setAttribute("customizable", "true");
+ toolbar.setAttribute("customindex", ++this.customToolbarCount);
+ toolbar.setAttribute("toolbarname", aName);
+ toolbar.setAttribute("currentset", aCurrentSet);
+ toolbar.setAttribute("mode", this.getAttribute("mode"));
+ toolbar.setAttribute("iconsize", this.getAttribute("iconsize"));
+ toolbar.setAttribute("context", this.toolbarset.getAttribute("context"));
+ toolbar.setAttribute("class", "chromeclass-toolbar");
+
+ this.insertBefore(toolbar, this.toolbarset);
+ return toolbar;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="toolbar" role="xul:toolbar">
+ <implementation>
+ <property name="toolbarName"
+ onget="return this.getAttribute('toolbarname');"
+ onset="this.setAttribute('toolbarname', val); return val;"/>
+
+ <field name="_toolbox">null</field>
+ <property name="toolbox" readonly="true">
+ <getter><![CDATA[
+ if (this._toolbox)
+ return this._toolbox;
+
+ let toolboxId = this.getAttribute("toolboxid");
+ if (toolboxId) {
+ let toolbox = document.getElementById(toolboxId);
+ if (!toolbox) {
+ let tbName = this.toolbarName;
+ if (tbName)
+ tbName = " (" + tbName + ")";
+ else
+ tbName = "";
+ throw new Error(`toolbar ID ${this.id}${tbName}: toolboxid attribute '${toolboxId}' points to a toolbox that doesn't exist`);
+ }
+
+ if (!toolbox.externalToolbars.includes(this))
+ toolbox.externalToolbars.push(this);
+
+ return this._toolbox = toolbox;
+ }
+
+ return this._toolbox = (this.parentNode &&
+ this.parentNode.localName == "toolbox") ?
+ this.parentNode : null;
+ ]]></getter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ if (document.readyState == "complete") {
+ this._init();
+ } else {
+ // Need to wait until XUL overlays are loaded. See bug 554279.
+ let self = this;
+ document.addEventListener("readystatechange", function listener(event) {
+ if (document.readyState != "complete")
+ return;
+ document.removeEventListener("readystatechange", listener);
+ self._init();
+ });
+ }
+ ]]>
+ </constructor>
+
+ <method name="_init">
+ <body>
+ <![CDATA[
+ // Searching for the toolbox palette in the toolbar binding because
+ // toolbars are constructed first.
+ var toolbox = this.toolbox;
+ if (!toolbox)
+ return;
+
+ if (!toolbox.palette) {
+ // Look to see if there is a toolbarpalette.
+ var node = toolbox.firstChild;
+ while (node) {
+ if (node.localName == "toolbarpalette")
+ break;
+ node = node.nextSibling;
+ }
+
+ if (!node)
+ return;
+
+ // Hold on to the palette but remove it from the document.
+ toolbox.palette = node;
+ toolbox.removeChild(node);
+ }
+
+ // Build up our contents from the palette.
+ var currentSet = this.getAttribute("currentset");
+ if (!currentSet)
+ currentSet = this.getAttribute("defaultset");
+ if (currentSet)
+ this.currentSet = currentSet;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_idFromNode">
+ <parameter name="aNode"/>
+ <body>
+ <![CDATA[
+ if (aNode.getAttribute("skipintoolbarset") == "true")
+ return "";
+
+ switch (aNode.localName) {
+ case "toolbarseparator":
+ return "separator";
+ case "toolbarspring":
+ return "spring";
+ case "toolbarspacer":
+ return "spacer";
+ default:
+ return aNode.id;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentSet">
+ <getter>
+ <![CDATA[
+ var node = this.firstChild;
+ var currentSet = [];
+ while (node) {
+ var id = this._idFromNode(node);
+ if (id) {
+ currentSet.push(id);
+ }
+ node = node.nextSibling;
+ }
+
+ return currentSet.join(",") || "__empty";
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val == this.currentSet)
+ return val;
+
+ var ids = (val == "__empty") ? [] : val.split(",");
+
+ var nodeidx = 0;
+ var paletteItems = { }, added = { };
+
+ var palette = this.toolbox ? this.toolbox.palette : null;
+
+ // build a cache of items in the toolbarpalette
+ var paletteChildren = palette ? palette.childNodes : [];
+ for (let c = 0; c < paletteChildren.length; c++) {
+ let curNode = paletteChildren[c];
+ paletteItems[curNode.id] = curNode;
+ }
+
+ var children = this.childNodes;
+
+ // iterate over the ids to use on the toolbar
+ for (let i = 0; i < ids.length; i++) {
+ let id = ids[i];
+ // iterate over the existing nodes on the toolbar. nodeidx is the
+ // spot where we want to insert items.
+ let found = false;
+ for (let c = nodeidx; c < children.length; c++) {
+ let curNode = children[c];
+ if (this._idFromNode(curNode) == id) {
+ // the node already exists. If c equals nodeidx, we haven't
+ // iterated yet, so the item is already in the right position.
+ // Otherwise, insert it here.
+ if (c != nodeidx) {
+ this.insertBefore(curNode, children[nodeidx]);
+ }
+
+ added[curNode.id] = true;
+ nodeidx++;
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ // move on to the next id
+ continue;
+ }
+
+ // the node isn't already on the toolbar, so add a new one.
+ var nodeToAdd = paletteItems[id] || this._getToolbarItem(id);
+ if (nodeToAdd && !(nodeToAdd.id in added)) {
+ added[nodeToAdd.id] = true;
+ this.insertBefore(nodeToAdd, children[nodeidx] || null);
+ nodeToAdd.setAttribute("removable", "true");
+ nodeidx++;
+ }
+ }
+
+ // remove any leftover removable nodes
+ for (let i = children.length - 1; i >= nodeidx; i--) {
+ let curNode = children[i];
+
+ let curNodeId = this._idFromNode(curNode);
+ // skip over fixed items
+ if (curNodeId && curNode.getAttribute("removable") == "true") {
+ if (palette)
+ palette.appendChild(curNode);
+ else
+ this.removeChild(curNode);
+ }
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="_newElementCount">0</field>
+ <method name="_getToolbarItem">
+ <parameter name="aId"/>
+ <body>
+ <![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/" +
+ "gatekeeper/there.is.only.xul";
+
+ var newItem = null;
+ switch (aId) {
+ // Handle special cases
+ case "separator":
+ case "spring":
+ case "spacer":
+ newItem = document.createElementNS(XUL_NS, "toolbar" + aId);
+ // Due to timers resolution Date.now() can be the same for
+ // elements created in small timeframes. So ids are
+ // differentiated through a unique count suffix.
+ newItem.id = aId + Date.now() + (++this._newElementCount);
+ if (aId == "spring")
+ newItem.flex = 1;
+ break;
+ default:
+ var toolbox = this.toolbox;
+ if (!toolbox)
+ break;
+
+ // look for an item with the same id, as the item may be
+ // in a different toolbar.
+ var item = document.getElementById(aId);
+ if (item && item.parentNode &&
+ item.parentNode.localName == "toolbar" &&
+ item.parentNode.toolbox == toolbox) {
+ newItem = item;
+ break;
+ }
+
+ if (toolbox.palette) {
+ // Attempt to locate an item with a matching ID within
+ // the palette.
+ let paletteItem = this.toolbox.palette.firstChild;
+ while (paletteItem) {
+ if (paletteItem.id == aId) {
+ newItem = paletteItem;
+ break;
+ }
+ paletteItem = paletteItem.nextSibling;
+ }
+ }
+ break;
+ }
+
+ return newItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertItem">
+ <parameter name="aId"/>
+ <parameter name="aBeforeElt"/>
+ <parameter name="aWrapper"/>
+ <body>
+ <![CDATA[
+ var newItem = this._getToolbarItem(aId);
+ if (!newItem)
+ return null;
+
+ var insertItem = newItem;
+ // make sure added items are removable
+ newItem.setAttribute("removable", "true");
+
+ // Wrap the item in another node if so inclined.
+ if (aWrapper) {
+ aWrapper.appendChild(newItem);
+ insertItem = aWrapper;
+ }
+
+ // Insert the palette item into the toolbar.
+ if (aBeforeElt)
+ this.insertBefore(insertItem, aBeforeElt);
+ else
+ this.appendChild(insertItem);
+
+ return newItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="hasCustomInteractiveItems">
+ <parameter name="aCurrentSet"/>
+ <body><![CDATA[
+ if (aCurrentSet == "__empty")
+ return false;
+
+ var defaultOrNoninteractive = (this.getAttribute("defaultset") || "")
+ .split(",")
+ .concat(["separator", "spacer", "spring"]);
+ return aCurrentSet.split(",").some(item => !defaultOrNoninteractive.includes(item));
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="toolbar-menubar-autohide"
+ extends="chrome://communicator/content/bindings/toolbar.xml#toolbar">
+ <implementation>
+ <constructor>
+ this._setInactive();
+ </constructor>
+ <destructor>
+ this._setActive();
+ </destructor>
+
+ <field name="_inactiveTimeout">null</field>
+
+ <field name="_contextMenuListener"><![CDATA[({
+ toolbar: this,
+ contextMenu: null,
+
+ get active() {
+ return !!this.contextMenu;
+ },
+
+ init(event) {
+ var node = event.target;
+ while (node != this.toolbar) {
+ if (node.localName == "menupopup")
+ return;
+ node = node.parentNode;
+ }
+
+ var contextMenuId = this.toolbar.getAttribute("context");
+ if (!contextMenuId)
+ return;
+
+ this.contextMenu = document.getElementById(contextMenuId);
+ if (!this.contextMenu)
+ return;
+
+ this.contextMenu.addEventListener("popupshown", this);
+ this.contextMenu.addEventListener("popuphiding", this);
+ this.toolbar.addEventListener("mousemove", this);
+ },
+ handleEvent(event) {
+ switch (event.type) {
+ case "popupshown":
+ this.toolbar.removeEventListener("mousemove", this);
+ break;
+ case "popuphiding":
+ case "mousemove":
+ this.toolbar._setInactiveAsync();
+ this.toolbar.removeEventListener("mousemove", this);
+ this.contextMenu.removeEventListener("popuphiding", this);
+ this.contextMenu.removeEventListener("popupshown", this);
+ this.contextMenu = null;
+ break;
+ }
+ },
+ })]]></field>
+
+ <method name="_setInactive">
+ <body><![CDATA[
+ this.setAttribute("inactive", "true");
+ ]]></body>
+ </method>
+
+ <method name="_setInactiveAsync">
+ <body><![CDATA[
+ this._inactiveTimeout = setTimeout(function(self) {
+ if (self.getAttribute("autohide") == "true") {
+ self._inactiveTimeout = null;
+ self._setInactive();
+ }
+ }, 0, this);
+ ]]></body>
+ </method>
+
+ <method name="_setActive">
+ <body><![CDATA[
+ if (this._inactiveTimeout) {
+ clearTimeout(this._inactiveTimeout);
+ this._inactiveTimeout = null;
+ }
+ this.removeAttribute("inactive");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="DOMMenuBarActive" action="this._setActive();"/>
+ <handler event="popupshowing" action="this._setActive();"/>
+ <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
+ <handler event="DOMMenuBarInactive"><![CDATA[
+ if (!this._contextMenuListener.active)
+ this._setInactiveAsync();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="menubar" role="xul:menubar">
+ <implementation>
+ <field name="_active">false</field>
+ <field name="_statusbar">null</field>
+ <field name="_originalStatusText">null</field>
+ <property name="statusbar" onget="return this.getAttribute('statusbar');"
+ onset="this.setAttribute('statusbar', val); return val;"/>
+ <method name="_updateStatusText">
+ <parameter name="itemText"/>
+ <body>
+ <![CDATA[
+ if (!this._active)
+ return;
+ var newText = itemText ? itemText : this._originalStatusText;
+ if (newText != this._statusbar.label)
+ this._statusbar.label = newText;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="DOMMenuBarActive">
+ <![CDATA[
+ if (!this.statusbar) return;
+ this._statusbar = document.getElementById(this.statusbar);
+ if (!this._statusbar)
+ return;
+ this._active = true;
+ this._originalStatusText = this._statusbar.label;
+ ]]>
+ </handler>
+ <handler event="DOMMenuBarInactive">
+ <![CDATA[
+ if (!this._active)
+ return;
+ this._active = false;
+ this._statusbar.label = this._originalStatusText;
+ ]]>
+ </handler>
+ <handler event="DOMMenuItemActive">this._updateStatusText(event.target.statusText);</handler>
+ <handler event="DOMMenuItemInactive">this._updateStatusText("");</handler>
+ </handlers>
+ </binding>
+
+ <binding id="toolbarpaletteitem">
+ <content>
+ <xul:hbox class="toolbarpaletteitem-box" flex="1" xbl:inherits="type,place">
+ <children/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="toolbarpaletteitem-palette"
+ extends="chrome://communicator/content/bindings/toolbar.xml#toolbarpaletteitem">
+ <content>
+ <xul:hbox class="toolbarpaletteitem-box" xbl:inherits="type,place">
+ <children/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=title"/>
+ </content>
+ </binding>
+
+ <binding id="toolbarpaletteitem-palette-wrapping-label"
+ extends="chrome://communicator/content/bindings/toolbar.xml#toolbarpaletteitem">
+ <content>
+ <xul:hbox class="toolbarpaletteitem-box" xbl:inherits="type,place">
+ <children/>
+ </xul:hbox>
+ <xul:label xbl:inherits="xbl:text=title"/>
+ </content>
+ </binding>
+
+ <binding id="menu-button"
+ extends="chrome://global/content/bindings/button.xml#button-base">
+
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:toolbarbutton class="box-inherit toolbarbutton-menubutton-button"
+ anonid="button" flex="1" allowevents="true"
+ xbl:inherits="disabled,crop,image,label,accesskey,command,wrap,badge,
+ align,dir,pack,orient,tooltiptext=buttontooltiptext"/>
+ <xul:dropmarker type="menu-button" class="toolbarbutton-menubutton-dropmarker"
+ anonid="dropmarker" xbl:inherits="align,dir,pack,orient,disabled,label,open,consumeanchor"/>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/build/Makefile.in b/comm/suite/components/build/Makefile.in
new file mode 100644
index 0000000000..2387227ab4
--- /dev/null
+++ b/comm/suite/components/build/Makefile.in
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+# Ensure that we don't embed a manifest referencing the CRT.
+EMBED_MANIFEST_AT =
diff --git a/comm/suite/components/build/moz.build b/comm/suite/components/build/moz.build
new file mode 100644
index 0000000000..4778cb3404
--- /dev/null
+++ b/comm/suite/components/build/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "nsSuiteCID.h",
+]
+
+SOURCES += [
+ "nsSuiteModule.cpp",
+]
+
+Library("suite")
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "../feeds",
+ "../migration/src",
+ "../profile",
+ "../shell",
+]
diff --git a/comm/suite/components/build/nsSuiteCID.h b/comm/suite/components/build/nsSuiteCID.h
new file mode 100644
index 0000000000..0ee2802151
--- /dev/null
+++ b/comm/suite/components/build/nsSuiteCID.h
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// {e5eeef51-05ce-4885-9434-7287616d9547}
+#define NS_FEEDSNIFFER_CID \
+ { 0xe5eeef51, 0x5ce, 0x4885, { 0x94, 0x34, 0x72, 0x87, 0x61, 0x6d, 0x95, 0x47 } }
+
+#define NS_FEEDSNIFFER_CONTRACTID \
+ "@mozilla.org/browser/feeds/sniffer;1"
+
+// {39b688ec-e308-49e5-be6b-28dc7fcd6154}
+#define NS_SHELLSERVICE_CID \
+ { 0x39b688ec, 0xe308, 0x49e5, { 0xbe, 0x6b, 0x28, 0xdc, 0x7f, 0xcd, 0x61, 0x54 } }
+
+#define NS_SHELLSERVICE_CONTRACTID \
+ "@mozilla.org/suite/shell-service;1"
+
+// {9aa21826-9d1d-433d-8c10-f313b26fa9dd}
+#define NS_SUITEDIRECTORYPROVIDER_CID \
+ { 0x9aa21826, 0x9d1d, 0x433d, { 0x8c, 0x10, 0xf3, 0x13, 0xb2, 0x6f, 0xa9, 0xdd } }
+
+#define NS_SUITEDIRECTORYPROVIDER_CONTRACTID \
+ "@mozilla.org/suite/directory-provider;1"
diff --git a/comm/suite/components/build/nsSuiteModule.cpp b/comm/suite/components/build/nsSuiteModule.cpp
new file mode 100644
index 0000000000..c9d2b4503c
--- /dev/null
+++ b/comm/suite/components/build/nsSuiteModule.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ModuleUtils.h"
+#include "nsSuiteDirectoryProvider.h"
+#include "nsThunderbirdProfileMigrator.h"
+#include "nsSuiteMigrationCID.h"
+#include "nsNetCID.h"
+#include "nsFeedSniffer.h"
+
+#if defined(XP_WIN)
+#include "nsWindowsShellService.h"
+#elif defined(XP_MACOSX)
+#include "nsMacShellService.h"
+#elif defined(MOZ_WIDGET_GTK)
+#include "nsGNOMEShellService.h"
+#endif
+
+using namespace mozilla;
+/////////////////////////////////////////////////////////////////////////////
+
+#if defined(XP_WIN)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsWindowsShellService, Init)
+#elif defined(XP_MACOSX)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacShellService)
+#elif defined(MOZ_WIDGET_GTK)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGNOMEShellService, Init)
+#endif
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsSuiteDirectoryProvider)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsThunderbirdProfileMigrator)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsFeedSniffer)
+
+#if defined(XP_WIN)
+NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
+#elif defined(XP_MACOSX)
+NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
+#elif defined(MOZ_WIDGET_GTK)
+NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
+#endif
+NS_DEFINE_NAMED_CID(NS_SUITEDIRECTORYPROVIDER_CID);
+NS_DEFINE_NAMED_CID(NS_THUNDERBIRDPROFILEMIGRATOR_CID);
+NS_DEFINE_NAMED_CID(NS_FEEDSNIFFER_CID);
+
+/////////////////////////////////////////////////////////////////////////////
+
+static const mozilla::Module::CIDEntry kSuiteCIDs[] = {
+#if defined(XP_WIN)
+ { &kNS_SHELLSERVICE_CID, false, NULL, nsWindowsShellServiceConstructor },
+#elif defined(XP_MACOSX)
+ { &kNS_SHELLSERVICE_CID, false, NULL, nsMacShellServiceConstructor },
+#elif defined(MOZ_WIDGET_GTK)
+ { &kNS_SHELLSERVICE_CID, false, NULL, nsGNOMEShellServiceConstructor },
+#endif
+ { &kNS_SUITEDIRECTORYPROVIDER_CID, false, NULL, nsSuiteDirectoryProviderConstructor },
+ { &kNS_THUNDERBIRDPROFILEMIGRATOR_CID, false, NULL, nsThunderbirdProfileMigratorConstructor },
+ { &kNS_FEEDSNIFFER_CID, false, NULL, nsFeedSnifferConstructor },
+ { NULL }
+};
+
+static const mozilla::Module::ContractIDEntry kSuiteContracts[] = {
+#if defined(XP_WIN)
+ { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
+#elif defined(XP_MACOSX)
+ { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
+#elif defined(MOZ_WIDGET_GTK)
+ { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
+#endif
+ { NS_SUITEDIRECTORYPROVIDER_CONTRACTID, &kNS_SUITEDIRECTORYPROVIDER_CID },
+ { NS_SUITEPROFILEMIGRATOR_CONTRACTID_PREFIX "thunderbird", &kNS_THUNDERBIRDPROFILEMIGRATOR_CID },
+ { NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID },
+ { NULL }
+};
+
+static const mozilla::Module::CategoryEntry kSuiteCategories[] = {
+ { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "suite-directory-provider", NS_SUITEDIRECTORYPROVIDER_CONTRACTID },
+ { NS_CONTENT_SNIFFER_CATEGORY, "Feed Sniffer", NS_FEEDSNIFFER_CONTRACTID },
+ { NULL }
+};
+
+extern const mozilla::Module kSuiteModule = {
+ mozilla::Module::kVersion,
+ kSuiteCIDs,
+ kSuiteContracts,
+ kSuiteCategories
+};
diff --git a/comm/suite/components/console/content/console.css b/comm/suite/components/console/content/console.css
new file mode 100644
index 0000000000..c3d0907c88
--- /dev/null
+++ b/comm/suite/components/console/content/console.css
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.console-box {
+ -moz-binding: url("chrome://communicator/content/console/consoleBindings.xml#console-box");
+ overflow: auto;
+}
+
+.console-rows {
+ -moz-user-focus: normal;
+}
+
+.console-row[type="error"],
+.console-row[type="warning"],
+.console-row[type="message"][typetext] {
+ -moz-binding: url("chrome://communicator/content/console/consoleBindings.xml#error");
+}
+
+.console-row[type="message"] {
+ -moz-binding: url("chrome://communicator/content/console/consoleBindings.xml#message");
+}
+
+.console-msg-text,
+.console-error-msg {
+ white-space: pre-wrap;
+}
+
+.console-error-source {
+ -moz-binding: url("chrome://communicator/content/console/consoleBindings.xml#console-error-source");
+}
+
+.console-dots {
+ width: 1px;
+}
+
+/* :::::::::: hiding and showing of rows for each mode :::::::::: */
+
+.console-box[mode="Warnings"] > .console-box-internal > .console-rows
+ > .console-row[type="error"],
+.console-box[mode="Messages"] > .console-box-internal > .console-rows
+ > .console-row[type="error"]
+{
+ display: none;
+}
+
+.console-box[mode="Errors"] > .console-box-internal > .console-rows
+ > .console-row[type="warning"],
+.console-box[mode="Messages"] > .console-box-internal > .console-rows
+ > .console-row[type="warning"]
+{
+ display: none;
+}
+
+.console-box[mode="Errors"] > .console-box-internal > .console-rows
+ > .console-row[type="message"],
+.console-box[mode="Warnings"] > .console-box-internal > .console-rows
+ > .console-row[type="message"]
+{
+ display: none;
+}
+
+.filtered-by-string {
+ display: none;
+}
+
+/* If line number is 0, hide the line number section */
+.lineNumberRow[line="0"] {
+ display: none;
+}
+
+#TextboxEval {
+ direction: ltr;
+}
diff --git a/comm/suite/components/console/content/console.js b/comm/suite/components/console/content/console.js
new file mode 100644
index 0000000000..53e3c9f6dd
--- /dev/null
+++ b/comm/suite/components/console/content/console.js
@@ -0,0 +1,111 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var gConsole, gConsoleBundle, gTextBoxEval, gEvaluator, gCodeToEvaluate;
+var gFilter;
+
+/* :::::::: Console Initialization ::::::::::::::: */
+
+window.onload = function()
+{
+ gConsole = document.getElementById("ConsoleBox");
+ gConsoleBundle = document.getElementById("ConsoleBundle");
+ gTextBoxEval = document.getElementById("TextboxEval");
+ gEvaluator = document.getElementById("Evaluator");
+ gFilter = document.getElementById("Filter");
+
+ updateSortCommand(gConsole.sortOrder);
+ updateModeCommand(gConsole.mode);
+
+ gEvaluator.addEventListener("load", loadOrDisplayResult, true);
+}
+
+/* :::::::: Console UI Functions ::::::::::::::: */
+
+function changeFilter()
+{
+ gConsole.filter = gFilter.value;
+
+ document.persist("ConsoleBox", "filter");
+}
+
+function changeMode(aMode)
+{
+ switch (aMode) {
+ case "Errors":
+ case "Warnings":
+ case "Messages":
+ gConsole.mode = aMode;
+ break;
+ case "All":
+ gConsole.mode = null;
+ }
+
+ document.persist("ConsoleBox", "mode");
+}
+
+function clearConsole()
+{
+ gConsole.clear();
+}
+
+function changeSortOrder(aOrder)
+{
+ updateSortCommand(gConsole.sortOrder = aOrder);
+}
+
+function updateSortCommand(aOrder)
+{
+ var orderString = aOrder == 'reverse' ? "Descend" : "Ascend";
+ var bc = document.getElementById("Console:sort"+orderString);
+ bc.setAttribute("checked", true);
+
+ orderString = aOrder == 'reverse' ? "Ascend" : "Descend";
+ bc = document.getElementById("Console:sort"+orderString);
+ bc.setAttribute("checked", false);
+}
+
+function updateModeCommand(aMode)
+{
+ /* aMode can end up invalid if it set by an extension that replaces */
+ /* mode and then it is uninstalled or disabled */
+ var bc = document.getElementById("Console:mode" + aMode) ||
+ document.getElementById("Console:modeAll");
+ bc.setAttribute("checked", true);
+}
+
+function onEvalKeyPress(aEvent)
+{
+ if (aEvent.keyCode == 13)
+ evaluateTypein();
+}
+
+function evaluateTypein()
+{
+ gCodeToEvaluate = gTextBoxEval.value;
+ // reset the iframe first; the code will be evaluated in loadOrDisplayResult
+ // below, once about:blank has completed loading (see bug 385092)
+ gEvaluator.contentWindow.location = "about:blank";
+}
+
+function loadOrDisplayResult()
+{
+ if (gCodeToEvaluate) {
+ gEvaluator.contentWindow.location = "javascript: " +
+ gCodeToEvaluate.replace(/%/g, "%25");
+ gCodeToEvaluate = "";
+ return;
+ }
+
+ var resultRange = gEvaluator.contentDocument.createRange();
+ resultRange.selectNode(gEvaluator.contentDocument.documentElement);
+ var result = resultRange.toString();
+ if (result)
+ Services.console.logStringMessage(result);
+ // or could use appendMessage which doesn't persist
+}
diff --git a/comm/suite/components/console/content/console.xul b/comm/suite/components/console/content/console.xul
new file mode 100644
index 0000000000..d5dd7ae0cb
--- /dev/null
+++ b/comm/suite/components/console/content/console.xul
@@ -0,0 +1,208 @@
+<?xml version="1.0"?> <!-- -*- tab-width: 4; indent-tabs-mode: nil -*- -->
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/console/console.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/content/console/console.css" type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+
+<!DOCTYPE window SYSTEM "chrome://communicator/locale/console/console.dtd" >
+
+<window id="JSConsoleWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&errorConsole.title;"
+ windowtype="suite:console"
+ width="640"
+ height="480"
+ screenX="10"
+ screenY="10"
+ persist="screenX screenY width height sizemode"
+ onclose="return closeWindow(false);">
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://communicator/content/console/console.js"/>
+ <script src="chrome://global/content/viewSourceUtils.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <stringbundle id="ConsoleBundle" src="chrome://communicator/locale/console/console.properties"/>
+
+ <commandset id="consoleCommands">
+ <commandset id="tasksCommands"/>
+ <command id="cmd_close" oncommand="closeWindow(true);"/>
+ </commandset>
+
+ <keyset id="consoleKeys">
+ <keyset id="tasksKeys"/>
+ <key id="key_close"
+ key="&closeCmd.commandkey;"
+ modifiers="accel"
+ command="cmd_close"/>
+ <key id="key_close2"
+ disabled="true"
+ keycode="VK_ESCAPE"
+ command="cmd_close"/>
+ <key id="key_focus1"
+ key="&focus1.commandkey;"
+ modifiers="accel"
+ oncommand="gTextBoxEval.focus();"/>
+ <key id="key_focus2"
+ key="&focus2.commandkey;"
+ modifiers="alt"
+ oncommand="gTextBoxEval.focus();"/>
+ <key id="key_copy"/>
+ </keyset>
+
+ <popupset id="ContextMenus">
+ <menupopup id="ConsoleContext">
+ <menuitem type="radio"
+ id="Console:sortAscend"
+ label="&sortFirst.label;"
+ accesskey="&sortFirst.accesskey;"
+ oncommand="changeSortOrder('forward');"/>
+ <menuitem type="radio"
+ id="Console:sortDescend"
+ label="&sortLast.label;"
+ accesskey="&sortLast.accesskey;"
+ oncommand="changeSortOrder('reverse');"/>
+ <menuseparator/>
+ <menuitem id="menu_copy_cm"
+ label="&copyCmd.label;"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"/>
+ </menupopup>
+ </popupset>
+
+ <toolbox id="console-toolbox">
+ <menubar id="main-menubar"
+ class="chromeclass-menubar"
+ grippytooltiptext="&menuBar.tooltip;">
+ <menu id="menu_File">
+ <menupopup id="menu_FilePopup">
+ <menuitem id="menu_close"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_Edit">
+ <menupopup>
+ <menuitem id="menu_copy"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_View">
+ <menupopup>
+ <menu label="&toolbarsCmd.label;"
+ accesskey="&toolbarsCmd.accesskey;">
+ <menupopup>
+ <menuitem id="toggleToolbarMode"
+ type="checkbox"
+ checked="true"
+ label="&toolbarMode.label;"
+ accesskey="&toolbarMode.accesskey;"
+ oncommand="goToggleToolbar('ToolbarMode','toggleToolbarMode');"/>
+ <menuitem id="toggleToolbarEval"
+ type="checkbox"
+ checked="true"
+ label="&toolbarEval.label;"
+ accesskey="&toolbarEval.accesskey;"
+ oncommand="goToggleToolbar('ToolbarEval','toggleToolbarEval');"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem type="radio" observes="Console:sortAscend"/>
+ <menuitem type="radio" observes="Console:sortDescend"/>
+ </menupopup>
+ </menu>
+
+ <!-- tasks menu filled from tasksOverlay -->
+ <menu id="tasksMenu"/>
+
+ <!-- window menu filled from tasksOverlay -->
+ <menu id="windowMenu"/>
+
+ <!-- help menu filled from globalOverlay -->
+ <menu id="menu_Help"/>
+ </menubar>
+
+ <toolbar class="chromeclass-toolbar"
+ id="ToolbarMode"
+ grippytooltiptext="&modeToolbar.tooltip;">
+ <hbox id="viewGroup">
+ <toolbarbutton type="radio"
+ group="mode"
+ id="Console:modeAll"
+ label="&all.label;"
+ accesskey="&all.accesskey;"
+ oncommand="changeMode('All');"/>
+ <toolbarbutton type="radio"
+ group="mode"
+ id="Console:modeErrors"
+ label="&errors.label;"
+ accesskey="&errors.accesskey;"
+ oncommand="changeMode('Errors');"/>
+ <toolbarbutton type="radio"
+ group="mode"
+ id="Console:modeWarnings"
+ label="&warnings.label;"
+ accesskey="&warnings.accesskey;"
+ oncommand="changeMode('Warnings');"/>
+ <toolbarbutton type="radio"
+ group="mode"
+ id="Console:modeMessages"
+ label="&messages.label;"
+ accesskey="&messages.accesskey;"
+ oncommand="changeMode('Messages');"/>
+ </hbox>
+ <toolbarseparator/>
+ <toolbarbutton id="Console:clear"
+ label="&clear.label;"
+ accesskey="&clear.accesskey;"
+ oncommand="clearConsole();"/>
+ </toolbar>
+
+ <toolbar class="chromeclass-toolbar"
+ id="ToolbarEval"
+ align="center"
+ nowindowdrag="true"
+ grippytooltiptext="&entryToolbar.tooltip;">
+ <label value="&codeEval.label;"
+ accesskey="&codeEval.accesskey;"
+ control="TextboxEval"/>
+ <textbox id="TextboxEval"
+ class="toolbar"
+ flex="1"
+ value=""
+ onkeypress="onEvalKeyPress(event);"/>
+ <toolbarbutton id="ButtonEval"
+ label="&evaluate.label;"
+ accesskey="&evaluate.accesskey;"
+ oncommand="evaluateTypein();"/>
+ </toolbar>
+
+ </toolbox>
+
+ <vbox id="ConsoleBox"
+ class="console-box"
+ flex="1"
+ context="ConsoleContext"
+ persist="sortOrder"/>
+
+ <iframe name="Evaluator"
+ id="Evaluator"
+ collapsed="true"/>
+
+ <statusbar>
+ <statusbarpanel flex="1" pack="start">
+ <label value="&filter2.label;" control="Filter"/>
+ <textbox type="search"
+ id="Filter"
+ accesskey="&filter2.accesskey;"
+ oncommand="changeFilter();"/>
+ </statusbarpanel>
+ </statusbar>
+
+</window>
diff --git a/comm/suite/components/console/content/consoleBindings.xml b/comm/suite/components/console/content/consoleBindings.xml
new file mode 100644
index 0000000000..7b87a9f4eb
--- /dev/null
+++ b/comm/suite/components/console/content/consoleBindings.xml
@@ -0,0 +1,543 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings SYSTEM "chrome://communicator/locale/console/console.dtd">
+
+<bindings id="consoleBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="console-box" extends="xul:box">
+ <content>
+ <xul:stringbundle src="chrome://communicator/locale/console/console.properties" role="string-bundle"/>
+ <xul:vbox class="console-box-internal">
+ <xul:vbox class="console-rows" role="console-rows" xbl:inherits="dir=sortOrder"/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <field name="limit" readonly="true">
+ 250
+ </field>
+
+ <field name="fieldMaxLength" readonly="true">
+ <!-- Limit displayed string lengths to avoid performance issues. (Bug 796179 and 831020) -->
+ 200
+ </field>
+
+ <field name="showChromeErrors" readonly="true">
+ Services.prefs.getBoolPref("javascript.options.showInConsole");
+ </field>
+
+ <property name="count" readonly="true">
+ <getter>return this.mCount</getter>
+ </property>
+
+ <property name="mode">
+ <getter>return this.mMode;</getter>
+ <setter><![CDATA[
+ if (this.mode != val) {
+ this.mMode = val || "All";
+ this.setAttribute("mode", this.mMode);
+ this.selectedItem = null;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="filter">
+ <getter>return this.mFilter;</getter>
+ <setter><![CDATA[
+ val = val.toLowerCase();
+ if (this.mFilter != val) {
+ this.mFilter = val;
+ for (let aRow of this.mConsoleRowBox.children) {
+ this.filterElement(aRow);
+ }
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="sortOrder">
+ <getter>return this.getAttribute("sortOrder");</getter>
+ <setter>this.setAttribute("sortOrder", val); return val;</setter>
+ </property>
+ <field name="mSelectedItem">null</field>
+ <property name="selectedItem">
+ <getter>return this.mSelectedItem</getter>
+ <setter><![CDATA[
+ if (this.mSelectedItem)
+ this.mSelectedItem.removeAttribute("selected");
+
+ this.mSelectedItem = val;
+ if (val)
+ val.setAttribute("selected", "true");
+
+ // Update edit commands
+ window.updateCommands("focus");
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="init">
+ <body><![CDATA[
+ this.mCount = 0;
+
+ this.mConsoleListener = {
+ console: this,
+ observe : function(aObject) {
+ // The message can arrive a little bit after the xbl binding has been
+ // unbind. So node.appendItem will not be available anymore.
+ if ('appendItem' in this.console)
+ this.console.appendItem(aObject);
+ }
+ };
+
+ this.mConsoleRowBox = document.getAnonymousElementByAttribute(this, "role", "console-rows");
+ this.mStrBundle = document.getAnonymousElementByAttribute(this, "role", "string-bundle");
+
+ try {
+ Services.console.registerListener(this.mConsoleListener);
+ } catch (ex) {
+ appendItem(
+ "Unable to display errors - couldn't get Console Service component. " +
+ "(Missing @mozilla.org/consoleservice;1)");
+ return;
+ }
+
+ this.mMode = this.getAttribute("mode") || "All";
+ this.mFilter = "";
+
+ this.appendInitialItems();
+ window.controllers.insertControllerAt(0, this._controller);
+ ]]></body>
+ </method>
+
+ <method name="destroy">
+ <body><![CDATA[
+ Services.console.unregisterListener(this.mConsoleListener);
+ window.controllers.removeController(this._controller);
+ ]]></body>
+ </method>
+
+ <method name="appendInitialItems">
+ <body><![CDATA[
+ var messages = Services.console.getMessageArray();
+
+ // In case getMessageArray returns 0-length array as null
+ if (!messages)
+ messages = [];
+
+ var limit = messages.length - this.limit;
+ if (limit < 0) limit = 0;
+
+ // Checks if console ever been cleared
+ for (var i = messages.length - 1; i >= limit; --i)
+ if (!messages[i].message)
+ break;
+
+ // Populate with messages after latest "clear"
+ while (++i < messages.length)
+ this.appendItem(messages[i]);
+ ]]></body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="aObject"/>
+ <body><![CDATA[
+ try {
+ // Try to QI it to a script error to get more info
+ var scriptError = aObject.QueryInterface(Ci.nsIScriptError);
+
+ // filter chrome urls
+ if (!this.showChromeErrors && scriptError.sourceName.substr(0, 9) == "chrome://")
+ return;
+
+ // filter private windows
+ if (scriptError.isFromPrivateWindow)
+ return;
+
+ this.appendError(scriptError);
+ } catch (ex) {
+ try {
+ // Try to QI it to a console message
+ var msg = aObject.QueryInterface(Ci.nsIConsoleMessage);
+ if (msg.message)
+ this.appendMessage(msg.message);
+ else // observed a null/"clear" message
+ this.clearConsole();
+ } catch (ex2) {
+ // Give up and append the object itself as a string
+ this.appendMessage(aObject);
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_truncateIfNecessary">
+ <parameter name="aString"/>
+ <parameter name="aMiddleCharacter"/>
+ <body><![CDATA[
+ if (!aString || aString.length <= this.fieldMaxLength)
+ return {string: aString, column: aMiddleCharacter};
+ let halfLimit = this.fieldMaxLength / 2;
+ if (!aMiddleCharacter || aMiddleCharacter < 0 || aMiddleCharacter > aString.length)
+ aMiddleCharacter = halfLimit;
+
+ let startPosition = 0;
+ let endPosition = aString.length;
+ if (aMiddleCharacter - halfLimit >= 0)
+ startPosition = aMiddleCharacter - halfLimit;
+ if (aMiddleCharacter + halfLimit <= aString.length)
+ endPosition = aMiddleCharacter + halfLimit;
+ if (endPosition - startPosition < this.fieldMaxLength)
+ endPosition += this.fieldMaxLength - (endPosition - startPosition);
+ let truncatedString = aString.substring(startPosition, endPosition);
+ let ellipsis = Services.prefs.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+ if (startPosition > 0) {
+ truncatedString = ellipsis + truncatedString;
+ aMiddleCharacter += ellipsis.length;
+ }
+ if (endPosition < aString.length)
+ truncatedString = truncatedString + ellipsis;
+
+ return {
+ string: truncatedString,
+ column: aMiddleCharacter - startPosition
+ };
+ ]]></body>
+ </method>
+
+ <method name="appendError">
+ <parameter name="aObject"/>
+ <body><![CDATA[
+ var row = this.createConsoleRow();
+ var nsIScriptError = Ci.nsIScriptError;
+
+ // nsIConsoleMessage constants: debug, info, warn, error
+ var typetext = ["typeMessage", "typeMessage", "typeWarning", "typeError"][aObject.logLevel];
+ var type = ["message", "message", "warning", "error"][aObject.logLevel];
+
+ row.setAttribute("typetext", this.mStrBundle.getString(typetext));
+ row.setAttribute("type", type);
+ row.setAttribute("msg", aObject.errorMessage);
+ row.setAttribute("category", aObject.category);
+ row.setAttribute("time", this.properFormatTime(aObject.timeStamp));
+ if (aObject.lineNumber || aObject.sourceName) {
+ row.setAttribute("href", this._truncateIfNecessary(aObject.sourceName).string);
+ row.mSourceName = aObject.sourceName;
+ row.setAttribute("line", aObject.lineNumber);
+ } else {
+ row.setAttribute("hideSource", "true");
+ }
+ if (aObject.sourceLine) {
+ let sourceLine = aObject.sourceLine.replace(/\s/g, " ");
+ let truncatedLineObj = this._truncateIfNecessary(sourceLine, aObject.columnNumber);
+ row.setAttribute("code", truncatedLineObj.string);
+ row.mSourceLine = sourceLine;
+ if (aObject.columnNumber) {
+ row.setAttribute("col", aObject.columnNumber);
+ row.setAttribute("errorDots", this.repeatChar(" ", truncatedLineObj.column));
+ row.setAttribute("errorCaret", " ");
+ } else {
+ row.setAttribute("hideCaret", "true");
+ }
+ } else {
+ row.setAttribute("hideCode", "true");
+ }
+
+ this.appendConsoleRow(row);
+ ]]></body>
+ </method>
+
+ <method name="appendMessage">
+ <parameter name="aMessage"/>
+ <parameter name="aType"/>
+ <body><![CDATA[
+ var row = this.createConsoleRow();
+ row.setAttribute("type", aType || "message");
+ row.setAttribute("msg", aMessage);
+ this.appendConsoleRow(row);
+ ]]></body>
+ </method>
+
+ <method name="clear">
+ <body><![CDATA[
+ // add a "clear" message (mainly for other listeners)
+ Services.console.logStringMessage(null);
+ Services.console.reset();
+ ]]></body>
+ </method>
+
+ <method name="properFormatTime">
+ <parameter name="aTime"/>
+ <body><![CDATA[
+ const dateServ = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short", timeStyle: "long"
+ });
+ return dateServ.format(aTime);
+ ]]></body>
+ </method>
+
+ <method name="copySelectedItem">
+ <body><![CDATA[
+ if (this.mSelectedItem) try {
+ const clipURI = "@mozilla.org/widget/clipboardhelper;1";
+ const clipI = Ci.nsIClipboardHelper;
+ var clipboard = Cc[clipURI].getService(clipI);
+
+ clipboard.copyString(this.mSelectedItem.toString());
+ } catch (ex) {
+ // Unable to copy anything, die quietly
+ }
+ ]]></body>
+ </method>
+
+ <method name="createConsoleRow">
+ <body><![CDATA[
+ var row = document.createElement("box");
+ row.setAttribute("class", "console-row");
+ row._IsConsoleRow = true;
+ row._ConsoleBox = this;
+ return row;
+ ]]></body>
+ </method>
+
+ <method name="appendConsoleRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ this.filterElement(aRow);
+ this.mConsoleRowBox.appendChild(aRow);
+ if (++this.mCount > this.limit) this.deleteFirst();
+ ]]></body>
+ </method>
+
+ <method name="deleteFirst">
+ <body><![CDATA[
+ var node = this.mConsoleRowBox.firstChild;
+ this.mConsoleRowBox.removeChild(node);
+ --this.mCount;
+ ]]></body>
+ </method>
+
+ <method name="clearConsole">
+ <body><![CDATA[
+ if (this.mCount == 0) // already clear
+ return;
+ this.mCount = 0;
+
+ var newRows = this.mConsoleRowBox.cloneNode(false);
+ this.mConsoleRowBox.parentNode.replaceChild(newRows, this.mConsoleRowBox);
+ this.mConsoleRowBox = newRows;
+ this.selectedItem = null;
+ ]]></body>
+ </method>
+
+ <method name="filterElement">
+ <parameter name="aRow" />
+ <body><![CDATA[
+ let anyMatch = ["msg", "line", "code"].some(function (key) {
+ return (aRow.hasAttribute(key) &&
+ this.stringMatchesFilters(aRow.getAttribute(key), this.mFilter));
+ }, this) || (aRow.mSourceName &&
+ this.stringMatchesFilters(aRow.mSourceName, this.mFilter));
+
+ if (anyMatch) {
+ aRow.classList.remove("filtered-by-string")
+ } else {
+ aRow.classList.add("filtered-by-string")
+ }
+ ]]></body>
+ </method>
+
+ <!-- UTILITY FUNCTIONS -->
+
+ <method name="repeatChar">
+ <parameter name="aChar"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ if (--aCol <= 0)
+ return "";
+
+ for (var i = 2; i < aCol; i += i)
+ aChar += aChar;
+
+ return aChar + aChar.slice(0, aCol - aChar.length);
+ ]]></body>
+ </method>
+
+ <method name="stringMatchesFilters">
+ <parameter name="aString"/>
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ if (!aString || !aFilter) {
+ return true;
+ }
+
+ let searchStr = aString.toLowerCase();
+ let filterStrings = aFilter.split(/\s+/);
+ return !filterStrings.some(function (f) {
+ return !searchStr.includes(f);
+ });
+ ]]></body>
+ </method>
+
+ <constructor>this.init();</constructor>
+ <destructor>this.destroy();</destructor>
+
+ <!-- Command controller for the copy command -->
+ <field name="_controller"><![CDATA[({
+ _outer: this,
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIController) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ supportsCommand: function(aCommand) {
+ return aCommand == "cmd_copy";
+ },
+
+ isCommandEnabled: function(aCommand) {
+ return aCommand == "cmd_copy" && this._outer.selectedItem;
+ },
+
+ doCommand: function(aCommand) {
+ if (aCommand == "cmd_copy")
+ this._outer.copySelectedItem();
+ },
+
+ onEvent: function() { }
+ });]]></field>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown"><![CDATA[
+ if (event.button == 0 || event.button == 2) {
+ var target = event.originalTarget;
+
+ while (target && !("_IsConsoleRow" in target))
+ target = target.parentNode;
+
+ if (target)
+ this.selectedItem = target;
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="error" extends="xul:box">
+ <content>
+ <xul:box class="console-row-internal-box" flex="1">
+ <xul:box class="console-row-icon" align="center" xbl:inherits="selected">
+ <xul:image class="console-icon" xbl:inherits="src,type"/>
+ </xul:box>
+ <xul:vbox class="console-row-content" xbl:inherits="selected" flex="1">
+ <xul:box class="console-row-msg" align="start">
+ <xul:label class="label" xbl:inherits="value=typetext"/>
+ <xul:description class="console-error-msg" xbl:inherits="xbl:text=msg" flex="1"/>
+ <xul:label class="label console-time" xbl:inherits="value=time"/>
+ </xul:box>
+ <xul:box class="console-row-file" xbl:inherits="hidden=hideSource">
+ <xul:label class="label" value="&errFile.label;"/>
+ <xul:box class="console-error-source" xbl:inherits="href,line"/>
+ <xul:spacer flex="1"/>
+ <xul:hbox class="lineNumberRow" xbl:inherits="line">
+ <xul:label class="label" value="&errLine.label;"/>
+ <xul:label class="label" xbl:inherits="value=line"/>
+ </xul:hbox>
+ </xul:box>
+ <xul:vbox class="console-row-code" xbl:inherits="selected,hidden=hideCode">
+ <xul:label class="monospace console-code" xbl:inherits="value=code" crop="end"/>
+ <xul:box xbl:inherits="hidden=hideCaret">
+ <xul:label class="monospace console-dots" xbl:inherits="value=errorDots"/>
+ <xul:label class="monospace console-caret" xbl:inherits="value=errorCaret"/>
+ <xul:spacer flex="1"/>
+ </xul:box>
+ </xul:vbox>
+ </xul:vbox>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <field name="mSourceName">null</field>
+ <field name="mSourceLine">null</field>
+
+ <method name="toString">
+ <body><![CDATA[
+ let msg = "";
+ let strBundle = this._ConsoleBox.mStrBundle;
+
+ if (this.hasAttribute("time"))
+ msg += strBundle.getFormattedString("errTime", [this.getAttribute("time")]) + "\n";
+
+ msg += this.getAttribute("typetext") + " " + this.getAttribute("msg");
+
+ if (this.hasAttribute("line") && this.mSourceName) {
+ msg += "\n" + strBundle.getFormattedString("errFile",
+ [this.mSourceName]) + "\n";
+ if (this.hasAttribute("col")) {
+ msg += strBundle.getFormattedString("errLineCol",
+ [this.getAttribute("line"), this.getAttribute("col")]);
+ } else
+ msg += strBundle.getFormattedString("errLine", [this.getAttribute("line")]);
+ }
+
+ if (this.hasAttribute("code"))
+ msg += "\n" + strBundle.getString("errCode") + "\n" + this.mSourceLine;
+
+ return msg;
+ ]]></body>
+ </method>
+ </implementation>
+
+ </binding>
+
+ <binding id="message" extends="xul:box">
+ <content>
+ <xul:box class="console-internal-box" flex="1">
+ <xul:box class="console-row-icon" align="center">
+ <xul:image class="console-icon" xbl:inherits="src,type"/>
+ </xul:box>
+ <xul:vbox class="console-row-content" xbl:inherits="selected" flex="1">
+ <xul:vbox class="console-row-msg" flex="1">
+ <xul:description class="console-msg-text" xbl:inherits="xbl:text=msg"/>
+ </xul:vbox>
+ </xul:vbox>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <method name="toString">
+ <body><![CDATA[
+ return this.getAttribute("msg");
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="console-error-source" extends="xul:box">
+ <content>
+ <xul:label class="text-link" xbl:inherits="value=href" crop="right"/>
+ </content>
+
+ <handlers>
+ <handler event="click" phase="capturing" button="0" preventdefault="true">
+ <![CDATA[
+ var url = document.getBindingParent(this).mSourceName;
+ url = url.substring(url.lastIndexOf(" ") + 1);
+ var line = getAttribute("line");
+ gViewSourceUtils.viewSource({URL: url, lineNumber: line});
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/console/jar.mn b/comm/suite/components/console/jar.mn
new file mode 100644
index 0000000000..3c54ac207c
--- /dev/null
+++ b/comm/suite/components/console/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/console/consoleBindings.xml (content/consoleBindings.xml)
+ content/communicator/console/console.css (content/console.css)
+ content/communicator/console/console.js (content/console.js)
+ content/communicator/console/console.xul (content/console.xul)
diff --git a/comm/suite/components/console/jsconsole-clhandler.js b/comm/suite/components/console/jsconsole-clhandler.js
new file mode 100644
index 0000000000..cf4612a296
--- /dev/null
+++ b/comm/suite/components/console/jsconsole-clhandler.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function jsConsoleHandler() {}
+jsConsoleHandler.prototype = {
+ handle: function clh_handle(cmdLine) {
+ if (!cmdLine.handleFlag("suiteconsole", false))
+ return;
+
+ var console = Services.wm.getMostRecentWindow("suite:console");
+ if (!console) {
+ Services.ww.openWindow(null,
+ "chrome://communicator/content/console/console.xul",
+ "_blank", "chrome,dialog=no,all", cmdLine);
+ } else {
+ console.focus(); // the Error console was already open
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO)
+ cmdLine.preventDefault = true;
+ },
+
+ helpInfo : " --suiteconsole Open the Error console.\n",
+
+ classID: Components.ID("{afeee354-8c99-4725-adb1-8502218c5c3c}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([jsConsoleHandler]);
diff --git a/comm/suite/components/console/jsconsole-clhandler.manifest b/comm/suite/components/console/jsconsole-clhandler.manifest
new file mode 100644
index 0000000000..af2cfb5f74
--- /dev/null
+++ b/comm/suite/components/console/jsconsole-clhandler.manifest
@@ -0,0 +1,3 @@
+component {afeee354-8c99-4725-adb1-8502218c5c3c} jsconsole-clhandler.js
+contract @mozilla.org/suite/console-clh;1 {afeee354-8c99-4725-adb1-8502218c5c3c}
+category command-line-handler t-jsconsole @mozilla.org/suite/console-clh;1
diff --git a/comm/suite/components/console/moz.build b/comm/suite/components/console/moz.build
new file mode 100644
index 0000000000..0fed37d675
--- /dev/null
+++ b/comm/suite/components/console/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_COMPONENTS += [
+ "jsconsole-clhandler.js",
+ "jsconsole-clhandler.manifest",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/customizeToolbar.css b/comm/suite/components/customizeToolbar.css
new file mode 100644
index 0000000000..f1242dc921
--- /dev/null
+++ b/comm/suite/components/customizeToolbar.css
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+#CustomizeToolbarWindow:-moz-lwtheme[lwtheme-image] {
+ background-image: none !important;
+ text-shadow: none;
+}
+
+#main-box {
+ padding: 8px;
+}
+
+#instructions {
+ font-weight: 600;
+ font-size: 1.2em;
+ margin-block: 5px 10px;
+}
+
+#palette-box {
+ overflow: auto;
+ display: block;
+ min-height: 3em;
+ background-color: hsla(0, 0%, 100%, .3);
+ border: 1px solid hsla(0, 0%, 50%, .4);
+}
+
+:root[lwt-tree] #palette-box {
+ scrollbar-color: rgba(204, 204, 204, .5) rgba(230, 230, 235, .5);
+}
+
+:root[lwt-tree-brighttext] #palette-box {
+ scrollbar-color: rgba(249, 249, 250, .4) rgba(20, 20, 25, .3);
+}
+
+#palette-box > toolbarpaletteitem {
+ padding: 8px 2px;
+ margin: 0 8px;
+}
+
+toolbarpaletteitem {
+ -moz-window-dragging: no-drag;
+ -moz-box-pack: start;
+}
+
+toolbarpaletteitem[place="palette"] {
+ -moz-box-orient: vertical;
+ width: 10em;
+ max-width: 10em;
+ /* icon (16) + margin (9 + 12) + 4 lines of text: */
+ height: calc(39px + 4em);
+ margin-bottom: 5px;
+ margin-inline-end: 24px;
+ overflow: visible;
+ display: inline-block;
+ vertical-align: top;
+}
+
+toolbarpaletteitem[place=palette]::after {
+ content: attr(title);
+ display: block;
+ text-align: center;
+}
+
+toolbarpaletteitem > toolbarbutton,
+toolbarpaletteitem > toolbarseparator,
+toolbarpaletteitem > toolbaritem {
+ /* Prevent children from getting events */
+ pointer-events: none;
+ -moz-box-pack: center;
+ -moz-box-flex: 1;
+}
+
+toolbarpaletteitem[type="separator"][place="palette"] {
+ -moz-box-align: center;
+}
+
+toolbarpaletteitem[type="separator"][place="palette"] toolbarseparator {
+ background-color: currentColor;
+}
+
+#main-box > box {
+ overflow: hidden;
+}
+
+/* Hide the toolbarbutton label because we replicate it on the wrapper */
+.toolbarbutton-text {
+ display: none;
+}
+
+toolbarbutton > .toolbarbutton-menubutton-dropmarker {
+ display: none;
+}
+
+#buttonBox {
+ margin-block: 5px;
+}
+
+#titlebarSettings > checkbox {
+ margin-inline: 0 15px;
+}
+
+#modelistLabel {
+ margin-top: 2px;
+}
diff --git a/comm/suite/components/customizeToolbar.js b/comm/suite/components/customizeToolbar.js
new file mode 100644
index 0000000000..0cdd45ba3d
--- /dev/null
+++ b/comm/suite/components/customizeToolbar.js
@@ -0,0 +1,855 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gToolboxDocument = null;
+var gToolbox = null;
+var gCurrentDragOverItem = null;
+var gToolboxChanged = false;
+var gToolboxSheet = false;
+var gPaletteBox = null;
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+function onLoad() {
+ if ("arguments" in window && window.arguments[0]) {
+ InitWithToolbox(window.arguments[0]);
+ repositionDialog(window);
+ } else if (window.frameElement && "toolbox" in window.frameElement) {
+ gToolboxSheet = true;
+ InitWithToolbox(window.frameElement.toolbox);
+ repositionDialog(window.frameElement.panel);
+ }
+}
+
+function InitWithToolbox(aToolbox) {
+ gToolbox = aToolbox;
+ dispatchCustomizationEvent("beforecustomization");
+ gToolboxDocument = gToolbox.ownerDocument;
+ gToolbox.customizing = true;
+ forEachCustomizableToolbar(function(toolbar) {
+ toolbar.setAttribute("customizing", "true");
+ });
+ gPaletteBox = document.getElementById("palette-box");
+
+ var elts = getRootElements();
+ for (let i = 0; i < elts.length; i++) {
+ elts[i].addEventListener("dragstart", onToolbarDragStart, true);
+ elts[i].addEventListener("dragover", onToolbarDragOver, true);
+ elts[i].addEventListener("dragexit", onToolbarDragExit, true);
+ elts[i].addEventListener("drop", onToolbarDrop, true);
+ }
+
+ initDialog();
+}
+
+function onClose() {
+ if (!gToolboxSheet) {
+ window.close();
+ } else {
+ finishToolbarCustomization();
+ }
+}
+
+function onUnload() {
+ if (!gToolboxSheet) {
+ finishToolbarCustomization();
+ }
+}
+
+function finishToolbarCustomization() {
+ removeToolboxListeners();
+ unwrapToolbarItems();
+ persistCurrentSets();
+ gToolbox.customizing = false;
+ forEachCustomizableToolbar(function(toolbar) {
+ toolbar.removeAttribute("customizing");
+ });
+
+ notifyParentComplete();
+}
+
+function initDialog() {
+ var mode = gToolbox.getAttribute("mode");
+ document.getElementById("modelist").value = mode;
+ var smallIconsCheckbox = document.getElementById("smallicons");
+ smallIconsCheckbox.checked = gToolbox.getAttribute("iconsize") == "small";
+ if (mode == "text") {
+ smallIconsCheckbox.disabled = true;
+ }
+
+ if (AppConstants.MOZ_APP_NAME == "thunderbird") {
+ document.getElementById(
+ "showTitlebar"
+ ).checked = !Services.prefs.getBoolPref("mail.tabs.drawInTitlebar");
+ document.getElementById(
+ "showDragSpace"
+ ).checked = Services.prefs.getBoolPref("mail.tabs.extraDragSpace");
+ if (
+ window.opener &&
+ window.opener.document.documentElement.getAttribute("windowtype") ==
+ "mail:3pane"
+ ) {
+ document.getElementById("titlebarSettings").hidden = false;
+ }
+ }
+
+ // Build up the palette of other items.
+ buildPalette();
+
+ // Wrap all the items on the toolbar in toolbarpaletteitems.
+ wrapToolbarItems();
+}
+
+function repositionDialog(aWindow) {
+ // Position the dialog touching the bottom of the toolbox and centered with
+ // it.
+ if (!aWindow) {
+ return;
+ }
+
+ var width;
+ if (aWindow != window) {
+ width = aWindow.getBoundingClientRect().width;
+ } else if (document.documentElement.hasAttribute("width")) {
+ width = document.documentElement.getAttribute("width");
+ } else {
+ width = parseInt(document.documentElement.style.width);
+ }
+ var boundingRect = gToolbox.getBoundingClientRect();
+ var screenX = gToolbox.screenX + (boundingRect.width - width) / 2;
+ var screenY = gToolbox.screenY + boundingRect.height;
+
+ aWindow.moveTo(screenX, screenY);
+}
+
+function removeToolboxListeners() {
+ var elts = getRootElements();
+ for (let i = 0; i < elts.length; i++) {
+ elts[i].removeEventListener("dragstart", onToolbarDragStart, true);
+ elts[i].removeEventListener("dragover", onToolbarDragOver, true);
+ elts[i].removeEventListener("dragexit", onToolbarDragExit, true);
+ elts[i].removeEventListener("drop", onToolbarDrop, true);
+ }
+}
+
+/**
+ * Invoke a callback on the toolbox to notify it that the dialog is done
+ * and going away.
+ */
+function notifyParentComplete() {
+ if ("customizeDone" in gToolbox) {
+ gToolbox.customizeDone(gToolboxChanged);
+ }
+ dispatchCustomizationEvent("aftercustomization");
+}
+
+function toolboxChanged(aType) {
+ gToolboxChanged = true;
+ if ("customizeChange" in gToolbox) {
+ gToolbox.customizeChange(aType);
+ }
+ dispatchCustomizationEvent("customizationchange");
+}
+
+function dispatchCustomizationEvent(aEventName) {
+ var evt = document.createEvent("Events");
+ evt.initEvent(aEventName, true, true);
+ gToolbox.dispatchEvent(evt);
+}
+
+/**
+ * Persist the current set of buttons in all customizable toolbars to
+ * localstore.
+ */
+function persistCurrentSets() {
+ if (!gToolboxChanged || gToolboxDocument.defaultView.closed) {
+ return;
+ }
+
+ forEachCustomizableToolbar(function(toolbar) {
+ // Calculate currentset and store it in the attribute.
+ var currentSet = toolbar.currentSet;
+ toolbar.setAttribute("currentset", currentSet);
+ Services.xulStore.persist(toolbar, "currentset");
+ });
+}
+
+/**
+ * Wraps all items in all customizable toolbars in a toolbox.
+ */
+function wrapToolbarItems() {
+ forEachCustomizableToolbar(function(toolbar) {
+ for (let item of toolbar.children) {
+ if (AppConstants.platform == "macosx") {
+ if (
+ item.firstElementChild &&
+ item.firstElementChild.localName == "menubar"
+ ) {
+ return;
+ }
+ }
+ if (isToolbarItem(item)) {
+ let wrapper = wrapToolbarItem(item);
+ cleanupItemForToolbar(item, wrapper);
+ }
+ }
+ });
+}
+
+function getRootElements() {
+ if (window.frameElement && "externalToolbars" in window.frameElement) {
+ return [gToolbox].concat(window.frameElement.externalToolbars);
+ }
+ if ("arguments" in window && window.arguments[1].length > 0) {
+ return [gToolbox].concat(window.arguments[1]);
+ }
+ return [gToolbox];
+}
+
+/**
+ * Unwraps all items in all customizable toolbars in a toolbox.
+ */
+function unwrapToolbarItems() {
+ let elts = getRootElements();
+ for (let i = 0; i < elts.length; i++) {
+ let paletteItems = elts[i].getElementsByTagName("toolbarpaletteitem");
+ let paletteItem;
+ while ((paletteItem = paletteItems.item(0)) != null) {
+ let toolbarItem = paletteItem.firstElementChild;
+ restoreItemForToolbar(toolbarItem, paletteItem);
+ paletteItem.parentNode.replaceChild(toolbarItem, paletteItem);
+ }
+ }
+}
+
+/**
+ * Creates a wrapper that can be used to contain a toolbaritem and prevent
+ * it from receiving UI events.
+ */
+function createWrapper(aId, aDocument) {
+ let wrapper = aDocument.createXULElement("toolbarpaletteitem");
+
+ wrapper.id = "wrapper-" + aId;
+ return wrapper;
+}
+
+/**
+ * Wraps an item that has been cloned from a template and adds
+ * it to the end of the palette.
+ */
+function wrapPaletteItem(aPaletteItem) {
+ var wrapper = createWrapper(aPaletteItem.id, document);
+
+ wrapper.appendChild(aPaletteItem);
+
+ // XXX We need to call this AFTER the palette item has been appended
+ // to the wrapper or else we crash dropping certain buttons on the
+ // palette due to removal of the command and disabled attributes - JRH
+ cleanUpItemForPalette(aPaletteItem, wrapper);
+
+ gPaletteBox.appendChild(wrapper);
+}
+
+/**
+ * Wraps an item that is currently on a toolbar and replaces the item
+ * with the wrapper. This is not used when dropping items from the palette,
+ * only when first starting the dialog and wrapping everything on the toolbars.
+ */
+function wrapToolbarItem(aToolbarItem) {
+ var wrapper = createWrapper(aToolbarItem.id, gToolboxDocument);
+
+ wrapper.flex = aToolbarItem.flex;
+
+ aToolbarItem.parentNode.replaceChild(wrapper, aToolbarItem);
+
+ wrapper.appendChild(aToolbarItem);
+
+ return wrapper;
+}
+
+/**
+ * Get the list of ids for the current set of items on each toolbar.
+ */
+function getCurrentItemIds() {
+ var currentItems = {};
+ forEachCustomizableToolbar(function(toolbar) {
+ var child = toolbar.firstElementChild;
+ while (child) {
+ if (isToolbarItem(child)) {
+ currentItems[child.id] = 1;
+ }
+ child = child.nextElementSibling;
+ }
+ });
+ return currentItems;
+}
+
+/**
+ * Builds the palette of draggable items that are not yet in a toolbar.
+ */
+function buildPalette() {
+ // Empty the palette first.
+ while (gPaletteBox.lastElementChild) {
+ gPaletteBox.lastChild.remove();
+ }
+
+ // Add the toolbar separator item.
+ var templateNode = document.createXULElement("toolbarseparator");
+ templateNode.id = "separator";
+ wrapPaletteItem(templateNode);
+
+ // Add the toolbar spring item.
+ templateNode = document.createXULElement("toolbarspring");
+ templateNode.id = "spring";
+ templateNode.flex = 1;
+ wrapPaletteItem(templateNode);
+
+ // Add the toolbar spacer item.
+ templateNode = document.createXULElement("toolbarspacer");
+ templateNode.id = "spacer";
+ templateNode.flex = 1;
+ wrapPaletteItem(templateNode);
+
+ var currentItems = getCurrentItemIds();
+ templateNode = gToolbox.palette.firstElementChild;
+ while (templateNode) {
+ // Check if the item is already in a toolbar before adding it to the
+ // palette, but do not add back separators, springs and spacers - we do
+ // not want them duplicated.
+ if (!isSpecialItem(templateNode) && !(templateNode.id in currentItems)) {
+ var paletteItem = document.importNode(templateNode, true);
+ wrapPaletteItem(paletteItem);
+ }
+
+ templateNode = templateNode.nextElementSibling;
+ }
+}
+
+/**
+ * Makes sure that an item that has been cloned from a template
+ * is stripped of any attributes that may adversely affect its
+ * appearance in the palette.
+ */
+function cleanUpItemForPalette(aItem, aWrapper) {
+ aWrapper.setAttribute("place", "palette");
+ setWrapperType(aItem, aWrapper);
+
+ if (aItem.hasAttribute("title")) {
+ aWrapper.setAttribute("title", aItem.getAttribute("title"));
+ } else if (aItem.hasAttribute("label")) {
+ aWrapper.setAttribute("title", aItem.getAttribute("label"));
+ } else if (isSpecialItem(aItem)) {
+ var stringBundle = document.getElementById("stringBundle");
+ // Remove the common "toolbar" prefix to generate the string name.
+ var title = stringBundle.getString(aItem.localName.slice(7) + "Title");
+ aWrapper.setAttribute("title", title);
+ }
+ aWrapper.setAttribute("tooltiptext", aWrapper.getAttribute("title"));
+
+ // Remove attributes that screw up our appearance.
+ aItem.removeAttribute("command");
+ aItem.removeAttribute("observes");
+ aItem.removeAttribute("type");
+ aItem.removeAttribute("width");
+ aItem.removeAttribute("checked");
+ aItem.removeAttribute("collapsed");
+
+ aWrapper.querySelectorAll("[disabled]").forEach(function(aNode) {
+ aNode.removeAttribute("disabled");
+ });
+}
+
+/**
+ * Makes sure that an item that has been cloned from a template
+ * is stripped of all properties that may adversely affect its
+ * appearance in the toolbar. Store critical properties on the
+ * wrapper so they can be put back on the item when we're done.
+ */
+function cleanupItemForToolbar(aItem, aWrapper) {
+ setWrapperType(aItem, aWrapper);
+ aWrapper.setAttribute("place", "toolbar");
+
+ if (aItem.hasAttribute("command")) {
+ aWrapper.setAttribute("itemcommand", aItem.getAttribute("command"));
+ aItem.removeAttribute("command");
+ }
+
+ if (aItem.hasAttribute("collapsed")) {
+ aWrapper.setAttribute("itemcollapsed", aItem.getAttribute("collapsed"));
+ aItem.removeAttribute("collapsed");
+ }
+
+ if (aItem.checked) {
+ aWrapper.setAttribute("itemchecked", "true");
+ aItem.checked = false;
+ }
+
+ if (aItem.disabled) {
+ aWrapper.setAttribute("itemdisabled", "true");
+ aItem.disabled = false;
+ }
+}
+
+/**
+ * Restore all the properties that we stripped off above.
+ */
+function restoreItemForToolbar(aItem, aWrapper) {
+ if (aWrapper.hasAttribute("itemdisabled")) {
+ aItem.disabled = true;
+ }
+
+ if (aWrapper.hasAttribute("itemchecked")) {
+ aItem.checked = true;
+ }
+
+ if (aWrapper.hasAttribute("itemcollapsed")) {
+ let collapsed = aWrapper.getAttribute("itemcollapsed");
+ aItem.setAttribute("collapsed", collapsed);
+ }
+
+ if (aWrapper.hasAttribute("itemcommand")) {
+ let commandID = aWrapper.getAttribute("itemcommand");
+ aItem.setAttribute("command", commandID);
+
+ // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
+ let command = gToolboxDocument.getElementById(commandID);
+ if (command && command.hasAttribute("disabled")) {
+ aItem.setAttribute("disabled", command.getAttribute("disabled"));
+ }
+ }
+}
+
+function setWrapperType(aItem, aWrapper) {
+ if (aItem.localName == "toolbarseparator") {
+ aWrapper.setAttribute("type", "separator");
+ } else if (aItem.localName == "toolbarspring") {
+ aWrapper.setAttribute("type", "spring");
+ } else if (aItem.localName == "toolbarspacer") {
+ aWrapper.setAttribute("type", "spacer");
+ } else if (aItem.localName == "toolbaritem" && aItem.firstElementChild) {
+ aWrapper.setAttribute("type", aItem.firstElementChild.localName);
+ }
+}
+
+function setDragActive(aItem, aValue) {
+ var node = aItem;
+ var direction = window.getComputedStyle(aItem).direction;
+ var value = direction == "ltr" ? "left" : "right";
+ if (aItem.localName == "toolbar") {
+ node = aItem.lastElementChild;
+ value = direction == "ltr" ? "right" : "left";
+ }
+
+ if (!node) {
+ return;
+ }
+
+ if (aValue) {
+ if (!node.hasAttribute("dragover")) {
+ node.setAttribute("dragover", value);
+ }
+ } else {
+ node.removeAttribute("dragover");
+ }
+}
+
+/**
+ * Restore the default set of buttons to fixed toolbars,
+ * remove all custom toolbars, and rebuild the palette.
+ */
+function restoreDefaultSet() {
+ // Unwrap the items on the toolbar.
+ unwrapToolbarItems();
+
+ // Remove all of the customized toolbars.
+ var child = gToolbox.lastElementChild;
+ while (child) {
+ if (child.hasAttribute("customindex")) {
+ var thisChild = child;
+ child = child.previousElementSibling;
+ thisChild.currentSet = "__empty";
+ gToolbox.removeChild(thisChild);
+ } else {
+ child = child.previousElementSibling;
+ }
+ }
+
+ // Restore the defaultset for fixed toolbars.
+ forEachCustomizableToolbar(function(toolbar) {
+ var defaultSet = toolbar.getAttribute("defaultset");
+ if (defaultSet) {
+ toolbar.currentSet = defaultSet;
+ }
+ });
+
+ // Restore the default icon size and mode.
+ document.getElementById("smallicons").checked = updateIconSize() == "small";
+ document.getElementById("modelist").value = updateToolbarMode();
+
+ // Now rebuild the palette.
+ buildPalette();
+
+ // Now re-wrap the items on the toolbar.
+ wrapToolbarItems();
+
+ toolboxChanged("reset");
+}
+
+function updateIconSize(aSize) {
+ return updateToolboxProperty("iconsize", aSize, "large");
+}
+
+function updateTitlebar() {
+ let titlebarCheckbox = document.getElementById("showTitlebar");
+ Services.prefs.setBoolPref(
+ "mail.tabs.drawInTitlebar",
+ !titlebarCheckbox.checked
+ );
+
+ // Bring the customizeToolbar window to front (on linux it's behind the main
+ // window). Otherwise the customization window gets left in the background.
+ setTimeout(() => window.focus(), 100);
+}
+
+function updateDragSpace() {
+ let dragSpaceCheckbox = document.getElementById("showDragSpace");
+ Services.prefs.setBoolPref(
+ "mail.tabs.extraDragSpace",
+ dragSpaceCheckbox.checked
+ );
+
+ // Bring the customizeToolbar window to front (on linux it's behind the main
+ // window). Otherwise the customization window gets left in the background.
+ setTimeout(() => window.focus(), 100);
+}
+
+function updateToolbarMode(aModeValue) {
+ var mode = updateToolboxProperty("mode", aModeValue, "icons");
+
+ var iconSizeCheckbox = document.getElementById("smallicons");
+ iconSizeCheckbox.disabled = mode == "text";
+
+ return mode;
+}
+
+function updateToolboxProperty(aProp, aValue, aToolkitDefault) {
+ var toolboxDefault =
+ gToolbox.getAttribute("default" + aProp) || aToolkitDefault;
+
+ gToolbox.setAttribute(aProp, aValue || toolboxDefault);
+ Services.xulStore.persist(gToolbox, aProp);
+
+ forEachCustomizableToolbar(function(toolbar) {
+ var toolbarDefault =
+ toolbar.getAttribute("default" + aProp) || toolboxDefault;
+ if (
+ toolbar.getAttribute("lock" + aProp) == "true" &&
+ toolbar.getAttribute(aProp) == toolbarDefault
+ ) {
+ return;
+ }
+
+ toolbar.setAttribute(aProp, aValue || toolbarDefault);
+ Services.xulStore.persist(toolbar, aProp);
+ });
+
+ toolboxChanged(aProp);
+
+ return aValue || toolboxDefault;
+}
+
+function forEachCustomizableToolbar(callback) {
+ if (window.frameElement && "externalToolbars" in window.frameElement) {
+ Array.from(window.frameElement.externalToolbars)
+ .filter(isCustomizableToolbar)
+ .forEach(callback);
+ } else if ("arguments" in window && window.arguments[1].length > 0) {
+ Array.from(window.arguments[1])
+ .filter(isCustomizableToolbar)
+ .forEach(callback);
+ }
+ Array.from(gToolbox.children)
+ .filter(isCustomizableToolbar)
+ .forEach(callback);
+}
+
+function isCustomizableToolbar(aElt) {
+ return (
+ aElt.localName == "toolbar" && aElt.getAttribute("customizable") == "true"
+ );
+}
+
+function isSpecialItem(aElt) {
+ return (
+ aElt.localName == "toolbarseparator" ||
+ aElt.localName == "toolbarspring" ||
+ aElt.localName == "toolbarspacer"
+ );
+}
+
+function isToolbarItem(aElt) {
+ return (
+ aElt.localName == "toolbarbutton" ||
+ aElt.localName == "toolbaritem" ||
+ aElt.localName == "toolbarseparator" ||
+ aElt.localName == "toolbarspring" ||
+ aElt.localName == "toolbarspacer"
+ );
+}
+
+// Drag and Drop observers
+
+function onToolbarDragExit(aEvent) {
+ if (isUnwantedDragEvent(aEvent)) {
+ return;
+ }
+
+ if (gCurrentDragOverItem) {
+ setDragActive(gCurrentDragOverItem, false);
+ }
+}
+
+function onToolbarDragStart(aEvent) {
+ var item = aEvent.target;
+ while (item && item.localName != "toolbarpaletteitem") {
+ if (item.localName == "toolbar") {
+ return;
+ }
+ item = item.parentNode;
+ }
+
+ item.setAttribute("dragactive", "true");
+
+ var dt = aEvent.dataTransfer;
+ var documentId = gToolboxDocument.documentElement.id;
+ dt.setData("text/toolbarwrapper-id/" + documentId, item.firstElementChild.id);
+ dt.effectAllowed = "move";
+}
+
+function onToolbarDragOver(aEvent) {
+ if (isUnwantedDragEvent(aEvent)) {
+ return;
+ }
+
+ var documentId = gToolboxDocument.documentElement.id;
+ if (
+ !aEvent.dataTransfer.types.includes(
+ "text/toolbarwrapper-id/" + documentId.toLowerCase()
+ )
+ ) {
+ return;
+ }
+
+ var toolbar = aEvent.target;
+ var dropTarget = aEvent.target;
+ while (toolbar && toolbar.localName != "toolbar") {
+ dropTarget = toolbar;
+ toolbar = toolbar.parentNode;
+ }
+
+ // Make sure we are dragging over a customizable toolbar.
+ if (!toolbar || !isCustomizableToolbar(toolbar)) {
+ gCurrentDragOverItem = null;
+ return;
+ }
+
+ var previousDragItem = gCurrentDragOverItem;
+
+ if (dropTarget.localName == "toolbar") {
+ gCurrentDragOverItem = dropTarget;
+ } else {
+ gCurrentDragOverItem = null;
+
+ var direction = window.getComputedStyle(dropTarget.parentNode).direction;
+ var boundingRect = dropTarget.getBoundingClientRect();
+ var dropTargetCenter = boundingRect.x + boundingRect.width / 2;
+ var dragAfter;
+ if (direction == "ltr") {
+ dragAfter = aEvent.clientX > dropTargetCenter;
+ } else {
+ dragAfter = aEvent.clientX < dropTargetCenter;
+ }
+
+ if (dragAfter) {
+ gCurrentDragOverItem = dropTarget.nextElementSibling;
+ if (!gCurrentDragOverItem) {
+ gCurrentDragOverItem = toolbar;
+ }
+ } else {
+ gCurrentDragOverItem = dropTarget;
+ }
+ }
+
+ if (previousDragItem && gCurrentDragOverItem != previousDragItem) {
+ setDragActive(previousDragItem, false);
+ }
+
+ setDragActive(gCurrentDragOverItem, true);
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+}
+
+function onToolbarDrop(aEvent) {
+ if (isUnwantedDragEvent(aEvent)) {
+ return;
+ }
+
+ if (!gCurrentDragOverItem) {
+ return;
+ }
+
+ setDragActive(gCurrentDragOverItem, false);
+
+ var documentId = gToolboxDocument.documentElement.id;
+ var draggedItemId = aEvent.dataTransfer.getData(
+ "text/toolbarwrapper-id/" + documentId
+ );
+ if (gCurrentDragOverItem.id == draggedItemId) {
+ return;
+ }
+
+ var toolbar = aEvent.target;
+ while (toolbar.localName != "toolbar") {
+ toolbar = toolbar.parentNode;
+ }
+
+ var draggedPaletteWrapper = document.getElementById(
+ "wrapper-" + draggedItemId
+ );
+ if (!draggedPaletteWrapper) {
+ // The wrapper has been dragged from the toolbar.
+ // Get the wrapper from the toolbar document and make sure that
+ // it isn't being dropped on itself.
+ let wrapper = gToolboxDocument.getElementById("wrapper-" + draggedItemId);
+ if (wrapper == gCurrentDragOverItem) {
+ return;
+ }
+
+ // Don't allow non-removable kids (e.g., the menubar) to move.
+ if (wrapper.firstElementChild.getAttribute("removable") != "true") {
+ return;
+ }
+
+ // Remove the item from its place in the toolbar.
+ wrapper.remove();
+
+ // Determine which toolbar we are dropping on.
+ var dropToolbar = null;
+ if (gCurrentDragOverItem.localName == "toolbar") {
+ dropToolbar = gCurrentDragOverItem;
+ } else {
+ dropToolbar = gCurrentDragOverItem.parentNode;
+ }
+
+ // Insert the item into the toolbar.
+ if (gCurrentDragOverItem != dropToolbar) {
+ dropToolbar.insertBefore(wrapper, gCurrentDragOverItem);
+ } else {
+ dropToolbar.appendChild(wrapper);
+ }
+ } else {
+ // The item has been dragged from the palette
+
+ // Create a new wrapper for the item. We don't know the id yet.
+ let wrapper = createWrapper("", gToolboxDocument);
+
+ // Ask the toolbar to clone the item's template, place it inside the wrapper, and insert it in the toolbar.
+ var newItem = toolbar.insertItem(
+ draggedItemId,
+ gCurrentDragOverItem == toolbar ? null : gCurrentDragOverItem,
+ wrapper
+ );
+
+ // Prepare the item and wrapper to look good on the toolbar.
+ cleanupItemForToolbar(newItem, wrapper);
+ wrapper.id = "wrapper-" + newItem.id;
+ wrapper.flex = newItem.flex;
+
+ // Remove the wrapper from the palette.
+ if (
+ draggedItemId != "separator" &&
+ draggedItemId != "spring" &&
+ draggedItemId != "spacer"
+ ) {
+ gPaletteBox.removeChild(draggedPaletteWrapper);
+ }
+ }
+
+ gCurrentDragOverItem = null;
+
+ toolboxChanged();
+}
+
+function onPaletteDragOver(aEvent) {
+ if (isUnwantedDragEvent(aEvent)) {
+ return;
+ }
+ var documentId = gToolboxDocument.documentElement.id;
+ if (
+ aEvent.dataTransfer.types.includes(
+ "text/toolbarwrapper-id/" + documentId.toLowerCase()
+ )
+ ) {
+ aEvent.preventDefault();
+ }
+}
+
+function onPaletteDrop(aEvent) {
+ if (isUnwantedDragEvent(aEvent)) {
+ return;
+ }
+ var documentId = gToolboxDocument.documentElement.id;
+ var itemId = aEvent.dataTransfer.getData(
+ "text/toolbarwrapper-id/" + documentId
+ );
+
+ var wrapper = gToolboxDocument.getElementById("wrapper-" + itemId);
+ if (wrapper) {
+ // Don't allow non-removable kids (e.g., the menubar) to move.
+ if (wrapper.firstElementChild.getAttribute("removable") != "true") {
+ return;
+ }
+
+ var wrapperType = wrapper.getAttribute("type");
+ if (
+ wrapperType != "separator" &&
+ wrapperType != "spacer" &&
+ wrapperType != "spring"
+ ) {
+ restoreItemForToolbar(wrapper.firstElementChild, wrapper);
+ wrapPaletteItem(document.importNode(wrapper.firstElementChild, true));
+ gToolbox.palette.appendChild(wrapper.firstElementChild);
+ }
+
+ // The item was dragged out of the toolbar.
+ wrapper.remove();
+ }
+
+ toolboxChanged();
+}
+
+function isUnwantedDragEvent(aEvent) {
+ try {
+ if (
+ Services.prefs.getBoolPref("toolkit.customization.unsafe_drag_events")
+ ) {
+ return false;
+ }
+ } catch (ex) {}
+
+ /* Discard drag events that originated from a separate window to
+ prevent content->chrome privilege escalations. */
+ let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
+ // mozSourceNode is null in the dragStart event handler or if
+ // the drag event originated in an external application.
+ if (!mozSourceNode) {
+ return true;
+ }
+ let sourceWindow = mozSourceNode.ownerGlobal;
+ return sourceWindow != window && sourceWindow != gToolboxDocument.defaultView;
+}
diff --git a/comm/suite/components/customizeToolbar.xhtml b/comm/suite/components/customizeToolbar.xhtml
new file mode 100644
index 0000000000..2888272901
--- /dev/null
+++ b/comm/suite/components/customizeToolbar.xhtml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog [
+<!ENTITY % customizeToolbarDTD SYSTEM
+#ifdef MOZ_SUITE
+ "chrome://communicator/locale/customizeToolbar.dtd">
+#else
+ "chrome://messenger/locale/customizeToolbar.dtd">
+#endif
+ %customizeToolbarDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+#ifdef MOZ_SUITE
+<?xml-stylesheet href="chrome://communicator/content/customizeToolbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/customizeToolbar.css" type="text/css"?>
+#else
+<?xml-stylesheet href="chrome://messenger/content/customizeToolbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/customizeToolbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/content/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messageHeader.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/primaryToolbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/chat.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressbook.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messengercompose/messengercompose.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+#endif
+<?xml-stylesheet href="chrome://calendar/skin/calendar-task-view.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/shared/dialogs/calendar-event-dialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/calendar-toolbar.css" type="text/css"?>
+
+<window id="CustomizeToolbarWindow"
+ title="&dialog.title;"
+#ifdef MOZ_SUITE
+ onload="onLoad();"
+#else
+ lightweightthemes="true"
+ windowtype="mailnews:customizeToolbar"
+ onload="overlayOnLoad();"
+#endif
+ onunload="onUnload();"
+ style="&dialog.dimensions;"
+ persist="width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+#ifdef MOZ_SUITE
+<script src="chrome://communicator/content/customizeToolbar.js"/>
+<stringbundle id="stringBundle" src="chrome://communicator/locale/customizeToolbar.properties"/>
+#else
+<script src="chrome://messenger/content/customizeToolbar.js"/>
+<script src="chrome://messenger/content/mailCore.js"/>
+<stringbundle id="stringBundle" src="chrome://messenger/locale/customizeToolbar.properties"/>
+#endif
+
+<keyset id="CustomizeToolbarKeyset">
+ <key id="cmd_close1" keycode="VK_ESCAPE" oncommand="onClose();"/>
+ <key id="cmd_close2" keycode="VK_RETURN" oncommand="onClose();"/>
+</keyset>
+
+<vbox id="main-box" flex="1">
+ <description id="instructions">
+ &instructions.description;
+ </description>
+
+ <vbox flex="1" id="palette-box"
+ ondragstart="onToolbarDragStart(event)"
+ ondragover="onPaletteDragOver(event)"
+ ondrop="onPaletteDrop(event)"/>
+
+ <hbox id="buttonBox" align="center">
+#ifndef MOZ_SUITE
+ <hbox id="titlebarSettings" hidden="true">
+ <checkbox id="showTitlebar" oncommand="updateTitlebar();" label="&showTitlebar2.label;"/>
+ <checkbox id="showDragSpace" oncommand="updateDragSpace();" label="&extraDragSpace2.label;"/>
+ </hbox>
+#endif
+ <label id="modelistLabel" value="&show.label;" control="modelist"/>
+ <menulist id="modelist"
+ value="icons"
+#ifdef MOZ_SUITE
+ oncommand="updateToolbarMode(this.value);">
+#else
+ oncommand="overlayUpdateToolbarMode(this.value, 'mail-toolbox');">
+#endif
+ <menupopup id="modelistpopup">
+ <menuitem id="modefull" value="full" label="&iconsAndText.label;"/>
+ <menuitem id="modeicons" value="icons" label="&icons.label;"/>
+ <menuitem id="modetext" value="text" label="&text.label;"/>
+#ifndef MOZ_SUITE
+ <menuitem id="textbesideiconItem" value="textbesideicon" label="&iconsBesideText.label;"/>
+#endif
+ </menupopup>
+ </menulist>
+ <checkbox id="smallicons" oncommand="updateIconSize(this.checked ? 'small' : 'large');" label="&useSmallIcons.label;"/>
+ </hbox>
+ <hbox align="center">
+ <button id="restoreDefault" label="&restoreDefaultSet.label;" oncommand="restoreDefaultSet();"/>
+ <spacer flex="1"/>
+ <button id="donebutton" label="&saveChanges.label;" oncommand="onClose();"
+ default="true"/>
+ </hbox>
+</vbox>
+
+</window>
diff --git a/comm/suite/components/dataman/content/dataman.css b/comm/suite/components/dataman/content/dataman.css
new file mode 100644
index 0000000000..0e47f37674
--- /dev/null
+++ b/comm/suite/components/dataman/content/dataman.css
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace xhtml "http://www.w3.org/1999/xhtml";
+
+/* HTML link elements do weird things to the layout if they are not hidden */
+xhtml|link {
+ display: none;
+}
+
+/* generic item gets used for permissions that don't need any special treatment */
+richlistitem.permission {
+ -moz-binding: url('chrome://communicator/content/dataman/dataman.xml#perm-generic-item');
+ -moz-box-orient: vertical;
+}
+
+/* cookie item has an allow for session option */
+richlistitem.permission[type="cookie"] {
+ -moz-binding: url('chrome://communicator/content/dataman/dataman.xml#perm-cookie-item');
+}
+
+/* geolocation and indexedDB items default to always ask */
+richlistitem.permission[type="geo"],
+richlistitem.permission[type="indexedDB"] {
+ -moz-binding: url('chrome://communicator/content/dataman/dataman.xml#perm-geo-item');
+}
+
+/* content blocker items have an allow for same domain option */
+richlistitem.permission[type="script"],
+richlistitem.permission[type="image"],
+richlistitem.permission[type="stylesheet"],
+richlistitem.permission[type="object"],
+richlistitem.permission[type="document"],
+richlistitem.permission[type="subdocument"],
+richlistitem.permission[type="refresh"],
+richlistitem.permission[type="xbl"],
+richlistitem.permission[type="ping"],
+richlistitem.permission[type="xmlhttprequest"],
+richlistitem.permission[type="objectsubrequest"],
+richlistitem.permission[type="dtd"],
+richlistitem.permission[type="font"],
+richlistitem.permission[type="media"] {
+ -moz-binding: url('chrome://communicator/content/dataman/dataman.xml#perm-content-item');
+}
diff --git a/comm/suite/components/dataman/content/dataman.js b/comm/suite/components/dataman/content/dataman.js
new file mode 100644
index 0000000000..be3063a81f
--- /dev/null
+++ b/comm/suite/components/dataman/content/dataman.js
@@ -0,0 +1,3270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Async} = ChromeUtils.import("resource://services-common/async.js");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+// Load DownloadUtils module for convertByteUnits
+const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+
+// locally loaded services
+var gLocSvc = {};
+ChromeUtils.defineModuleGetter(gLocSvc, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm",
+ "FormHistory");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "url",
+ "@mozilla.org/network/url-parser;1?auth=maybe",
+ "nsIURLParser");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "clipboard",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "idn",
+ "@mozilla.org/network/idn-service;1",
+ "nsIIDNService");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "appcache",
+ "@mozilla.org/network/application-cache-service;1",
+ "nsIApplicationCacheService");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "domstoremgr",
+ "@mozilla.org/dom/storagemanager;1",
+ "nsIDOMStorageManager");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "idxdbmgr",
+ "@mozilla.org/dom/indexeddb/manager;1",
+ "nsIIndexedDatabaseManager");
+XPCOMUtils.defineLazyServiceGetter(gLocSvc, "ssm",
+ "@mozilla.org/scriptsecuritymanager;1",
+ "nsIScriptSecurityManager");
+
+// From nsContentBlocker.cpp
+const NOFOREIGN = 3;
+
+// :::::::::::::::::::: general functions ::::::::::::::::::::
+var gDataman = {
+ bundle: null,
+ debug: false,
+ timer: null,
+ viewToLoad: ["*", "formdata"],
+
+ initialize: function dataman_initialize() {
+ try {
+ this.debug = Services.prefs.getBoolPref("data_manager.debug");
+ }
+ catch (e) {}
+ this.bundle = document.getElementById("datamanBundle");
+
+ Services.obs.addObserver(this, "cookie-changed");
+ Services.obs.addObserver(this, "perm-changed");
+ Services.obs.addObserver(this, "passwordmgr-storage-changed");
+ Services.contentPrefs2.addObserverForName(null, this);
+ Services.obs.addObserver(this, "satchel-storage-changed");
+ Services.obs.addObserver(this, "dom-storage-changed");
+ Services.obs.addObserver(this, "dom-storage2-changed");
+
+ this.timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+
+ gTabs.initialize();
+ gDomains.initialize();
+
+ if ("arguments" in window &&
+ window.arguments.length >= 1 &&
+ window.arguments[0]) {
+ this.loadView(window.arguments[0])
+ }
+ },
+
+ shutdown: function dataman_shutdown() {
+ Services.obs.removeObserver(this, "cookie-changed");
+ Services.obs.removeObserver(this, "perm-changed");
+ Services.obs.removeObserver(this, "passwordmgr-storage-changed");
+ Services.contentPrefs2.removeObserverForName(null, this);
+ Services.obs.removeObserver(this, "satchel-storage-changed");
+ Services.obs.removeObserver(this, "dom-storage-changed");
+ Services.obs.removeObserver(this, "dom-storage2-changed");
+
+ gDomains.shutdown();
+ },
+
+ loadView: function dataman_loadView(aView) {
+ // Set variable, used in initizalization routine.
+ // Syntax: <domain>|<pane> (|<pane> is optional)
+ // Examples: example.com
+ // example.org|permissions
+ // example.org:8888|permissions|add|popup
+ // |cookies
+ // Allowed pane names:
+ // cookies, permissions, preferences, passwords, formdata
+ // Invalid views fall back to the default available ones
+ // Full host names (even including ports) for domain are allowed
+ // Empty domain with a pane specified will only list this data type
+ // Permissions allow specifying "add" and type to prefill the adding field
+ this.viewToLoad = aView.split('|');
+ if (gDomains.listLoadCompleted)
+ gDomains.loadView();
+ // Else will call this at the end of loading the list.
+ },
+
+ handleKeyPress: function dataman_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE &&
+ gTabs.tabbox.selectedPanel &&
+ gTabs.tabbox.selectedPanel.id == "forgetPanel") {
+ gForget.handleKeyPress(aEvent);
+ }
+ },
+
+ debugMsg: function dataman_debugMsg(aLogMessage) {
+ if (this.debug)
+ Services.console.logStringMessage(aLogMessage);
+ },
+
+ debugError: function dataman_debugError(aLogMessage) {
+ if (this.debug)
+ Cu.reportError(aLogMessage);
+ },
+
+ // :::::::::: data change observers ::::::::::
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIContentPrefObserver]),
+
+ observe: function co_observe(aSubject, aTopic, aData) {
+ gDataman.debugMsg("Observed: " + aTopic + " - " + aData);
+ switch (aTopic) {
+ case "cookie-changed":
+ gCookies.reactToChange(aSubject, aData);
+ break;
+ case "perm-changed":
+ gPerms.reactToChange(aSubject, aData);
+ break;
+ case "passwordmgr-storage-changed":
+ gPasswords.reactToChange(aSubject, aData);
+ break;
+ case "satchel-storage-changed":
+ gFormdata.reactToChange(aSubject, aData);
+ break;
+ case "dom-storage2-changed": // sessionStorage, localStorage
+ gStorage.reactToChange(aSubject, aData);
+ break;
+ default:
+ gDataman.debugError("Unexpected change topic observed: " + aTopic);
+ break;
+ }
+ },
+
+ // Compat with nsITimerCallback so we can be used in a timer.
+ notify: function(timer) {
+ gDataman.debugMsg("Timer fired, reloading storage: " + Date.now()/1000);
+ gStorage.reloadList();
+ },
+
+ onContentPrefSet: function co_onContentPrefSet(aGroup, aName, aValue) {
+ gDataman.debugMsg("Observed: content pref set");
+ gPrefs.reactToChange({host: aGroup, name: aName, value: aValue}, "prefSet");
+ },
+
+ onContentPrefRemoved: function co_onContentPrefRemoved(aGroup, aName) {
+ gDataman.debugMsg("Observed: content pref removed");
+ gPrefs.reactToChange({host: aGroup, name: aName}, "prefRemoved");
+ },
+
+ // :::::::::: utility functions ::::::::::
+ getTreeSelections: function dataman_getTreeSelections(aTree) {
+ let selections = [];
+ let select = aTree.view.selection;
+ if (select && aTree.view.rowCount) {
+ let count = select.getRangeCount();
+ let min = {};
+ let max = {};
+ for (let i = 0; i < count; i++) {
+ select.getRangeAt(i, min, max);
+ for (let k = min.value; k <= max.value; k++)
+ if (k != -1)
+ selections.push(k);
+ }
+ }
+ return selections;
+ },
+
+ getSelectedIDs: function dataman_getSelectedIDs(aTree, aIDFunction) {
+ // Get IDs of selected elements for later restoration.
+ let selectionCache = [];
+ if (aTree.view.selection.count < 1 || aTree.view.rowCount < 1)
+ return selectionCache;
+
+ // Walk all selected rows and cache their IDs.
+ let start = {};
+ let end = {};
+ let numRanges = aTree.view.selection.getRangeCount();
+ for (let rg = 0; rg < numRanges; rg++){
+ aTree.view.selection.getRangeAt(rg, start, end);
+ for (let row = start.value; row <= end.value; row++)
+ selectionCache.push(aIDFunction(row));
+ }
+ return selectionCache;
+ },
+
+ restoreSelectionFromIDs: function dataman_restoreSelectionFromIDs(aTree, aIDFunction, aCachedIDs) {
+ // Restore selection from cached IDs (as possible).
+ if (!aCachedIDs.length)
+ return;
+
+ aTree.view.selection.clearSelection();
+ // Find out which current rows match a cached selection and add them to the selection.
+ for (let row = 0; row < aTree.view.rowCount; row++)
+ if (aCachedIDs.includes(aIDFunction(row)))
+ aTree.view.selection.toggleSelect(row);
+ },
+}
+
+// :::::::::::::::::::: base object to use as a prototype for all others ::::::::::::::::::::
+var gBaseTreeView = {
+ setTree: function(aTree) {},
+ getImageSrc: function(aRow, aColumn) {},
+ getProgressMode: function(aRow, aColumn) {},
+ getCellValue: function(aRow, aColumn) {},
+ isSeparator: function(aIndex) { return false; },
+ isSorted: function() { return false; },
+ isContainer: function(aIndex) { return false; },
+ cycleHeader: function(aCol) {},
+ getRowProperties: function(aRow) { return ""; },
+ getColumnProperties: function(aColumn) { return ""; },
+ getCellProperties: function(aRow, aColumn) { return ""; }
+};
+
+// :::::::::::::::::::: domain list ::::::::::::::::::::
+var gDomains = {
+ tree: null,
+ selectfield: null,
+ searchfield: null,
+
+ domains: {},
+ domainObjects: {},
+ displayedDomains: [],
+ selectedDomain: {},
+ xlcache: {},
+
+ ignoreSelect: false,
+ ignoreUpdate: false,
+ listLoadCompleted: false,
+
+ initialize: function domain_initialize() {
+ gDataman.debugMsg("Start building domain list: " + Date.now()/1000);
+
+ this.tree = document.getElementById("domainTree");
+ this.tree.view = this;
+
+ this.selectfield = document.getElementById("typeSelect");
+ this.searchfield = document.getElementById("domainSearch");
+
+ // global "domain"
+ Services.contentPrefs2.hasPrefs(null, null, {
+ handleResult(resultPref) {
+ gDomains.domainObjects["*"] = {title: "*",
+ displayTitle: "*",
+ hasPermissions: true,
+ hasPreferences: resultPref.value,
+ hasFormData: true};
+ },
+ handleCompletion: () => {
+ },
+ });
+
+ this.search("");
+ if (!gDataman.viewToLoad.length)
+ this.tree.view.selection.select(0);
+
+ let loaderInstance;
+
+ function nextStep() {
+ loaderInstance.next();
+ }
+
+ function* loader() {
+ // Add domains for all cookies we find.
+ gDataman.debugMsg("Add cookies to domain list: " + Date.now()/1000);
+ gDomains.ignoreUpdate = true;
+ gCookies.loadList();
+ for (let cookie of gCookies.cookies)
+ gDomains.addDomainOrFlag(cookie.rawHost, "hasCookies");
+ gDomains.ignoreUpdate = false;
+ gDomains.search(gDomains.searchfield.value);
+ yield setTimeout(nextStep, 0);
+
+ // Add domains for permissions.
+ gDataman.debugMsg("Add permissions to domain list: " + Date.now()/1000);
+ gDomains.ignoreUpdate = true;
+ let enumerator = Services.perms.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let nextPermission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
+
+ if (!gDomains.commonScheme(nextPermission.principal.URI.scheme)) {
+ gDomains.addDomainOrFlag("*", "hasPermissions");
+ }
+ else {
+ gDomains.addDomainOrFlag(nextPermission.principal.URI.host.replace(/^\./, ""), "hasPermissions");
+ }
+ }
+ gDomains.ignoreUpdate = false;
+ gDomains.search(gDomains.searchfield.value);
+ yield setTimeout(nextStep, 0);
+
+ let domains = [];
+ Services.contentPrefs2.getDomains(null, {
+ handleResult(resultPref) {
+ domains.push(resultPref.domain);
+ },
+ handleCompletion: () => {
+ // Add domains for content prefs.
+ gDataman.debugMsg("Add content prefs to domain list: " +
+ Date.now()/1000);
+ gDomains.ignoreUpdate = true;
+ for (let domain of domains) {
+ gDataman.debugMsg("Found pref: " + domain);
+ let prefHost = gDomains.getDomainFromHostWithCheck(domain);
+ gDomains.addDomainOrFlag(prefHost, "hasPreferences");
+ }
+ gDomains.ignoreUpdate = false;
+ gDomains.search(gDomains.searchfield.value);
+ },
+ });
+
+ // Add domains for passwords.
+ gDataman.debugMsg("Add passwords to domain list: " + Date.now()/1000);
+ gDomains.ignoreUpdate = true;
+ gPasswords.loadList();
+ for (let pSignon of gPasswords.allSignons) {
+ gDomains.addDomainOrFlag(pSignon.hostname, "hasPasswords");
+ }
+ gDomains.ignoreUpdate = false;
+ gDomains.search(gDomains.searchfield.value);
+ yield setTimeout(nextStep, 0);
+
+ // Add domains for web storages.
+ gDataman.debugMsg("Add storages to domain list: " + Date.now()/1000);
+ // Force DOM Storage to write its data to the disk.
+ Services.obs.notifyObservers(window, "domstorage-flush-timer");
+ yield setTimeout(nextStep, 0);
+ gStorage.loadList();
+ for (let sStorage of gStorage.storages) {
+ gDomains.addDomainOrFlag(sStorage.rawHost, "hasStorage");
+ }
+ gDomains.search(gDomains.searchfield.value);
+ // As we don't get notified of storage changes properly, reload on timer.
+ // The repeat time is in milliseconds, we're using 10 min for now.
+ gDataman.timer.initWithCallback(gDataman, 10 * 60000,
+ Ci.nsITimer.TYPE_REPEATING_SLACK);
+ yield setTimeout(nextStep, 0);
+
+ gDataman.debugMsg("Domain list built: " + Date.now()/1000);
+ gDomains.listLoadCompleted = true;
+ gDomains.loadView();
+ yield undefined;
+ }
+
+ loaderInstance = loader();
+ setTimeout(nextStep, 0);
+ },
+
+ shutdown: function domain_shutdown() {
+ gDataman.timer.cancel();
+ gTabs.shutdown();
+ this.tree.view = null;
+ },
+
+ loadView: function domain_loadView() {
+ // Load the view set in the dataman object.
+ gDataman.debugMsg("Load View: " + gDataman.viewToLoad.join(", "));
+ let loaderInstance;
+ function nextStep() {
+ loaderInstance.next();
+ }
+
+ function* loader() {
+ if (gDataman.viewToLoad.length) {
+ if (gDataman.viewToLoad[0] == "" && gDataman.viewToLoad.length > 1) {
+ let sType = gDataman.viewToLoad[1].substr(0,1).toUpperCase() +
+ gDataman.viewToLoad[1].substr(1);
+ gDataman.debugMsg("Select a specific data type: " + sType);
+ gDomains.selectfield.value = sType;
+ gDomains.selectType(sType);
+ yield setTimeout(nextStep, 0);
+
+ if (gDomains.tree.view.rowCount) {
+ // Select first domain and panel fitting selected type.
+ gDomains.tree.view.selection.select(0);
+ gDomains.tree.treeBoxObject.ensureRowIsVisible(0);
+ yield setTimeout(nextStep, 0);
+
+ // This should always exist and be enabled, but play safe.
+ let loadTabID = gDataman.viewToLoad[1] + "Tab";
+ if (gTabs[loadTabID] && !gTabs[loadTabID].disabled)
+ gTabs.tabbox.selectedTab = gTabs[loadTabID];
+ }
+ }
+ else {
+ gDataman.debugMsg("Domain for view found");
+ gDomains.selectfield.value = "all";
+ gDomains.selectType("all");
+ let host = gDataman.viewToLoad[0];
+
+ // Might have a host:port case, fake a scheme when none present.
+ if (!/:\//.test(host))
+ host = "foo://" + host;
+
+ gDataman.debugMsg("host: " + host);
+
+ let viewdomain = "*";
+
+ // avoid error message in log for the generic entry
+ if (host != "foo://*")
+ viewdomain = gDomains.getDomainFromHost(host);
+
+ gDataman.debugMsg("viewDomain: " + viewdomain);
+
+ let selectIdx = 0; // tree index to be selected
+ for (let i = 0; i < gDomains.displayedDomains.length; i++) {
+ if (gDomains.displayedDomains[i].title == viewdomain) {
+ selectIdx = i;
+ break;
+ }
+ }
+
+ let permAdd = (gDataman.viewToLoad[1] &&
+ gDataman.viewToLoad[1] == "permissions" &&
+ gDataman.viewToLoad[2] &&
+ gDataman.viewToLoad[2] == "add");
+ if (permAdd && selectIdx != 0 &&
+ (!(viewdomain in gDomains.domainObjects) ||
+ !gDomains.domainObjects[viewdomain].hasPermissions)) {
+ selectIdx = 0; // Force * domain as we have a perm panel there.
+ }
+
+ if (gDomains.tree.currentIndex != selectIdx) {
+ gDomains.tree.view.selection.select(selectIdx);
+ gDomains.tree.treeBoxObject.ensureRowIsVisible(selectIdx);
+ }
+ yield setTimeout(nextStep, 0);
+
+ if (gDataman.viewToLoad.length > 1) {
+ gDataman.debugMsg("Pane for view found");
+ let loadTabID = gDataman.viewToLoad[1] + "Tab";
+ if (gTabs[loadTabID] && !gTabs[loadTabID].disabled)
+ gTabs.tabbox.selectedTab = gTabs[loadTabID];
+
+ yield setTimeout(nextStep, 0);
+
+ if (permAdd) {
+ gDataman.debugMsg("Adding permission");
+ if (gPerms.addSelBox.hidden)
+ gPerms.addButtonClick();
+ gPerms.addHost.value = gDataman.viewToLoad[0];
+ if (gDataman.viewToLoad[3])
+ gPerms.addType.value = gDataman.viewToLoad[3];
+ gPerms.addCheck();
+ gPerms.addButton.focus();
+ }
+ }
+ }
+ }
+ yield setTimeout(nextStep, 0);
+
+ // Send a notification that we have finished.
+ Services.obs.notifyObservers(window, "dataman-loaded");
+ yield undefined;
+ }
+
+ loaderInstance = loader();
+ setTimeout(nextStep, 0);
+ },
+
+ _getObjID: function domain__getObjID(aIdx) {
+ return gDomains.displayedDomains[aIdx].title;
+ },
+
+ getDomainFromHostWithCheck: function domain_getDomainFromHostWithCheck(aHost) {
+ // Global content pref changes and others might not have a host.
+ if (!aHost) {
+ return '*';
+ }
+
+ let host = gDomains.getDomainFromHost(aHost);
+ // Host couldn't be found or is an internal page or data.
+ if (!host ||
+ host.trim().length == 0 ||
+ aHost.startsWith("about:") ||
+ aHost.startsWith("jar:"))
+ return '*';
+
+ return host.trim();
+ },
+
+ getDomainFromHost: function domain_getDomainFromHost(aHostname) {
+ // Find the base domain name for the given host name.
+ if (!this.xlcache[aHostname]) {
+ // aHostname is not always an actual host name, but potentially something
+ // URI-like, e.g. gopher://example.com and newURI doesn't work there as we
+ // need to display entries for schemes that are not supported (any more).
+ // nsIURLParser is a fast way to generically ensure a pure host name.
+ var hostName;
+ // Return vars for nsIURLParser must all be objects,
+ // see bug 568997 for improvements to that interface.
+ var schemePos = {}, schemeLen = {}, authPos = {}, authLen = {}, pathPos = {},
+ pathLen = {}, usernamePos = {}, usernameLen = {}, passwordPos = {},
+ passwordLen = {}, hostnamePos = {}, hostnameLen = {}, port = {};
+ try {
+ gLocSvc.url.parseURL(aHostname, -1, schemePos, schemeLen, authPos, authLen,
+ pathPos, pathLen);
+ var auth = aHostname.substring(authPos.value, authPos.value + authLen.value);
+ gLocSvc.url.parseAuthority(auth, authLen.value, usernamePos, usernameLen,
+ passwordPos, passwordLen, hostnamePos, hostnameLen, port);
+ hostName = auth.substring(hostnamePos.value, hostnamePos.value + hostnameLen.value);
+ }
+ catch (e) {
+ // IPv6 host names can come in without [] around them and therefore
+ // cause an error. Those consist of at least two colons and else only
+ // hexadecimal digits. Fix them by putting [] around them.
+ if (/^[a-f0-9]*:[a-f0-9]*:[a-f0-9:]*$/.test(aHostname)) {
+ gDataman.debugMsg("bare IPv6 address found: " + aHostname);
+ hostName = "[" + aHostname + "]";
+ }
+ else {
+ gDataman.debugError("Error while trying to get hostname from input: " + aHostname);
+ gDataman.debugError(e);
+ hostName = aHostname;
+ }
+ }
+
+ var domain;
+ try {
+ domain = Services.eTLD.getBaseDomainFromHost(hostName);
+ }
+ catch (e) {
+ gDataman.debugMsg("Unable to get domain from host name: " + hostName);
+ domain = hostName;
+ }
+ this.xlcache[aHostname] = domain;
+ gDataman.debugMsg("cached: " + aHostname + " -> " + this.xlcache[aHostname]);
+ } // end hostname not cached
+ return this.xlcache[aHostname];
+ },
+
+ // Used for checking if * global data domain should be used.
+ commonScheme: function domain_commonScheme(aScheme) {
+ // case intensitive search for domain schemes
+ return /^(https?|ftp|gopher)/i.test(aScheme);
+ },
+
+ hostMatchesSelected: function domain_hostMatchesSelected(aHostname) {
+ return this.getDomainFromHost(aHostname) == this.selectedDomain.title;
+ },
+
+ hostMatchesSelectedURI: function domain_hostMatchesSelectedURI(aURI) {
+ // default to * global data domain.
+ let mScheme = "*";
+
+ // First, try to get the scheme.
+ try {
+ mScheme = aURI.scheme;
+ }
+ catch (e) {
+ gDataman.debugError("Invalid permission found: " + aUri);
+ }
+
+ // See if his is a scheme which does not go into the global data domain.
+ if (!this.commonScheme(mScheme)) {
+ return ("*") == this.selectedDomain.title;
+ }
+
+ rawHost = aURI.host.replace(/^\./, "");
+ return this.getDomainFromHost(rawHost) == this.selectedDomain.title;
+ },
+
+ addDomainOrFlag: function domain_addDomainOrFlag(aHostname, aFlag) {
+ let domain;
+ // For existing domains, add flags, for others, add them to the object.
+ if (aHostname == "*")
+ domain = aHostname;
+ else
+ domain = this.getDomainFromHost(aHostname);
+
+ if (!this.domainObjects[domain]) {
+ this.domainObjects[domain] = {title: domain};
+ if (/xn--/.test(domain))
+ this.domainObjects[domain].displayTitle = gLocSvc.idn.convertToDisplayIDN(domain, {});
+ else
+ this.domainObjects[domain].displayTitle = this.domainObjects[domain].title;
+ this.domainObjects[domain][aFlag] = true;
+ gDataman.debugMsg("added domain: " + domain + " (with flag " + aFlag + ")");
+ if (!this.ignoreUpdate)
+ this.search(this.searchfield.value);
+ }
+ else if (!this.domainObjects[domain][aFlag]) {
+ this.domainObjects[domain][aFlag] = true;
+ gDataman.debugMsg("added flag " + aFlag + " to " + domain);
+ if (domain == this.selectedDomain.title) {
+ // Just update the tab states.
+ this.select(true);
+ }
+ }
+ },
+
+ removeDomainOrFlag: function domain_removeDomainOrFlag(aDomain, aFlag) {
+ // Remove a flag from the given domain,
+ // remove the whole domain if it doesn't have any flags left.
+ if (!this.domainObjects[aDomain])
+ return;
+
+ gDataman.debugMsg("removed flag " + aFlag + " from " + aDomain);
+ this.domainObjects[aDomain][aFlag] = false;
+ if (!this.domainObjects[aDomain].hasCookies &&
+ !this.domainObjects[aDomain].hasPermissions &&
+ !this.domainObjects[aDomain].hasPreferences &&
+ !this.domainObjects[aDomain].hasPasswords &&
+ !this.domainObjects[aDomain].hasStorage &&
+ !this.domainObjects[aDomain].hasFormData) {
+ gDataman.debugMsg("removed domain: " + aDomain);
+ // Get index in display tree.
+ let disp_idx = -1;
+ for (let i = 0; i < this.displayedDomains.length; i++) {
+ if (this.displayedDomains[i] == this.domainObjects[aDomain]) {
+ disp_idx = i;
+ break;
+ }
+ }
+ this.displayedDomains.splice(disp_idx, 1);
+ this.tree.treeBoxObject.rowCountChanged(disp_idx, -1);
+ delete this.domainObjects[aDomain];
+ // Make sure we clear the data pane when selection has been removed.
+ if (!this.tree.view.selection.count)
+ this.select();
+ }
+ else {
+ // Just update the tab states.
+ this.select(true);
+ }
+ },
+
+ resetFlagToDomains: function domain_resetFlagToDomains(aFlag, aDomainList) {
+ // Reset a flag to be only set on a specific set of domains,
+ // purging then-emtpy domain in the process.
+ // Needed when we need to reload a complete set of items.
+ gDataman.debugMsg("resetting domains for flag: " + aFlag);
+ this.ignoreSelect = true;
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ this.tree.view.selection.clearSelection();
+ // First, clear all domains of this flag.
+ for (let domain in this.domainObjects) {
+ this.domainObjects[domain][aFlag] = false;
+ }
+ // Then, set it again on all domains in the new list.
+ for (let domain of aDomainList) {
+ this.addDomainOrFlag(domain, aFlag);
+ }
+ // Now, purge all empty domains.
+ for (let domain in this.domainObjects) {
+ if (!this.domainObjects[domain].hasCookies &&
+ !this.domainObjects[domain].hasPermissions &&
+ !this.domainObjects[domain].hasPreferences &&
+ !this.domainObjects[domain].hasPasswords &&
+ !this.domainObjects[domain].hasStorage &&
+ !this.domainObjects[domain].hasFormData) {
+ delete this.domainObjects[domain];
+ }
+ }
+ this.search(this.searchfield.value);
+ this.ignoreSelect = false;
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ // Make sure we clear the data pane when selection has been removed.
+ if (!this.tree.view.selection.count && selectionCache.length)
+ this.select();
+ },
+
+ select: function domain_select(aNoTabSelect) {
+ if (this.ignoreSelect) {
+ if (this.tree.view.selection.count == 1)
+ this.selectedDomain = this.displayedDomains[this.tree.currentIndex];
+ return;
+ }
+
+ gDataman.debugMsg("Domain selected: " + Date.now()/1000);
+
+ if (!this.tree.view.selection.count) {
+ gTabs.cookiesTab.disabled = true;
+ gTabs.permissionsTab.disabled = true;
+ gTabs.preferencesTab.disabled = true;
+ gTabs.passwordsTab.disabled = true;
+ gTabs.storageTab.disabled = true;
+ gTabs.formdataTab.hidden = true;
+ gTabs.formdataTab.disabled = true;
+ gTabs.forgetTab.hidden = true;
+ gTabs.forgetTab.disabled = true;
+ gTabs.shutdown();
+ this.selectedDomain = {title: null};
+ gDataman.debugMsg("Domain select aborted (no selection)");
+ return;
+ }
+
+ if (this.tree.view.selection.count > 1) {
+ gDataman.debugError("Data Manager doesn't support anything but one selected domain");
+ this.tree.view.selection.clearSelection();
+ this.selectedDomain = {title: null};
+ return;
+ }
+ this.selectedDomain = this.displayedDomains[this.tree.currentIndex];
+ // Disable/enable and hide/show the tabs as needed.
+ gTabs.cookiesTab.disabled = !this.selectedDomain.hasCookies;
+ gTabs.permissionsTab.disabled = !this.selectedDomain.hasPermissions;
+ gTabs.preferencesTab.disabled = !this.selectedDomain.hasPreferences;
+ gTabs.passwordsTab.disabled = !this.selectedDomain.hasPasswords;
+ gTabs.storageTab.disabled = !this.selectedDomain.hasStorage;
+ gTabs.formdataTab.hidden = !this.selectedDomain.hasFormData;
+ gTabs.formdataTab.disabled = !this.selectedDomain.hasFormData;
+ gTabs.forgetTab.disabled = true;
+ gTabs.forgetTab.hidden = true;
+ // Switch to the first non-disabled tab if the one that's showing is
+ // disabled, otherwise, you can't use the keyboard to switch tabs.
+ if (gTabs.tabbox.selectedTab.disabled) {
+ for (let i = 0; i < gTabs.tabbox.tabs.childNodes.length; ++i) {
+ if (!gTabs.tabbox.tabs.childNodes[i].disabled) {
+ gTabs.tabbox.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ if (!aNoTabSelect)
+ gTabs.select();
+
+ // Ensure the focus stays on our tree.
+ this.tree.focus();
+
+ gDataman.debugMsg("Domain select finished: " + Date.now()/1000);
+ },
+
+ handleKeyPress: function domain_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.forget();
+ }
+ else if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE &&
+ gTabs.activePanel == "forgetPanel") {
+ gForget.handleKeyPress(aEvent);
+ }
+ },
+
+ sort: function domain_sort() {
+ if (!this.displayedDomains.length)
+ return;
+
+ // compare function for two domain items
+ let compfunc = function domain_sort_compare(aOne, aTwo) {
+ // Make sure "*" is always first.
+ if (aOne.displayTitle == "*")
+ return -1;
+ if (aTwo.displayTitle == "*")
+ return 1;
+ return aOne.displayTitle.localeCompare(aTwo.displayTitle);
+ };
+
+ // Do the actual sorting of the array.
+ this.displayedDomains.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+ },
+
+ forget: function domain_forget() {
+ gTabs.forgetTab.hidden = false;
+ gTabs.forgetTab.disabled = false;
+ gTabs.tabbox.selectedTab = gTabs.forgetTab;
+ },
+
+ selectType: function domain_selectType(aType) {
+ this.search(this.searchfield.value, aType);
+ },
+
+ search: function domain_search(aSearchString, aType) {
+ this.ignoreSelect = true;
+ this.tree.treeBoxObject.beginUpdateBatch();
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ this.tree.view.selection.clearSelection();
+ this.displayedDomains = [];
+ var lcSearch = aSearchString.toLocaleLowerCase();
+ var sType = aType || this.selectfield.value;
+ for (let domain in this.domainObjects) {
+ if (this.domainObjects[domain].displayTitle
+ .toLocaleLowerCase().includes(lcSearch) &&
+ (sType == "all" || this.domainObjects[domain]["has" + sType]))
+ this.displayedDomains.push(this.domainObjects[domain]);
+ }
+ this.sort();
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ this.tree.treeBoxObject.endUpdateBatch();
+ this.ignoreSelect = false;
+ // Make sure we clear the data pane when selection has been removed.
+ if (!this.tree.view.selection.count && selectionCache.length)
+ this.select();
+ },
+
+ focusSearch: function domain_focusSearch() {
+ this.searchfield.focus();
+ },
+
+ updateContext: function domain_updateContext() {
+ let forgetCtx = document.getElementById("domain-context-forget");
+ forgetCtx.disabled = !this.selectedDomain.title;
+ forgetCtx.label = this.selectedDomain.title == "*" ?
+ forgetCtx.getAttribute("label_global") :
+ forgetCtx.getAttribute("label_domain");
+ forgetCtx.accesskey = this.selectedDomain.title == "*" ?
+ forgetCtx.getAttribute("accesskey_global") :
+ forgetCtx.getAttribute("accesskey_domain");
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.displayedDomains.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ switch (aColumn.id) {
+ case "domainCol":
+ return this.displayedDomains[aRow].displayTitle;
+ }
+ },
+};
+
+// :::::::::::::::::::: tab management ::::::::::::::::::::
+var gTabs = {
+ tabbox: null,
+ tabs: null,
+ cookiesTab: null,
+ permissionsTab: null,
+ preferencesTab: null,
+ passwordsTab: null,
+ storageTab: null,
+ formdataTab: null,
+ forgetTab: null,
+
+ panels: {},
+ activePanel: null,
+
+ initialize: function tabs_initialize() {
+ gDataman.debugMsg("Initializing tabs");
+ this.tabbox = document.getElementById("tabbox");
+ this.cookiesTab = document.getElementById("cookiesTab");
+ this.permissionsTab = document.getElementById("permissionsTab");
+ this.preferencesTab = document.getElementById("preferencesTab");
+ this.passwordsTab = document.getElementById("passwordsTab");
+ this.storageTab = document.getElementById("storageTab");
+ this.formdataTab = document.getElementById("formdataTab");
+ this.forgetTab = document.getElementById("forgetTab");
+
+ this.panels = {
+ cookiesPanel: gCookies,
+ permissionsPanel: gPerms,
+ preferencesPanel: gPrefs,
+ passwordsPanel: gPasswords,
+ storagePanel: gStorage,
+ formdataPanel: gFormdata,
+ forgetPanel: gForget
+ };
+ },
+
+ shutdown: function tabs_shutdown() {
+ gDataman.debugMsg("Shutting down tabs");
+ if (this.activePanel) {
+ this.panels[this.activePanel].shutdown();
+ this.activePanel = null;
+ }
+ },
+
+ select: function tabs_select() {
+ gDataman.debugMsg("Selecting tab");
+ if (this.activePanel) {
+ this.panels[this.activePanel].shutdown();
+ this.activePanel = null;
+ }
+
+ if (!this.tabbox || this.tabbox.selectedPanel.disabled)
+ return;
+
+ this.activePanel = this.tabbox.selectedPanel.id;
+ this.panels[this.activePanel].initialize();
+ },
+
+ selectAll: function tabs_selectAll() {
+ try {
+ this.panels[this.activePanel].selectAll();
+ }
+ catch (e) {
+ gDataman.debugError("SelectAll didn't work for " + this.activePanel + ": " + e);
+ }
+ },
+
+ focusSearch: function tabs_focusSearch() {
+ try {
+ this.panels[this.activePanel].focusSearch();
+ }
+ catch (e) {
+ gDataman.debugError("focusSearch didn't work for " + this.activePanel + ": " + e);
+ }
+ },
+};
+
+// :::::::::::::::::::: cookies panel ::::::::::::::::::::
+var gCookies = {
+ tree: null,
+ cookieInfoName: null,
+ cookieInfoValue: null,
+ cookieInfoHostLabel: null,
+ cookieInfoHost: null,
+ cookieInfoPath: null,
+ cookieInfoSendType: null,
+ cookieInfoExpires: null,
+ removeButton: null,
+ blockOnRemove: null,
+
+ cookies: [],
+ displayedCookies: [],
+
+ initialize: function cookies_initialize() {
+ gDataman.debugMsg("Initializing cookies panel");
+ this.tree = document.getElementById("cookiesTree");
+ this.tree.view = this;
+
+ this.cookieInfoName = document.getElementById("cookieInfoName");
+ this.cookieInfoValue = document.getElementById("cookieInfoValue");
+ this.cookieInfoHostLabel = document.getElementById("cookieInfoHostLabel");
+ this.cookieInfoHost = document.getElementById("cookieInfoHost");
+ this.cookieInfoPath = document.getElementById("cookieInfoPath");
+ this.cookieInfoSendType = document.getElementById("cookieInfoSendType");
+ this.cookieInfoExpires = document.getElementById("cookieInfoExpires");
+
+ this.removeButton = document.getElementById("cookieRemove");
+ this.blockOnRemove = document.getElementById("cookieBlockOnRemove");
+
+ // this.loadList() is being called in gDomains.initialize() already
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.displayedCookies = this.cookies.filter(
+ function (aCookie) {
+ return gDomains.hostMatchesSelected(aCookie.rawHost);
+ });
+ this.sort(null, false, false);
+ this.tree.treeBoxObject.endUpdateBatch();
+ },
+
+ shutdown: function cookies_shutdown() {
+ gDataman.debugMsg("Shutting down cookies panel");
+ this.tree.view.selection.clearSelection();
+ this.tree.view = null;
+ this.displayedCookies = [];
+ },
+
+ loadList: function cookies_loadList() {
+ this.cookies = [];
+ let enumerator = Services.cookies.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let nextCookie = enumerator.getNext();
+ if (!nextCookie) break;
+ nextCookie = nextCookie.QueryInterface(Ci.nsICookie2);
+ this.cookies.push(this._makeCookieObject(nextCookie));
+ }
+ },
+
+ _makeCookieObject: function cookies__makeCookieObject(aCookie) {
+ return {host: aCookie.host,
+ name: aCookie.name,
+ path: aCookie.path,
+ originAttributes: aCookie.originAttributes,
+ value: aCookie.value,
+ isDomain: aCookie.isDomain,
+ rawHost: aCookie.rawHost,
+ displayHost: gLocSvc.idn.convertToDisplayIDN(aCookie.rawHost, {}),
+ isSecure: aCookie.isSecure,
+ isSession: aCookie.isSession,
+ isHttpOnly: aCookie.isHttpOnly,
+ expires: this._getExpiresString(aCookie.expires),
+ expiresSortValue: aCookie.expires};
+ },
+
+ _getObjID: function cookies__getObjID(aIdx) {
+ var curCookie = gCookies.displayedCookies[aIdx];
+ return curCookie.host + "|" + curCookie.path + "|" + curCookie.name;
+ },
+
+ _getExpiresString: function cookies__getExpiresString(aExpires) {
+ if (aExpires) {
+ let date = new Date(1000 * aExpires);
+
+ // If a server manages to set a really long-lived cookie, the dateservice
+ // can't cope with it properly, so we'll just return a blank string.
+ // See bug 238045 for details.
+ let expiry = "";
+ try {
+ const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "full", timeStyle: "long" });
+ expiry = dateTimeFormatter.format(date);
+ }
+ catch (e) {}
+ return expiry;
+ }
+ return gDataman.bundle.getString("cookies.expireAtEndOfSession");
+ },
+
+ select: function cookies_select() {
+ var selections = gDataman.getTreeSelections(this.tree);
+ this.removeButton.disabled = !selections.length;
+ if (!selections.length) {
+ this._clearCookieInfo();
+ return true;
+ }
+
+ if (selections.length > 1) {
+ this._clearCookieInfo();
+ return true;
+ }
+
+ // At this point, we have a single cookie selected.
+ var showCookie = this.displayedCookies[selections[0]];
+
+ this.cookieInfoName.value = showCookie.name;
+ this.cookieInfoValue.value = showCookie.value;
+ this.cookieInfoHostLabel.value = showCookie.isDomain ?
+ this.cookieInfoHostLabel.getAttribute("value_domain") :
+ this.cookieInfoHostLabel.getAttribute("value_host");
+ this.cookieInfoHost.value = showCookie.host;
+ this.cookieInfoPath.value = showCookie.path;
+ var typestringID = "cookies." +
+ (showCookie.isSecure ? "secureOnly" : "anyConnection") +
+ (showCookie.isHttpOnly ? ".httponly" : ".all");
+ this.cookieInfoSendType.value = gDataman.bundle.getString(typestringID);
+ this.cookieInfoExpires.value = showCookie.expires;
+ return true;
+ },
+
+ selectAll: function cookies_selectAll() {
+ this.tree.view.selection.selectAll();
+ },
+
+ _clearCookieInfo: function cookies__clearCookieInfo() {
+ var fields = ["cookieInfoName", "cookieInfoValue", "cookieInfoHost",
+ "cookieInfoPath", "cookieInfoSendType", "cookieInfoExpires"];
+ for (let field of fields) {
+ this[field].value = "";
+ }
+ this.cookieInfoHostLabel.value = this.cookieInfoHostLabel.getAttribute("value_host");
+ },
+
+ handleKeyPress: function cookies_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.delete();
+ }
+ },
+
+ sort: function cookies_sort(aColumn, aUpdateSelection, aInvertDirection) {
+ // Make sure we have a valid column.
+ let column = aColumn;
+ if (!column) {
+ let sortedCol = this.tree.columns.getSortedColumn();
+ if (sortedCol)
+ column = sortedCol.element;
+ else
+ column = document.getElementById("cookieHostCol");
+ }
+ else if (column.localName == "treecols" || column.localName == "splitter")
+ return;
+
+ if (!column || column.localName != "treecol") {
+ Cu.reportError("No column found to sort cookies by");
+ return;
+ }
+
+ let dirAscending = column.getAttribute("sortDirection") !=
+ (aInvertDirection ? "ascending" : "descending");
+ let dirFactor = dirAscending ? 1 : -1;
+
+ // Clear attributes on all columns, we're setting them again after sorting.
+ for (let node = column.parentNode.firstChild; node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // compare function for two formdata items
+ let compfunc = function formdata_sort_compare(aOne, aTwo) {
+ switch (column.id) {
+ case "cookieHostCol":
+ return dirFactor * aOne.displayHost.localeCompare(aTwo.displayHost);
+ case "cookieNameCol":
+ return dirFactor * aOne.name.localeCompare(aTwo.name);
+ case "cookieExpiresCol":
+ return dirFactor * (aOne.expiresSortValue - aTwo.expiresSortValue);
+ }
+ return 0;
+ };
+
+ if (aUpdateSelection) {
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ }
+ this.tree.view.selection.clearSelection();
+
+ // Do the actual sorting of the array.
+ this.displayedCookies.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+
+ if (aUpdateSelection) {
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ }
+
+ // Set attributes to the sorting we did.
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", dirAscending ? "ascending" : "descending");
+ },
+
+ delete: function cookies_delete() {
+ var selections = gDataman.getTreeSelections(this.tree);
+
+ if (selections.length > 1) {
+ let title = gDataman.bundle.getString("cookies.deleteSelectedTitle");
+ let msg = gDataman.bundle.getString("cookies.deleteSelected");
+ let flags = ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ let yes = gDataman.bundle.getString("cookies.deleteSelectedYes");
+ if (Services.prompt.confirmEx(window, title, msg, flags, yes, null, null,
+ null, {value: 0}) == 1) // 1=="Cancel" button
+ return;
+ }
+
+ this.tree.view.selection.clearSelection();
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = selections.length - 1; i >= 0; i--) {
+ let delCookie = this.displayedCookies[selections[i]];
+ this.cookies.splice(this.cookies.indexOf(this.displayedCookies[selections[i]]), 1);
+ this.displayedCookies.splice(selections[i], 1);
+ this.tree.treeBoxObject.rowCountChanged(selections[i], -1);
+ Services.cookies.remove(delCookie.host, delCookie.name, delCookie.path,
+ this.blockOnRemove.checked, delCookie.originAttributes);
+ }
+ if (!this.displayedCookies.length)
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasCookies");
+ // Select the entry after the first deleted one or the last of all entries.
+ if (selections.length && this.displayedCookies.length)
+ this.tree.view.selection.toggleSelect(selections[0] < this.displayedCookies.length ?
+ selections[0] :
+ this.displayedCookies.length - 1);
+ },
+
+ updateContext: function cookies_updateContext() {
+ document.getElementById("cookies-context-remove").disabled =
+ this.removeButton.disabled;
+ document.getElementById("cookies-context-selectall").disabled =
+ this.tree.view.selection.count >= this.tree.view.rowCount;
+ },
+
+ reactToChange: function cookies_reactToChange(aSubject, aData) {
+ // aData: added, changed, deleted, batch-deleted, cleared, reload
+ // see http://mxr.mozilla.org/mozilla-central/source/netwerk/cookie/nsICookieService.idl
+ if (aData == "batch-deleted" || aData == "cleared" || aData == "reload") {
+ // Go for re-parsing the whole thing, as cleared and reload need that anyhow
+ // (batch-deleted has an nsIArray of cookies, we could in theory do better there).
+ var selectionCache = [];
+ if (this.displayedCookies.length) {
+ selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ this.displayedCookies = [];
+ }
+ this.loadList();
+ var domainList = [];
+ for (let cookie of this.cookies) {
+ let domain = gDomains.getDomainFromHost(cookie.rawHost);
+ if (!domainList.includes(domain))
+ domainList.push(domain);
+ }
+ gDomains.resetFlagToDomains("hasCookies", domainList);
+ // Restore the local panel display if needed.
+ if (gTabs.activePanel == "cookiesPanel" &&
+ gDomains.selectedDomain.hasCookies) {
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.displayedCookies = this.cookies.filter(
+ function (aCookie) {
+ return gDomains.hostMatchesSelected(aCookie.rawHost);
+ });
+ this.sort(null, false, false);
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ this.tree.treeBoxObject.endUpdateBatch();
+ }
+ return;
+ }
+
+ // Usual notifications for added, changed, deleted - do "surgical" updates.
+ aSubject.QueryInterface(Ci.nsICookie2);
+ let domain = gDomains.getDomainFromHost(aSubject.rawHost);
+ // Does change affect possibly loaded Cookies pane?
+ let affectsLoaded = this.displayedCookies.length &&
+ gDomains.hostMatchesSelected(aSubject.rawHost);
+ if (aData == "added") {
+ this.cookies.push(this._makeCookieObject(aSubject));
+ if (affectsLoaded) {
+ this.displayedCookies.push(this.cookies[this.cookies.length - 1]);
+ this.tree.treeBoxObject.rowCountChanged(this.cookies.length - 1, 1);
+ this.sort(null, true, false);
+ }
+ else {
+ gDomains.addDomainOrFlag(aSubject.rawHost, "hasCookies");
+ }
+ }
+ else {
+ let idx = -1, disp_idx = -1, domainCookies = 0;
+ if (affectsLoaded) {
+ for (let i = 0; i < this.displayedCookies.length; i++) {
+ let cookie = this.displayedCookies[i];
+ if (cookie.host == aSubject.host && cookie.name == aSubject.name &&
+ cookie.path == aSubject.path) {
+ idx = this.cookies.indexOf(this.displayedCookies[i]);
+ disp_idx = i;
+ break;
+ }
+ }
+ if (aData == "deleted")
+ domainCookies = this.displayedCookies.length;
+ }
+ else {
+ for (let i = 0; i < this.cookies.length; i++) {
+ let cookie = this.cookies[i];
+ if (cookie.host == aSubject.host && cookie.name == aSubject.name &&
+ cookie.path == aSubject.path) {
+ idx = i;
+ if (aData != "deleted")
+ break;
+ }
+ if (aData == "deleted" &&
+ gDomains.getDomainFromHost(cookie.rawHost) == domain)
+ domainCookies++;
+ }
+ }
+ if (idx >= 0) {
+ if (aData == "changed") {
+ this.cookies[idx] = this._makeCookieObject(aSubject);
+ if (affectsLoaded)
+ this.tree.treeBoxObject.invalidateRow(disp_idx);
+ }
+ else if (aData == "deleted") {
+ this.cookies.splice(idx, 1);
+ if (affectsLoaded) {
+ this.displayedCookies.splice(disp_idx, 1);
+ this.tree.treeBoxObject.rowCountChanged(disp_idx, -1);
+ }
+ if (domainCookies == 1)
+ gDomains.removeDomainOrFlag(domain, "hasCookies");
+ }
+ }
+ }
+ },
+
+ forget: function cookies_forget() {
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = this.cookies.length - 1; i >= 0; i--) {
+ if (gDomains.hostMatchesSelected(this.cookies[i].rawHost)) {
+ // Remove from internal list needs to be before actually deleting.
+ let delCookie = this.cookies[i];
+ this.cookies.splice(i, 1);
+ Services.cookies.remove(delCookie.host, delCookie.name,
+ delCookie.path, false);
+ }
+ }
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasCookies");
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.displayedCookies.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ let cookie = this.displayedCookies[aRow];
+ switch (aColumn.id) {
+ case "cookieHostCol":
+ return cookie.displayHost;
+ case "cookieNameCol":
+ return cookie.name;
+ case "cookieExpiresCol":
+ return cookie.expires;
+ }
+ },
+};
+
+// :::::::::::::::::::: permissions panel ::::::::::::::::::::
+var gPerms = {
+ list: null,
+ listPermission: [],
+
+ initialize: function permissions_initialize() {
+ gDataman.debugMsg("Initializing permissions panel");
+ this.list = document.getElementById("permList");
+ this.addSelBox = document.getElementById("permSelectionBox");
+ this.addHost = document.getElementById("permHost");
+ this.addType = document.getElementById("permType");
+ this.addButton = document.getElementById("permAddButton");
+
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let nextPermission = enumerator.getNext();
+ nextPermission = nextPermission.QueryInterface(Ci.nsIPermission);
+
+ if (gDomains.hostMatchesSelectedURI(nextPermission.principal.URI)) {
+ let permElem = document.createElement("richlistitem");
+ permElem.setAttribute("type", nextPermission.type);
+ permElem.setAttribute("host", nextPermission.principal.origin);
+ permElem.setAttribute("displayHost", nextPermission.principal.origin);
+ permElem.setAttribute("capability", nextPermission.capability);
+ permElem.setAttribute("class", "permission");
+ gDataman.debugMsg("Adding Origin: " + nextPermission.principal.origin);
+ this.list.appendChild(permElem);
+ this.listPermission.push({id: nextPermission.length,
+ origin: nextPermission.principal.origin,
+ principal: nextPermission.principal,
+ type: nextPermission.type});
+ }
+ }
+ this.list.disabled = !this.list.itemCount;
+ this.addButton.disabled = false;
+ },
+
+ shutdown: function permissions_shutdown() {
+ gDataman.debugMsg("Shutting down permissions panel");
+ // XXX: Here we could detect if we still hold any non-default settings and
+ // trigger the removeDomainOrFlag if not.
+ while (this.list.hasChildNodes())
+ this.list.lastChild.remove();
+
+ this.addSelBox.hidden = true;
+ this.listPermission.length = 0;
+ },
+
+ // Find an item in the permissionsList by origin and type.
+ getPrincipalListItem: function permissions_getPrincipalListItem(aOrigin, aType) {
+
+ gDataman.debugMsg("Getting list item: " + aOrigin + " " + aType);
+
+ for (let elem of this.listPermission) {
+
+ gDataman.debugMsg("elem: " + elem.type);
+
+ // check if this is the one
+ if (elem.type == aType &&
+ elem.origin == aOrigin) {
+ gDataman.debugMsg("Found Element " + elem.origin);
+ return elem;
+ }
+ }
+ return null;
+ },
+
+ // Directly remove a permission.
+ // This function is called when the user checks the 'Use Default' button on the permissions panel.
+ // The item will be removed and the default permissions for the origin will be in place afterwards.
+ // This function will only handle the deletion. The remove will trigger an Observer message.
+ // Because the permission might be removed outside of this panel the code in there needs to clean
+ // up the panel and lists.
+ removeItem: function permissions_removeItem(aOrigin, aType) {
+
+ gDataman.debugMsg("Removing an Item: " + aOrigin + " " + aType);
+
+ let permElem = this.getPrincipalListItem(aOrigin, aType);
+
+ // This happens when we add a new permission.
+ if (permElem == null) {
+ gDataman.debugMsg("Unable to find an Item: " + aOrigin + " " + aType);
+ return;
+ }
+
+ gDataman.debugMsg("Found Element " + permElem.origin);
+
+ // It might be a new element. In this case the principal is null and we do not need to do
+ // anything here. We can not remove the list entry because it might be a new permission the
+ // user wants to change.
+ if (permElem.principal != null) {
+ // Delete the permission. We will deactivate the list item in the subsequent observer message.
+ try {
+ gDataman.debugMsg("Removing permission");
+ Services.perms.removeFromPrincipal(permElem.principal, permElem.type);
+ }
+ catch (e) {
+ gDataman.debugError("Permission could not be removed " +
+ permElem.principal.origin + " " +
+ permElem.principal.type
+ );
+ }
+ }
+ },
+
+ // Directly change a permission.
+ // This function is called when the user changes the value of a permission on the permissions panel.
+ // This function will only handle the update. The update will trigger an Observer message.
+ // Because the permission might be changed outside of this panel the code in there needs to handle
+ // further generic changes.
+ updateItem: function permissions_updateItem(aOrigin, aType, aValue) {
+
+ gDataman.debugMsg("Updating an Item: " + aOrigin + " " + aType + " " + aValue);
+
+ let permElem = this.getPrincipalListItem(aOrigin, aType);
+
+ if (permElem == null) {
+ gDataman.debugMsg("Unable to find an Item: " + aOrigin + " " + aType);
+ return;
+ }
+
+ // If this is a completely new permission we do not have a principal yet.
+ // This happens when we add a new item. We need to create a new permission
+ // from scratch.
+ // Maybe use
+ // principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {})
+ // but this might be undocumented?
+ if (permElem.principal == null)
+ {
+ // This can currently fail for some schemes like 'file://'.
+ // Maybe fix it later if needed.
+ try {
+ let uri = Services.io.newURI(new URL(aOrigin));
+ Services.perms.add(uri, aType, aValue);
+ }
+ catch (e) {
+ gDataman.debugError("New Permission could not be added " +
+ permElem.origin + " " +
+ permElem.type
+ );
+ }
+
+ } else {
+ Services.perms.addFromPrincipal(permElem.principal, permElem.type, aValue);
+ }
+ },
+
+ // Most functions of permissions are in the XBL items!
+ addButtonClick: function permissions_addButtonClick() {
+
+ gDataman.debugMsg("Add permissions button clicked!");
+
+ if (this.addSelBox.hidden) {
+ // Show addition box, disable button.
+ this.addButton.disabled = true;
+ this.addType.removeAllItems(); // Make sure list is clean.
+ let permTypes = ["allowXULXBL", "cookie", "geo", "image", "indexedDB",
+ "install", "login-saving", "object", "offline-app",
+ "popup", "script", "stylesheet",
+ "trackingprotection"];
+
+ // Look for a translation.
+ for (let permType of permTypes) {
+ let typeDesc = permType;
+ try {
+ typeDesc = gDataman.bundle.getString("perm." + permType + ".label");
+ }
+ catch (e) {
+ }
+ let menuitem = this.addType.appendItem(typeDesc, permType);
+ }
+ this.addType.setAttribute("label",
+ gDataman.bundle.getString("perm.type.default"));
+ this.addHost.value =
+ gDomains.selectedDomain.title == "*" ? "" : ("http://www." + gDomains.selectedDomain.title);
+ this.addSelBox.hidden = false;
+ }
+ else {
+ // Let the backend do the validation of the input field.
+ let nOrigin = "";
+
+ try {
+ nOrigin = new URL(this.addHost.value).origin;
+ } catch (e) {
+ // Show an error if URL is invalid.
+ window.alert(gDataman.bundle.getString("perm.validation.invalidurl"));
+ return;
+ }
+
+ // Url could be validated but User did probably enter half valid nonsense
+ // because the origin is undefined.
+ if ((nOrigin == null) || (nOrigin == "")) {
+ window.alert(gDataman.bundle.getString("perm.validation.invalidurl"));
+ return;
+ }
+
+ gDataman.debugMsg("New origin: " + nOrigin);
+
+ // Add entry to list, hide addition box.
+ let permElem = document.createElement("richlistitem");
+ permElem.setAttribute("type", this.addType.value);
+ permElem.setAttribute("host", nOrigin);
+ permElem.setAttribute("displayHost", nOrigin);
+ permElem.setAttribute("capability", this.getDefault(this.addType.value));
+ permElem.setAttribute("class", "permission");
+ this.list.appendChild(permElem);
+ this.list.disabled = false;
+ permElem.useDefault(true);
+ // Add a new entry to the permissions list.
+ // We do not have a principal yet so we use only the origin as identification.
+ this.listPermission.push({id: this.listPermission.length + 1,
+ origin: nOrigin,
+ principal: null,
+ type: this.addType.value});
+
+ this.addSelBox.hidden = true;
+ this.addType.removeAllItems();
+ }
+ },
+
+ addCheck: function permissions_addCheck() {
+ // Only enable button if both fields have (reasonable) values.
+ this.addButton.disabled = !(this.addType.value &&
+ gDomains.getDomainFromHost(this.addHost.value));
+ },
+
+ getDefault: function permissions_getDefault(aType) {
+ switch (aType) {
+ case "allowXULXBL":
+ return Services.perms.DENY_ACTION;
+ case "cookie":
+ if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == 2)
+ return Services.perms.DENY_ACTION;
+ if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == 2)
+ return Ci.nsICookiePermission.ACCESS_SESSION;
+ return Services.perms.ALLOW_ACTION;
+ case "geo":
+ return Services.perms.DENY_ACTION;
+ case "indexedDB":
+ return Services.perms.DENY_ACTION;
+ case "install":
+ if (Services.prefs.getBoolPref("xpinstall.whitelist.required"))
+ return Services.perms.DENY_ACTION;
+ return Services.perms.ALLOW_ACTION;
+ case "offline-app":
+ try {
+ if (Services.prefs.getBoolPref("offline-apps.allow_by_default"))
+ return Services.perms.ALLOW_ACTION;
+ } catch(e) {
+ // this pref isn't set by default, ignore failures
+ }
+ if (Services.prefs.getBoolPref("browser.offline-apps.notify"))
+ return Services.perms.DENY_ACTION;
+ return Services.perms.UNKNOWN_ACTION;
+ case "popup":
+ if (Services.prefs.getBoolPref("dom.disable_open_during_load"))
+ return Services.perms.DENY_ACTION;
+ return Services.perms.ALLOW_ACTION;
+ case "trackingprotection":
+ return Services.perms.DENY_ACTION;
+ }
+
+ // We are not done yet.
+ // This should only be called for new permission types which have not been
+ // added to the Data Manager yet.
+ try {
+ // Look for an nsContentBlocker permission.
+ switch (Services.prefs.getIntPref("permissions.default." + aType)) {
+ case 3:
+ return NOFOREIGN;
+ case 2:
+ return Services.perms.DENY_ACTION;
+ default:
+ return Services.perms.ALLOW_ACTION;
+ }
+ } catch (e) {
+ return Services.perms.UNKNOWN_ACTION;
+ }
+ },
+
+ reactToChange: function permissions_reactToChange(aSubject, aData) {
+
+ // aData: added, changed, deleted, cleared
+ // aSubject: the subject which is the permission to be changed
+ // See http://mxr.mozilla.org/mozilla-central/source/netwerk/base/public/nsIPermissionManager.idl
+ if (aData == "cleared") {
+ gDataman.debugMsg("something has been cleared but why in permission?");
+ gDomains.resetFlagToDomains("hasPermissions", domainList);
+ return;
+ }
+
+ gDataman.debugMsg("react to change: " + aSubject.principal.origin + " " + aData);
+
+ aSubject.QueryInterface(Ci.nsIPermission);
+
+ let rawHost;
+ let domain;
+
+ if (!gDomains.commonScheme(aSubject.principal.URI.scheme)) {
+ rawHost = "*";
+ domain = "*";
+ }
+ else {
+ rawHost = aSubject.principal.URI.host.replace(/^\./, "");
+ domain = gDomains.getDomainFromHost(rawHost);
+ }
+
+ // Does change affect possibly loaded Preferences pane?
+ let affectsLoaded = this.list && this.list.childElementCount &&
+ gDomains.hostMatchesSelectedURI(aSubject.principal.URI);
+
+ let permElem = null;
+
+ if (affectsLoaded) {
+ for (let lChild of this.list.children) {
+ gDataman.debugMsg("checking type: " + lChild.getAttribute("class") + " " +
+ lChild.getAttribute("type") + " " + aSubject.type);
+
+ // Check type and host (origin) first.
+ if (lChild.getAttribute("type") == aSubject.type &&
+ lChild.getAttribute("host") == aSubject.principal.origin)
+ permElem = lChild;
+ }
+ }
+
+ if (aData == "deleted") {
+ if (affectsLoaded) {
+ permElem.useDefault(true, true);
+ }
+ else {
+ // Only remove if domain is not shown, note that this may leave an empty domain.
+ let haveDomainPerms = false;
+ let enumerator = Services.perms.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let nextPermission = enumerator.getNext();
+ nextPermission = nextPermission.QueryInterface(Ci.nsIPermission);
+
+ let dDomain;
+
+ if (!gDomains.commonScheme(nextPermission.principal.URI.scheme)) {
+ dDomain = "*";
+ }
+ else {
+ dDomain = gDomains.getDomainFromHost(nextPermission.principal.URI.host.replace(/^\./, ""));
+ }
+
+ if (domain == dDomain) {
+ haveDomainPerms = true;
+ break;
+ }
+ }
+ if (!haveDomainPerms)
+ gDomains.removeDomainOrFlag(domain, "hasPermissions");
+ }
+ }
+ else if (aData == "changed" && affectsLoaded) {
+ permElem.setCapability(aSubject.capability, true);
+ }
+ else if (aData == "added") {
+ if (affectsLoaded) {
+ if (permElem) {
+ // Check if them permission list contains the principal.
+ // If not adding it to the permissions list.
+ // This might be the case for newly created items.
+ let permElem2 = this.getPrincipalListItem(aSubject.principal.origin, aSubject.type);
+
+ if (permElem2 != null &&
+ permElem2.principal == null) {
+ permElem2.principal = aSubject.principal;
+ }
+
+ permElem.useDefault(false, true);
+ permElem.setCapability(aSubject.capability, true);
+ }
+ else {
+ gDataman.debugMsg("Adding completely new item: " + aSubject.principal.origin + " " + aSubject.type);
+ permElem = document.createElement("richlistitem");
+ permElem.setAttribute("type", aSubject.type);
+ permElem.setAttribute("host", aSubject.principal.origin);
+ permElem.setAttribute("displayHost", aSubject.principal.origin);
+ permElem.setAttribute("capability", aSubject.capability);
+ permElem.setAttribute("class", "permission");
+ permElem.setAttribute("orient", "vertical");
+ this.list.appendChild(permElem);
+
+ // add an entry to the permissions list
+ this.listPermission.push({id: this.listPermission.length + 1,
+ origin: aSubject.principal.origin,
+ principal: aSubject.principal,
+ type: aSubject.type});
+ }
+ }
+ gDomains.addDomainOrFlag(rawHost, "hasPermissions");
+ }
+
+ this.list.disabled = !this.list.itemCount;
+ },
+
+ // This function is a called when you check that all permissions for the given domain should be
+ // deleted (forget).
+ forget: function permissions_forget() {
+ let delPerms = [];
+ let enumerator = Services.perms.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let nextPermission = enumerator.getNext();
+ nextPermission = nextPermission.QueryInterface(Ci.nsIPermission);
+
+ if (gDomains.hostMatchesSelectedURI(nextPermission.principal.URI)) {
+ delPerms.push({principal: nextPermission.principal, type: nextPermission.type});
+ }
+ }
+
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = delPerms.length - 1; i >= 0; i--) {
+ Services.perms.removeFromPrincipal(delPerms[i].principal, delPerms[i].type);
+ }
+
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasPermissions");
+ },
+};
+
+// :::::::::::::::::::: content prefs panel ::::::::::::::::::::
+var gPrefs = {
+ tree: null,
+ removeButton: null,
+
+ prefs: [],
+
+ initialize: function prefs_initialize() {
+ gDataman.debugMsg("Initializing prefs panel");
+ this.tree = document.getElementById("prefsTree");
+ this.tree.view = this;
+
+ this.removeButton = document.getElementById("prefsRemove");
+
+ // Get all groups (hosts) that match the domain.
+ let domain = gDomains.selectedDomain.title;
+
+ if (domain == "*") {
+ domain = null;
+ }
+
+ let prefs = [];
+ Services.contentPrefs2.getBySubdomain(domain, null, {
+ handleResult(resultPref) {
+ prefs.push(resultPref);
+ },
+ handleCompletion: () => {
+ gPrefs.tree.treeBoxObject.beginUpdateBatch();
+ gPrefs.prefs = [];
+ for (let pref of prefs) {
+ if (!domain) {
+ gPrefs.prefs.push({host: null, displayHost: "", name: pref.name,
+ value: pref.value});
+ }
+ else {
+ let display = gLocSvc.idn.convertToDisplayIDN(pref.domain, {});
+ gPrefs.prefs.push({host: pref.domain, displayHost: display,
+ name: pref.name, value: pref.value});
+ }
+ }
+
+ gPrefs.sort(null, false, false);
+ gPrefs.tree.treeBoxObject.endUpdateBatch();
+ },
+ });
+ },
+
+ shutdown: function prefs_shutdown() {
+ gDataman.debugMsg("Shutting down prefs panel");
+ this.tree.view.selection.clearSelection();
+ this.tree.view = null;
+ this.prefs = [];
+ },
+
+ _getObjID: function prefs__getObjID(aIdx) {
+ var curPref = gPrefs.prefs[aIdx];
+ return curPref.host + "|" + curPref.name;
+ },
+
+ select: function prefs_select() {
+ var selections = gDataman.getTreeSelections(this.tree);
+ this.removeButton.disabled = !selections.length;
+ return true;
+ },
+
+ selectAll: function prefs_selectAll() {
+ this.tree.view.selection.selectAll();
+ },
+
+ handleKeyPress: function prefs_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.delete();
+ }
+ },
+
+ sort: function prefs_sort(aColumn, aUpdateSelection, aInvertDirection) {
+ // Make sure we have a valid column.
+ let column = aColumn;
+ if (!column) {
+ let sortedCol = this.tree.columns.getSortedColumn();
+ if (sortedCol)
+ column = sortedCol.element;
+ else
+ column = document.getElementById("prefsHostCol");
+ }
+ else if (column.localName == "treecols" || column.localName == "splitter")
+ return;
+
+ if (!column || column.localName != "treecol") {
+ Cu.reportError("No column found to sort form data by");
+ return;
+ }
+
+ let dirAscending = column.getAttribute("sortDirection") !=
+ (aInvertDirection ? "ascending" : "descending");
+ let dirFactor = dirAscending ? 1 : -1;
+
+ // Clear attributes on all columns, we're setting them again after sorting.
+ for (let node = column.parentNode.firstChild; node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // compare function for two content prefs
+ let compfunc = function prefs_sort_compare(aOne, aTwo) {
+ switch (column.id) {
+ case "prefsHostCol":
+ return dirFactor * aOne.displayHost.localeCompare(aTwo.displayHost);
+ case "prefsNameCol":
+ return dirFactor * aOne.name.localeCompare(aTwo.name);
+ case "prefsValueCol":
+ return dirFactor * aOne.value.toString().localeCompare(aTwo.value);
+ }
+ return 0;
+ };
+
+ if (aUpdateSelection) {
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ }
+ this.tree.view.selection.clearSelection();
+
+ // Do the actual sorting of the array.
+ this.prefs.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+
+ if (aUpdateSelection) {
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ }
+
+ // Set attributes to the sorting we did.
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", dirAscending ? "ascending" : "descending");
+ },
+
+ delete: function prefs_delete() {
+ var selections = gDataman.getTreeSelections(this.tree);
+
+ if (selections.length > 1) {
+ let title = gDataman.bundle.getString("prefs.deleteSelectedTitle");
+ let msg = gDataman.bundle.getString("prefs.deleteSelected");
+ let flags = ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ let yes = gDataman.bundle.getString("prefs.deleteSelectedYes");
+ if (Services.prompt.confirmEx(window, title, msg, flags, yes, null, null,
+ null, {value: 0}) == 1) // 1=="Cancel" button
+ return;
+ }
+
+ this.tree.view.selection.clearSelection();
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = selections.length - 1; i >= 0; i--) {
+ let delPref = this.prefs[selections[i]];
+ this.prefs.splice(selections[i], 1);
+ this.tree.treeBoxObject.rowCountChanged(selections[i], -1);
+ Services.contentPrefs2.removeByDomainAndName(delPref.host, delPref.name, null);
+ }
+ if (!this.prefs.length)
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasPreferences");
+ // Select the entry after the first deleted one or the last of all entries.
+ if (selections.length && this.prefs.length)
+ this.tree.view.selection.toggleSelect(selections[0] < this.prefs.length ?
+ selections[0] :
+ this.prefs.length - 1);
+ },
+
+ updateContext: function prefs_updateContext() {
+ document.getElementById("prefs-context-remove").disabled =
+ this.removeButton.disabled;
+ document.getElementById("prefs-context-selectall").disabled =
+ this.tree.view.selection.count >= this.tree.view.rowCount;
+ },
+
+ reactToChange: function prefs_reactToChange(aSubject, aData) {
+ // aData: prefSet, prefRemoved
+ gDataman.debugMsg("Observed pref change for: " + aSubject.host);
+ // Do "surgical" updates.
+ let domain = gDomains.getDomainFromHostWithCheck(aSubject.host);
+ gDataman.debugMsg("domain: " + domain);
+ // Does change affect possibly loaded Preferences pane?
+ let affectsLoaded = this.prefs.length &&
+ (domain == gDomains.selectedDomain.title);
+
+ let idx = -1, domainPrefs = 0;
+ if (affectsLoaded) {
+ gDataman.debugMsg("affects loaded");
+ for (let i = 0; i < this.prefs.length; i++) {
+ let cpref = this.prefs[i];
+ if (cpref && cpref.host == aSubject.host && cpref.name == aSubject.name) {
+ idx = i;
+ break;
+ }
+ }
+ if (aData == "prefRemoved")
+ domainPrefs = this.prefs.length;
+ }
+ else if (aData == "prefRemoved") {
+ // See if there are any prefs left for that domain.
+ Services.contentPrefs2.hasPrefs(domain != "*" ? domain : null, null, {
+ handleResult(prefResult) {
+ if (!prefResult.value) {
+ gDomains.removeDomainOrFlag(domain, "hasPreferences");
+ }
+ },
+ handleCompletion: () => {
+ },
+ });
+ }
+ if (aData == "prefSet")
+ aSubject.displayHost = gLocSvc.idn.convertToDisplayIDN(aSubject.host, {});
+
+ // Affects loaded domain and is an existing pref.
+ if (idx >= 0) {
+ if (aData == "prefSet") {
+ this.prefs[idx] = aSubject;
+ if (affectsLoaded)
+ this.tree.treeBoxObject.invalidateRow(idx);
+ }
+ else if (aData == "prefRemoved") {
+ this.prefs.splice(idx, 1);
+ if (affectsLoaded) {
+ this.tree.treeBoxObject.rowCountChanged(idx, -1);
+ }
+ if (domainPrefs == 1)
+ gDomains.removeDomainOrFlag(domain, "hasPreferences");
+ }
+ }
+ else if (aData == "prefSet") {
+ // Affects loaded domain but is not an existing pref.
+ // Pref set, no prev index known - either new or existing pref domain.
+ if (affectsLoaded) {
+ this.prefs.push(aSubject);
+ this.tree.treeBoxObject.rowCountChanged(this.prefs.length - 1, 1);
+ this.sort(null, true, false);
+ }
+ // Not the loaded domain but it now has a preference.
+ else {
+ gDomains.addDomainOrFlag(domain, "hasPreferences");
+ }
+ }
+ },
+
+ forget: function prefs_forget() {
+ let callbacks = {
+ handleResult(resultDomain) {
+ },
+ handleCompletion: () => {
+ gDomains.removeDomainOrFlag(domain, "hasPreferences");
+ },
+ };
+
+ let domain = gDomains.selectedDomain.title;
+ if (domain == "*") {
+ Services.contentPrefs2.removeAllGlobals(null, callbacks);
+ }
+ else {
+ Services.contentPrefs2.removeBySubdomain(domain, null, callbacks);
+ }
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.prefs.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ let cpref = this.prefs[aRow];
+ switch (aColumn.id) {
+ case "prefsHostCol":
+ return cpref.displayHost || "*";
+ case "prefsNameCol":
+ return cpref.name;
+ case "prefsValueCol":
+ return cpref.value;
+ }
+ },
+};
+
+// :::::::::::::::::::: passwords panel ::::::::::::::::::::
+var gPasswords = {
+ tree: null,
+ removeButton: null,
+ toggleButton: null,
+ pwdCol: null,
+
+ allSignons: [],
+ displayedSignons: [],
+ showPasswords: false,
+
+ initialize: function passwords_initialize() {
+ gDataman.debugMsg("Initializing passwords panel");
+ this.tree = document.getElementById("passwordsTree");
+ this.tree.view = this;
+
+ this.removeButton = document.getElementById("pwdRemove");
+ this.toggleButton = document.getElementById("pwdToggle");
+ this.toggleButton.label = gDataman.bundle.getString("pwd.showPasswords");
+ this.toggleButton.accessKey = gDataman.bundle.getString("pwd.showPasswords.accesskey");
+
+ this.pwdCol = document.getElementById("pwdPasswordCol");
+
+ this.tree.treeBoxObject.beginUpdateBatch();
+ // this.loadList() is being called in gDomains.initialize() already
+ this.displayedSignons = this.allSignons.filter(
+ function (aSignon) {
+ return gDomains.hostMatchesSelected(aSignon.hostname);
+ });
+ this.sort(null, false, false);
+ this.tree.treeBoxObject.endUpdateBatch();
+ },
+
+ shutdown: function passwords_shutdown() {
+ gDataman.debugMsg("Shutting down passwords panel");
+ if (this.showPasswords)
+ this.togglePasswordVisible();
+ this.tree.view.selection.clearSelection();
+ this.tree.view = null;
+ this.displayedSignons = [];
+ },
+
+ loadList: function passwords_loadList() {
+ this.allSignons = Services.logins.getAllLogins();
+ },
+
+ _getObjID: function passwords__getObjID(aIdx) {
+ var curSignon = gPasswords.displayedSignons[aIdx];
+ return curSignon.hostname + "|" + curSignon.httpRealm + "|" + curSignon.username;
+ },
+
+ select: function passwords_select() {
+ var selections = gDataman.getTreeSelections(this.tree);
+ this.removeButton.disabled = !selections.length;
+ return true;
+ },
+
+ selectAll: function passwords_selectAll() {
+ this.tree.view.selection.selectAll();
+ },
+
+ handleKeyPress: function passwords_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.delete();
+ }
+ },
+
+ sort: function passwords_sort(aColumn, aUpdateSelection, aInvertDirection) {
+ // Make sure we have a valid column.
+ let column = aColumn;
+ if (!column) {
+ let sortedCol = this.tree.columns.getSortedColumn();
+ if (sortedCol)
+ column = sortedCol.element;
+ else
+ column = document.getElementById("pwdHostCol");
+ }
+ else if (column.localName == "treecols" || column.localName == "splitter")
+ return;
+
+ if (!column || column.localName != "treecol") {
+ Cu.reportError("No column found to sort form data by");
+ return;
+ }
+
+ let dirAscending = column.getAttribute("sortDirection") !=
+ (aInvertDirection ? "ascending" : "descending");
+ let dirFactor = dirAscending ? 1 : -1;
+
+ // Clear attributes on all columns, we're setting them again after sorting.
+ for (let node = column.parentNode.firstChild; node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // compare function for two signons
+ let compfunc = function passwords_sort_compare(aOne, aTwo) {
+ switch (column.id) {
+ case "pwdHostCol":
+ return dirFactor * aOne.hostname.localeCompare(aTwo.hostname);
+ case "pwdUserCol":
+ return dirFactor * aOne.username.localeCompare(aTwo.username);
+ case "pwdPasswordCol":
+ return dirFactor * aOne.password.localeCompare(aTwo.password);
+ }
+ return 0;
+ };
+
+ if (aUpdateSelection) {
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ }
+ this.tree.view.selection.clearSelection();
+
+ // Do the actual sorting of the array.
+ this.displayedSignons.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+
+ if (aUpdateSelection) {
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ }
+
+ // Set attributes to the sorting we did.
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", dirAscending ? "ascending" : "descending");
+ },
+
+ delete: function passwords_delete() {
+ var selections = gDataman.getTreeSelections(this.tree);
+
+ if (selections.length > 1) {
+ let title = gDataman.bundle.getString("pwd.deleteSelectedTitle");
+ let msg = gDataman.bundle.getString("pwd.deleteSelected");
+ let flags = ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ let yes = gDataman.bundle.getString("pwd.deleteSelectedYes");
+ if (Services.prompt.confirmEx(window, title, msg, flags, yes, null, null,
+ null, {value: 0}) == 1) // 1=="Cancel" button
+ return;
+ }
+
+ this.tree.view.selection.clearSelection();
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = selections.length - 1; i >= 0; i--) {
+ let delSignon = this.displayedSignons[selections[i]];
+ this.allSignons.splice(this.allSignons.indexOf(this.displayedSignons[selections[i]]), 1);
+ this.displayedSignons.splice(selections[i], 1);
+ this.tree.treeBoxObject.rowCountChanged(selections[i], -1);
+ Services.logins.removeLogin(delSignon);
+ }
+ if (!this.displayedSignons.length)
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasPasswords");
+ // Select the entry after the first deleted one or the last of all entries.
+ if (selections.length && this.displayedSignons.length)
+ this.tree.view.selection.toggleSelect(selections[0] < this.displayedSignons.length ?
+ selections[0] :
+ this.displayedSignons.length - 1);
+ },
+
+ togglePasswordVisible: function passwords_togglePasswordVisible() {
+ if (this.showPasswords || this._confirmShowPasswords()) {
+ this.showPasswords = !this.showPasswords;
+ this.toggleButton.label = gDataman.bundle.getString(this.showPasswords ?
+ "pwd.hidePasswords" :
+ "pwd.showPasswords");
+ this.toggleButton.accessKey = gDataman.bundle.getString(this.showPasswords ?
+ "pwd.hidePasswords.accesskey" :
+ "pwd.showPasswords.accesskey");
+ this.pwdCol.hidden = !this.showPasswords;
+ }
+ },
+
+ _confirmShowPasswords: function passwords__confirmShowPasswords() {
+ // This doesn't harm if passwords are not encrypted.
+ let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .createInstance(Ci.nsIPK11TokenDB);
+ let token = tokendb.getInternalKeyToken();
+
+ // If there is no master password, still give the user a chance to opt-out
+ // of displaying passwords
+ if (token.checkPassword(""))
+ return this._askUserShowPasswords();
+
+ // So there's a master password. But since checkPassword didn't succeed,
+ // we're logged out (per nsIPK11Token.idl).
+ try {
+ // Relogin and ask for the master password.
+ token.login(true); // 'true' means always prompt for token password. User
+ // will be prompted until clicking 'Cancel' or
+ // entering the correct password.
+ }
+ catch (e) {
+ // An exception will be thrown if the user cancels the login prompt dialog.
+ // User is also logged out of Software Security Device.
+ }
+
+ return token.isLoggedIn();
+ },
+
+ _askUserShowPasswords: function passwords__askUserShowPasswords() {
+ // Confirm the user wants to display passwords.
+ return Services.prompt.confirmEx(window, null,
+ gDataman.bundle.getString("pwd.noMasterPasswordPrompt"),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null, null, null, null, { value: false }) == 0; // 0=="Yes" button
+ },
+
+ updateContext: function passwords_updateContext() {
+ document.getElementById("pwd-context-remove").disabled =
+ this.removeButton.disabled;
+ document.getElementById("pwd-context-copypassword").disabled =
+ this.tree.view.selection.count != 1;
+ document.getElementById("pwd-context-selectall").disabled =
+ this.tree.view.selection.count >= this.tree.view.rowCount;
+ },
+
+ copySelPassword: function passwords_copySelPassword() {
+ // Copy selected signon's password to clipboard.
+ let row = this.tree.currentIndex;
+ let password = gPasswords.displayedSignons[row].password;
+ gLocSvc.clipboard.copyString(password);
+ },
+
+ copyPassword: function passwords_copyPassword() {
+ // Prompt for the master password upfront.
+ let token = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB)
+ .getInternalKeyToken();
+
+ if (this.showPasswords || token.checkPassword(""))
+ this.copySelPassword();
+ else {
+ try {
+ token.login(true);
+ this.copySelPassword();
+ } catch (ex) {
+ // If user cancels an exception is expected.
+ }
+ }
+ },
+
+ reactToChange: function passwords_reactToChange(aSubject, aData) {
+
+ // Not interested in legacy hostsaving changes here.
+ // They will be handled in perm-changed.
+ if (/^hostSaving/.test(aData))
+ return;
+
+ // aData: addLogin, modifyLogin, removeLogin, removeAllLogins
+ if (aData == "removeAllLogins") {
+ // Go for re-parsing the whole thing.
+ if (this.displayedSignons.length) {
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.tree.view.selection.clearSelection();
+ this.displayedSignons = [];
+ this.tree.treeBoxObject.endUpdateBatch();
+ }
+ this.loadList();
+ let domainList = [];
+ for (let lSignon of this.allSignons) {
+ let domain = gDomains.getDomainFromHost(lSignon.hostname);
+ if (!domainList.includes(domain))
+ domainList.push(domain);
+ }
+ gDomains.resetFlagToDomains("hasPasswords", domainList);
+ return;
+ }
+
+ // Usual notifications for addLogin, modifyLogin, removeLogin - do "surgical" updates.
+ let curLogin = null, oldLogin = null;
+ if (aData == "modifyLogin" &&
+ aSubject instanceof Ci.nsIArray) {
+ let enumerator = aSubject.enumerate();
+ if (enumerator.hasMoreElements()) {
+ oldLogin = enumerator.getNext();
+ oldLogin.QueryInterface(Ci.nsILoginInfo);
+ }
+ if (enumerator.hasMoreElements()) {
+ curLogin = enumerator.getNext();
+ curLogin.QueryInterface(Ci.nsILoginInfo);
+ }
+ }
+ else if (aSubject instanceof Ci.nsILoginInfo) {
+ curLogin = aSubject; oldLogin = aSubject;
+ }
+ else {
+ Cu.reportError("Observed an unrecognized signon change of type " + aData);
+ }
+
+ let domain = gDomains.getDomainFromHost(curLogin.hostname);
+ // Does change affect possibly loaded Passwords pane?
+ let affectsLoaded = this.displayedSignons.length &&
+ gDomains.hostMatchesSelected(curLogin.hostname);
+ if (aData == "addLogin") {
+ this.allSignons.push(curLogin);
+
+ if (affectsLoaded) {
+ this.displayedSignons.push(this.allSignons[this.allSignons.length - 1]);
+ this.tree.treeBoxObject.rowCountChanged(this.allSignons.length - 1, 1);
+ this.sort(null, true, false);
+ }
+ else {
+ gDomains.addDomainOrFlag(curLogin.hostname, "hasPasswords");
+ }
+ }
+ else {
+ let idx = -1, disp_idx = -1, domainPasswords = 0;
+ if (affectsLoaded) {
+ for (let i = 0; i < this.displayedSignons.length; i++) {
+ let signon = this.displayedSignons[i];
+ if (signon && signon.equals(oldLogin)) {
+ idx = this.allSignons.indexOf(this.displayedSignons[i]);
+ disp_idx = i;
+ break;
+ }
+ }
+ if (aData == "removeLogin")
+ domainPasswords = this.displayedSignons.length;
+ }
+ else {
+ for (let i = 0; i < this.allSignons.length; i++) {
+ let signon = this.allSignons[i];
+ if (signon && signon.equals(oldLogin)) {
+ idx = i;
+ if (aData != "removeLogin")
+ break;
+ }
+ if (aData == "removeLogin" &&
+ gDomains.getDomainFromHost(signon.hostname) == domain)
+ domainPasswords++;
+ }
+ }
+ if (idx >= 0) {
+ if (aData == "modifyLogin") {
+ this.allSignons[idx] = curLogin;
+ if (affectsLoaded)
+ this.tree.treeBoxObject.invalidateRow(disp_idx);
+ }
+ else if (aData == "removeLogin") {
+ this.allSignons.splice(idx, 1);
+ if (affectsLoaded) {
+ this.displayedSignons.splice(disp_idx, 1);
+ this.tree.treeBoxObject.rowCountChanged(disp_idx, -1);
+ }
+ if (domainPasswords == 1)
+ gDomains.removeDomainOrFlag(domain, "hasPasswords");
+ }
+ }
+ }
+ },
+
+ forget: function passwords_forget() {
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = this.allSignons.length - 1; i >= 0; i--) {
+ if (gDomains.hostMatchesSelected(this.allSignons[i].hostname)) {
+ // Remove from internal list needs to be before actually deleting.
+ let delSignon = this.allSignons[i];
+ this.allSignons.splice(i, 1);
+ Services.logins.removeLogin(delSignon);
+ }
+ }
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasPasswords");
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.displayedSignons.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ let signon = this.displayedSignons[aRow];
+ switch (aColumn.id) {
+ case "pwdHostCol":
+ return signon.httpRealm ?
+ (signon.hostname + " (" + signon.httpRealm + ")") :
+ signon.hostname;
+ case "pwdUserCol":
+ return signon.username || "";
+ case "pwdPasswordCol":
+ return signon.password || "";
+ }
+ },
+};
+
+// :::::::::::::::::::: web storage panel ::::::::::::::::::::
+var gStorage = {
+ tree: null,
+ removeButton: null,
+
+ storages: [],
+ displayedStorages: [],
+
+ initialize: function storage_initialize() {
+ gDataman.debugMsg("Initializing storage panel");
+ this.tree = document.getElementById("storageTree");
+ this.tree.view = this;
+
+ this.removeButton = document.getElementById("storageRemove");
+
+ this.tree.treeBoxObject.beginUpdateBatch();
+ // this.loadList() is being called in gDomains.initialize() already
+ this.displayedStorages = this.storages.filter(
+ function (aStorage) {
+ return gDomains.hostMatchesSelected(aStorage.rawHost);
+ });
+ this.sort(null, false, false);
+ this.tree.treeBoxObject.endUpdateBatch();
+ },
+
+ shutdown: function storage_shutdown() {
+ gDataman.debugMsg("Shutting down storage panel");
+ this.tree.view.selection.clearSelection();
+ this.tree.view = null;
+ this.displayedStorages = [];
+ },
+
+ loadList: function storage_loadList() {
+ this.storages = [];
+
+ // Load appCache entries.
+ let groups = gLocSvc.appcache.getGroups();
+ gDataman.debugMsg("Loading " + groups.length + " appcache entries");
+ for (let lGroup of groups) {
+ let uri = Services.io.newURI(lGroup);
+ let cache = gLocSvc.appcache.getActiveCache(lGroup);
+ this.storages.push({host: uri.host,
+ rawHost: uri.host,
+ type: "appCache",
+ size: cache.usage,
+ groupID: lGroup});
+ }
+
+ // Load DOM storage entries, unfortunately need to go to the DB. :(
+ // Bug 343163 would make this easier and clean.
+ let domstorelist = [];
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("webappsstore.sqlite");
+ if (file.exists()) {
+ var connection = Services.storage.openDatabase(file);
+ try {
+ if (connection.tableExists("webappsstore2")) {
+ var statement =
+ connection.createStatement("SELECT scope, key FROM webappsstore2");
+ while (statement.executeStep())
+ domstorelist.push({scope: statement.getString(0),
+ key: statement.getString(1)});
+ statement.reset();
+ statement.finalize();
+ }
+ } finally {
+ connection.close();
+ }
+ }
+ gDataman.debugMsg("Loading " + domstorelist.length + " DOM Storage entries");
+ // Scopes are reversed, e.g. |moc.elgoog.www.:http:80| (for localStorage).
+ for (let i = 0; i < domstorelist.length; i++) {
+ // Get the host from the reversed scope.
+ let scopeparts = domstorelist[i].scope.split(":");
+ let host = "", type = "unknown";
+ let origHost = scopeparts[0].split("").reverse().join("");
+ let rawHost = host = origHost.replace(/^\./, "");
+ if (scopeparts.length > 1) {
+ // This is a localStore, [1] is protocol, [2] is port.
+ type = "localStorage";
+ host = scopeparts[1].length ? scopeparts[1] + "://" + host : host;
+ // Add port if it's not the default for this protocol.
+ if (scopeparts[2] &&
+ !((scopeparts[1] == "http" && scopeparts[2] == 80) ||
+ (scopeparts[1] == "https" && scopeparts[2] == 443))) {
+ host = host + ":" + scopeparts[2];
+ }
+ }
+ // Make sure we only add known/supported types
+ if (type != "unknown") {
+ // Merge entries for one scope into a single entry if possible.
+ let scopefound = false;
+ for (let j = 0; j < this.storages.length; j++) {
+ if (this.storages[j].type == type && this.storages[j].host == host) {
+ this.storages[j].keys.push(domstorelist[i].key);
+ scopefound = true;
+ break;
+ }
+ }
+ if (!scopefound) {
+ this.storages.push({host: host,
+ rawHost: rawHost,
+ type: type,
+ // FIXME if you want getUsage no longer exists
+ // But I think it's not worth it. Seems the only way
+ // to do this is to get all the key names and values
+ // and add the string lengths together
+ // size: gLocSvc.domstoremgr.getUsage(rawHost),
+ size: 0,
+ origHost: origHost,
+ keys: [domstorelist[i].key]});
+ }
+ }
+ }
+
+ // Load indexedDB entries, unfortunately need to read directory for now. :(
+ // Bug 630858 would make this easier and clean.
+ let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dir.append("indexedDB");
+ if (dir.exists() && dir.isDirectory()) {
+ // Enumerate subdir entries, names are like "http+++davidflanagan.com" or
+ // "https+++mochi.test+8888", and filter out the domain name and protocol
+ // from that.
+ // gLocSvc.idxdbmgr is usable as soon as we have a URI.
+ let files = dir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ gDataman.debugMsg("Loading IndexedDB entries");
+
+ while (files.hasMoreElements()) {
+ let file = files.nextFile;
+ // Convert directory name to a URI.
+ let host = file.leafName.replace(/\+\+\+/, "://").replace(/\+(\d+)$/, ":$1");
+ let uri = Services.io.newURI(host);
+ this.storages.push({host: host,
+ rawHost: uri.host,
+ type: "indexedDB",
+ size: 0,
+ path: file.path});
+ // Get IndexedDB usage (DB size)
+ // See http://mxr.mozilla.org/mozilla-central/source/dom/indexedDB/nsIIndexedDatabaseManager.idl?mark=39-52#39
+ gLocSvc.idxdbmgr.getUsageForURI(uri,
+ function(aUri, aUsage) {
+ gStorage.storages.forEach(function(aElement) {
+ if (aUri.host == aElement.rawHost)
+ aElement.size = aUsage;
+ });
+ });
+ }
+ }
+ },
+
+ _getObjID: function storage__getObjID(aIdx) {
+ var curStorage = gStorage.displayedStorages[aIdx];
+ return curStorage.host + "|" + curStorage.type;
+ },
+
+ select: function storage_select() {
+ var selections = gDataman.getTreeSelections(this.tree);
+ this.removeButton.disabled = !selections.length;
+ return true;
+ },
+
+ selectAll: function storage_selectAll() {
+ this.tree.view.selection.selectAll();
+ },
+
+ handleKeyPress: function storage_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.delete();
+ }
+ },
+
+ sort: function storage_sort(aColumn, aUpdateSelection, aInvertDirection) {
+ // Make sure we have a valid column.
+ let column = aColumn;
+ if (!column) {
+ let sortedCol = this.tree.columns.getSortedColumn();
+ if (sortedCol)
+ column = sortedCol.element;
+ else
+ column = document.getElementById("storageHostCol");
+ }
+ else if (column.localName == "treecols" || column.localName == "splitter")
+ return;
+
+ if (!column || column.localName != "treecol") {
+ Cu.reportError("No column found to sort form data by");
+ return;
+ }
+
+ let dirAscending = column.getAttribute("sortDirection") !=
+ (aInvertDirection ? "ascending" : "descending");
+ let dirFactor = dirAscending ? 1 : -1;
+
+ // Clear attributes on all columns, we're setting them again after sorting.
+ for (let node = column.parentNode.firstChild; node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // compare function for two content prefs
+ let compfunc = function storage_sort_compare(aOne, aTwo) {
+ switch (column.id) {
+ case "storageHostCol":
+ return dirFactor * aOne.host.localeCompare(aTwo.host);
+ case "storageTypeCol":
+ return dirFactor * aOne.type.localeCompare(aTwo.type);
+ }
+ return 0;
+ };
+
+ if (aUpdateSelection) {
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ }
+ this.tree.view.selection.clearSelection();
+
+ // Do the actual sorting of the array.
+ this.displayedStorages.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+
+ if (aUpdateSelection) {
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ }
+
+ // Set attributes to the sorting we did.
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", dirAscending ? "ascending" : "descending");
+ },
+
+ delete: function storage_delete() {
+ var selections = gDataman.getTreeSelections(this.tree);
+
+ if (selections.length > 1) {
+ let title = gDataman.bundle.getString("storage.deleteSelectedTitle");
+ let msg = gDataman.bundle.getString("storage.deleteSelected");
+ let flags = ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ let yes = gDataman.bundle.getString("storage.deleteSelectedYes");
+ if (Services.prompt.confirmEx(window, title, msg, flags, yes, null, null,
+ null, {value: 0}) == 1) // 1=="Cancel" button
+ return;
+ }
+
+ this.tree.view.selection.clearSelection();
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = selections.length - 1; i >= 0; i--) {
+ let delStorage = this.displayedStorages[selections[i]];
+ this.storages.splice(
+ this.storages.indexOf(this.displayedStorages[selections[i]]), 1);
+ this.displayedStorages.splice(selections[i], 1);
+ this.tree.treeBoxObject.rowCountChanged(selections[i], -1);
+ // Remove the actual entry.
+ this._deleteItem(delStorage);
+ }
+ if (!this.displayedStorages.length)
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasStorage");
+ // Select the entry after the first deleted one or the last of all entries.
+ if (selections.length && this.displayedStorages.length)
+ this.tree.view.selection.toggleSelect(selections[0] < this.displayedStorages.length ?
+ selections[0] :
+ this.displayedStorages.length - 1);
+ },
+
+ _deleteItem: function storage__deleteItem(aStorageItem) {
+ switch (aStorageItem.type) {
+ case "appCache":
+ gLocSvc.appcache.getActiveCache(aStorageItem.groupID).discard();
+ break;
+ case "localStorage":
+ let testHost = aStorageItem.host;
+ if (!/:/.test(testHost))
+ testHost = "http://" + testHost;
+ let uri = Services.io.newURI(testHost);
+ let principal = gLocSvc.ssm.createCodebasePrincipal(uri, {});
+ let storage = gLocSvc.domstoremgr.createStorage(null, principal, "");
+ storage.clear();
+ break;
+ case "indexedDB":
+ gLocSvc.idxdbmgr.clearDatabasesForURI(
+ Services.io.newURI(aStorageItem.host));
+ break;
+ }
+ },
+
+ updateContext: function storage_updateContext() {
+ document.getElementById("storage-context-remove").disabled =
+ this.removeButton.disabled;
+ document.getElementById("storage-context-selectall").disabled =
+ this.tree.view.selection.count >= this.tree.view.rowCount;
+ },
+
+ reloadList: function storage_reloadList() {
+ // As many storage types don't have app-wide functions to notify us of
+ // changes, call this one periodically to completely redo the storage
+ // list and so keep the Data Manager up to date.
+ var selectionCache = [];
+ if (this.displayedStorages.length) {
+ selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ this.displayedStorages = [];
+ }
+ this.loadList();
+ var domainList = [];
+ for (let lStorage of this.storages) {
+ let domain = gDomains.getDomainFromHost(lStorage.rawHost);
+ if (!domainList.includes(domain))
+ domainList.push(domain);
+ }
+ gDomains.resetFlagToDomains("hasStorage", domainList);
+ // Restore the local panel display if needed.
+ if (gTabs.activePanel == "storagePanel" &&
+ gDomains.selectedDomain.hasStorage) {
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.displayedStorages = this.storages.filter(
+ function (aStorage) {
+ return gDomains.hostMatchesSelected(aStorage.rawHost);
+ });
+ this.sort(null, false, false);
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ this.tree.treeBoxObject.endUpdateBatch();
+ }
+ },
+
+ reactToChange: function storage_reactToChange(aSubject, aData) {
+ // aData: null (sessionStorage, localStorage) + nsIDOMStorageEvent in aSubject
+ // --- for appCache and indexedDB, no change notifications are known!
+ // --- because of that, we don't do anything here and instead use
+ // reloadList periodically
+ // session storage also comes here, but currently not supported
+ // aData: null, all data in aSubject
+ // see https://developer.mozilla.org/en/DOM/Event/StorageEvent
+ switch (aData) {
+ case "localStorage":
+ case "sessionStorage":
+ break;
+ default:
+ Cu.reportError("Observed an unrecognized storage change of type " + aData);
+ }
+
+ gDataman.debugMsg("Found storage event for: " + aData);
+ },
+
+ forget: function storage_forget() {
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = this.storages.length - 1; i >= 0; i--) {
+ if (gDomains.hostMatchesSelected(this.storages[i].hostname)) {
+ // Remove from internal list should be before actually deleting.
+ let delStorage = this.storages[i];
+ this.storages.splice(i, 1);
+ this._deleteItem(delStorage);
+ }
+ }
+ gDomains.removeDomainOrFlag(gDomains.selectedDomain.title, "hasStorage");
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.displayedStorages.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ let storage = this.displayedStorages[aRow];
+ switch (aColumn.id) {
+ case "storageHostCol":
+ return storage.host;
+ case "storageTypeCol":
+ return storage.type;
+ case "storageSizeCol":
+ return gDataman.bundle.getFormattedString("storageUsage",
+ DownloadUtils.convertByteUnits(storage.size));
+ }
+ },
+};
+
+// :::::::::::::::::::: form data panel ::::::::::::::::::::
+var gFormdata = {
+ tree: null,
+ removeButton: null,
+ searchfield: null,
+
+ formdata: [],
+ displayedFormdata: [],
+
+ initialize: function formdata_initialize() {
+ gDataman.debugMsg("Initializing form data panel");
+ this.tree = document.getElementById("formdataTree");
+ this.tree.view = this;
+
+ this.searchfield = document.getElementById("fdataSearch");
+ this.removeButton = document.getElementById("fdataRemove");
+
+ // Always load fresh list, no need to react to changes when pane not open.
+ this.loadList();
+ this.search("");
+ },
+
+ shutdown: function formdata_shutdown() {
+ gDataman.debugMsg("Shutting down form data panel");
+ this.tree.view.selection.clearSelection();
+ this.tree.view = null;
+ this.displayedFormdata = [];
+ },
+
+ _promiseLoadFormHistory: function formdata_promiseLoadFormHistory() {
+ return new Promise(resolve => {
+ let callbacks = {
+ handleResult(result) {
+ gFormdata.formdata.push({fieldname: result.fieldname,
+ value: result.value,
+ timesUsed: result.timesUsed,
+ firstUsed: gFormdata._getTimeString(result.firstUsed),
+ firstUsedSortValue: result.firstUsed,
+ lastUsed: gFormdata._getTimeString(result.lastUsed),
+ lastUsedSortValue: result.lastUsed,
+ guid: result.guid});
+ },
+ handleError(aError) {
+ Cu.reportError(aError);
+ },
+ handleCompletion(aReason) {
+ // This needs to stay in or Async.promiseSpinningly will fail.
+ resolve();
+ }
+ };
+ gLocSvc.FormHistory.search(["fieldname", "value", "timesUsed", "firstUsed", "lastUsed", "guid"],
+ {},
+ callbacks);
+ });
+ },
+
+ loadList: function formdata_loadList() {
+ this.formdata = [];
+ // Use Async.promiseSpinningly to Sync the call.
+ Async.promiseSpinningly(this._promiseLoadFormHistory());
+ },
+
+ _getTimeString: function formdata__getTimeString(aTimestamp) {
+ if (aTimestamp) {
+ let date = new Date(aTimestamp / 1000);
+
+ // If a date has an extreme value, the dateservice can't cope with it
+ // properly, so we'll just return a blank string.
+ // See bug 238045 for details.
+ let dtString = "";
+ try {
+ const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "full", timeStyle: "long" });
+ dtString = dateTimeFormatter.format(date);
+ }
+ catch (e) {}
+ return dtString;
+ }
+ return "";
+ },
+
+ _getObjID: function formdata__getObjID(aIdx) {
+ return gFormdata.displayedFormdata[aIdx].guid;
+ },
+
+ select: function formdata_select() {
+ var selections = gDataman.getTreeSelections(this.tree);
+ this.removeButton.disabled = !selections.length;
+ return true;
+ },
+
+ selectAll: function formdata_selectAll() {
+ this.tree.view.selection.selectAll();
+ },
+
+ handleKeyPress: function formdata_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.delete();
+ }
+ },
+
+ sort: function formdata_sort(aColumn, aUpdateSelection, aInvertDirection) {
+ // Make sure we have a valid column.
+ let column = aColumn;
+ if (!column) {
+ let sortedCol = this.tree.columns.getSortedColumn();
+ if (sortedCol)
+ column = sortedCol.element;
+ else
+ column = document.getElementById("fdataFieldCol");
+ }
+ else if (column.localName == "treecols" || column.localName == "splitter")
+ return;
+
+ if (!column || column.localName != "treecol") {
+ Cu.reportError("No column found to sort form data by");
+ return;
+ }
+
+ let dirAscending = column.getAttribute("sortDirection") !=
+ (aInvertDirection ? "ascending" : "descending");
+ let dirFactor = dirAscending ? 1 : -1;
+
+ // Clear attributes on all columns, we're setting them again after sorting.
+ for (let node = column.parentNode.firstChild; node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // compare function for two formdata items
+ let compfunc = function formdata_sort_compare(aOne, aTwo) {
+ switch (column.id) {
+ case "fdataFieldCol":
+ return dirFactor * aOne.fieldname.localeCompare(aTwo.fieldname);
+ case "fdataValueCol":
+ return dirFactor * aOne.value.localeCompare(aTwo.value);
+ case "fdataCountCol":
+ return dirFactor * (aOne.timesUsed - aTwo.timesUsed);
+ case "fdataFirstCol":
+ return dirFactor * (aOne.firstUsedSortValue - aTwo.firstUsedSortValue);
+ case "fdataLastCol":
+ return dirFactor * (aOne.lastUsedSortValue - aTwo.lastUsedSortValue);
+ }
+ return 0;
+ };
+
+ if (aUpdateSelection) {
+ var selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ }
+ this.tree.view.selection.clearSelection();
+
+ // Do the actual sorting of the array.
+ this.displayedFormdata.sort(compfunc);
+ this.tree.treeBoxObject.invalidate();
+
+ if (aUpdateSelection) {
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ }
+
+ // Set attributes to the sorting we did.
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", dirAscending ? "ascending" : "descending");
+ },
+
+ delete: function formdata_delete() {
+ var selections = gDataman.getTreeSelections(this.tree);
+
+ if (selections.length > 1) {
+ let title = gDataman.bundle.getString("fdata.deleteSelectedTitle");
+ let msg = gDataman.bundle.getString("fdata.deleteSelected");
+ let flags = ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ let yes = gDataman.bundle.getString("fdata.deleteSelectedYes");
+ if (Services.prompt.confirmEx(window, title, msg, flags, yes, null, null,
+ null, {value: 0}) == 1) // 1=="Cancel" button
+ return;
+ }
+
+ this.tree.view.selection.clearSelection();
+ // Loop backwards so later indexes in the list don't change.
+ for (let i = selections.length - 1; i >= 0; i--) {
+ let delFData = this.displayedFormdata[selections[i]];
+ this.formdata.splice(this.formdata.indexOf(this.displayedFormdata[selections[i]]), 1);
+ this.displayedFormdata.splice(selections[i], 1);
+ this.tree.treeBoxObject.rowCountChanged(selections[i], -1);
+ let changes = [{op: "remove",
+ fieldname: delFData.fieldname,
+ value: delFData.value}];
+ // Async call but we don't care about the completion just now and remove the entry from the panel.
+ // If the call fails the entry will just reappear the next time the form panel is opened.
+ gLocSvc.FormHistory.update(changes);
+ }
+ // Select the entry after the first deleted one or the last of all entries.
+ if (selections.length && this.displayedFormdata.length)
+ this.tree.view.selection.toggleSelect(selections[0] < this.displayedFormdata.length ?
+ selections[0] :
+ this.displayedFormdata.length - 1);
+ },
+
+ search: function formdata_search(aSearchString) {
+ let selectionCache = gDataman.getSelectedIDs(this.tree, this._getObjID);
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.tree.view.selection.clearSelection();
+ var lcSearch = aSearchString.toLocaleLowerCase();
+ this.displayedFormdata = this.formdata.filter(
+ function(aFd) {
+ return aFd.fieldname.toLocaleLowerCase().includes(lcSearch) ||
+ aFd.value.toLocaleLowerCase().includes(lcSearch);
+ });
+ this.sort(null, false, false);
+ gDataman.restoreSelectionFromIDs(this.tree, this._getObjID, selectionCache);
+ this.tree.treeBoxObject.endUpdateBatch();
+ },
+
+ focusSearch: function formdata_focusSearch() {
+ this.searchfield.focus();
+ },
+
+ updateContext: function formdata_updateContext() {
+ document.getElementById("fdata-context-remove").disabled =
+ this.removeButton.disabled;
+ document.getElementById("fdata-context-selectall").disabled =
+ this.tree.view.selection.count >= this.tree.view.rowCount;
+ },
+
+ /**
+ * _promiseReadFormHistory
+ *
+ * Retrieves the formddata from the data for the given guid.
+ *
+ * @param aGuid guid for which form data should be returned.
+ * @return Promise<null if no row is found with the specified guid,
+ * or an object containing the row full content values>
+ */
+ _promiseReadFormHistory: function formdata_promiseReadFormHistory(aGuid) {
+
+ return new Promise((resolve, reject) => {
+ var entry = null;
+ let callbacks = {
+ handleResult(result) {
+ // There can be only one entry for a given guid.
+ // If there are more we will not behead it but instead
+ // only keep the last returned result.
+ entry = result;
+ },
+ handleError(aError) {
+ Cu.reportError(aError);
+ reject(error);
+ },
+ handleCompletion(aReason) {
+ resolve(entry);
+ }
+ };
+
+ gLocSvc.FormHistory.search(["fieldname", "value", "timesUsed", "firstUsed", "lastUsed", "guid"],
+ {guid :aGuid},
+ callbacks);
+ });
+ },
+
+ // Updates the form data panel when receiving a notification.
+ //
+ // The notification type is passed in aData.
+ //
+ // The following types are supported:
+ // formhistory-add formhistory-update formhistory-remove
+ // formhistory-expireoldentries
+ //
+ // The following types will be ignored:
+ // formhistory-shutdown formhistory-beforeexpireoldentries
+ reactToChange: function formdata_reactToChange(aSubject, aData) {
+
+ // Ignore changes when no form data pane is loaded
+ // or if we caught an unsupported notification.
+ if (!this.displayedFormdata.length ||
+ aData == "formhistory-shutdown" ||
+ aData == "formhistory-beforeexpireoldentries") {
+ return;
+ }
+
+ if (aData == "formhistory-expireoldentries") {
+ // Go for re-parsing the whole thing.
+ this.tree.treeBoxObject.beginUpdateBatch();
+ this.tree.view.selection.clearSelection();
+ this.displayedFormdata = [];
+ this.tree.treeBoxObject.endUpdateBatch();
+
+ this.loadList();
+ this.search("");
+ return;
+ }
+
+ if (aData != "formhistory-add" && aData != "formhistory-change" &&
+ aData != "formhistory-remove") {
+ Cu.reportError("Observed an unrecognized formdata change of type " + aData);
+ return;
+ }
+
+ var cGuid = null;
+
+ if (aSubject instanceof Ci.nsISupportsString) {
+ cGuid = aSubject.toString();
+ }
+
+ if (!cGuid) {
+ // See bug 1346850. Remove has a problem and always sends a null guid.
+ // We just let the panel stay the same which might cause minor problems
+ // because there is no longer a notification when removing all entries.
+ if (aData != "formhistory-remove") {
+ Cu.reportError("FormHistory guid is null for " + aData);
+ }
+ return;
+ }
+
+ var entryData = null;
+
+ if (aData == "formhistory-add" || aData == "formhistory-change") {
+ // Use Async.promiseSpinningly to Sync the call.
+ Async.promiseSpinningly(this._promiseReadFormHistory(cGuid).then(entry => {
+ if (entry) {
+ entryData = entry;
+ }
+ return;
+ }));
+
+ if (!entryData) {
+ Cu.reportError("Could not find added/modifed formdata entry");
+ return;
+ }
+ }
+
+ if (aData == "formhistory-add") {
+ this.formdata.push(entryData);
+ this.displayedFormdata.push(this.formdata[this.formdata.length - 1]);
+ this.tree.treeBoxObject.rowCountChanged(this.formdata.length - 1, 1);
+ this.search("");
+ }
+ else {
+ let idx = -1, disp_idx = -1;
+ for (let i = 0; i < this.displayedFormdata.length; i++) {
+ let fdata = this.displayedFormdata[i];
+ if (fdata && fdata.guid == cGuid) {
+ idx = this.formdata.indexOf(this.displayedFormdata[i]);
+ disp_idx = i;
+ break;
+ }
+ }
+ if (idx >= 0) {
+ if (aData == "formhistory-change") {
+ this.formdata[idx] = entryData;
+ this.tree.treeBoxObject.invalidateRow(disp_idx);
+ }
+ else if (aData == "formhistory-remove") {
+ this.formdata.splice(idx, 1);
+ this.displayedFormdata.splice(disp_idx, 1);
+ this.tree.treeBoxObject.rowCountChanged(disp_idx, -1);
+ }
+ }
+ }
+ },
+
+ forget: function formdata_forget() {
+ gLocSvc.FormHistory.update({ op: "remove" });
+ },
+
+ // nsITreeView
+ __proto__: gBaseTreeView,
+ get rowCount() {
+ return this.displayedFormdata.length;
+ },
+ getCellText: function(aRow, aColumn) {
+ let fdata = this.displayedFormdata[aRow];
+ switch (aColumn.id) {
+ case "fdataFieldCol":
+ return fdata.fieldname;
+ case "fdataValueCol":
+ return fdata.value;
+ case "fdataCountCol":
+ return fdata.timesUsed;
+ case "fdataFirstCol":
+ return fdata.firstUsed;
+ case "fdataLastCol":
+ return fdata.lastUsed;
+ }
+ },
+};
+
+// :::::::::::::::::::: forget panel ::::::::::::::::::::
+var gForget = {
+ forgetDesc: null,
+ forgetCookies: null,
+ forgetPermissions: null,
+ forgetPreferences: null,
+ forgetPasswords: null,
+ forgetStorage: null,
+ forgetFormdata: null,
+ forgetCookiesLabel: null,
+ forgetPermissionsLabel: null,
+ forgetPreferencesLabel: null,
+ forgetPasswordsLabel: null,
+ forgetStorageLabel: null,
+ forgetFormdataLabel: null,
+ forgetButton: null,
+
+ initialize: function forget_initialize() {
+ gDataman.debugMsg("Initializing forget panel");
+
+ this.forgetDesc = document.getElementById("forgetDesc");
+ ["forgetCookies", "forgetPermissions", "forgetPreferences",
+ "forgetPasswords", "forgetStorage", "forgetFormdata"]
+ .forEach(function(elemID) {
+ gForget[elemID] = document.getElementById(elemID);
+ gForget[elemID].hidden = false;
+ gForget[elemID].checked = false;
+ let labelID = elemID + "Label";
+ gForget[labelID] = document.getElementById(labelID);
+ gForget[labelID].hidden = true;
+ });
+ this.forgetButton = document.getElementById("forgetButton");
+ this.forgetButton.hidden = false;
+
+ if (gDomains.selectedDomain.title == "*")
+ this.forgetDesc.value = gDataman.bundle.getString("forget.desc.global.pre");
+ else
+ this.forgetDesc.value = gDataman.bundle.getFormattedString("forget.desc.domain.pre",
+ [gDomains.selectedDomain.title]);
+
+ this.forgetCookies.disabled = !gDomains.selectedDomain.hasCookies;
+ this.forgetPermissions.disabled = !gDomains.selectedDomain.hasPermissions;
+ this.forgetPreferences.disabled = !gDomains.selectedDomain.hasPreferences;
+ this.forgetPasswords.disabled = !gDomains.selectedDomain.hasPasswords;
+ this.forgetStorage.disabled = !gDomains.selectedDomain.hasStorage;
+ this.forgetFormdata.disabled = !gDomains.selectedDomain.hasFormData;
+ this.forgetFormdata.hidden = !gDomains.selectedDomain.hasFormData;
+ this.updateOptions();
+ },
+
+ shutdown: function forget_shutdown() {
+ gDataman.debugMsg("Shutting down forget panel");
+ },
+
+ updateOptions: function forget_updateOptions() {
+ this.forgetButton.disabled = !(this.forgetCookies.checked ||
+ this.forgetPermissions.checked ||
+ this.forgetPreferences.checked ||
+ this.forgetPasswords.checked ||
+ this.forgetStorage.checked ||
+ this.forgetFormdata.checked);
+ },
+
+ handleKeyPress: function forget_handleKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ // Make sure we do something that makes this panel go away.
+ if (gDomains.selectedDomain.title)
+ gDomains.select();
+ else
+ gDomains.tree.view.selection.select(0);
+ }
+ },
+
+ forget: function forget_forget() {
+ // Domain might get removed and selected domain changed!
+ let delDomainTitle = gDomains.selectedDomain.title;
+
+ if (this.forgetCookies.checked) {
+ gCookies.forget();
+ this.forgetCookiesLabel.hidden = false;
+ }
+ this.forgetCookies.hidden = true;
+
+ if (this.forgetPermissions.checked) {
+ gPerms.forget();
+ this.forgetPermissionsLabel.hidden = false;
+ }
+ this.forgetPermissions.hidden = true;
+
+ if (this.forgetPreferences.checked) {
+ gPrefs.forget();
+ this.forgetPreferencesLabel.hidden = false;
+ }
+ this.forgetPreferences.hidden = true;
+
+ if (this.forgetPasswords.checked) {
+ gPasswords.forget();
+ this.forgetPasswordsLabel.hidden = false;
+ }
+ this.forgetPasswords.hidden = true;
+
+ if (this.forgetStorage.checked) {
+ gStorage.forget();
+ this.forgetStorageLabel.hidden = false;
+ }
+ this.forgetStorage.hidden = true;
+
+ if (this.forgetFormdata.checked) {
+ gFormdata.forget();
+ this.forgetFormdataLabel.hidden = false;
+ }
+ this.forgetFormdata.hidden = true;
+
+ if (delDomainTitle == "*")
+ this.forgetDesc.value = gDataman.bundle.getString("forget.desc.global.post");
+ else
+ this.forgetDesc.value = gDataman.bundle.getFormattedString("forget.desc.domain.post",
+ [delDomainTitle]);
+ this.forgetButton.hidden = true;
+ },
+};
diff --git a/comm/suite/components/dataman/content/dataman.xml b/comm/suite/components/dataman/content/dataman.xml
new file mode 100644
index 0000000000..de90cac68d
--- /dev/null
+++ b/comm/suite/components/dataman/content/dataman.xml
@@ -0,0 +1,249 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+<!ENTITY % datamanDTD SYSTEM "chrome://communicator/locale/dataman/dataman.dtd">
+%datamanDTD;
+]>
+
+<bindings id="datamanBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="perm-base-item"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <implementation>
+ <constructor><![CDATA[
+ var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ var permLabel = this.type;
+ try {
+ permLabel = gDataman.bundle.getString("perm." + this.type + ".label");
+ }
+ catch (e) {
+ }
+ this.setAttribute("label", permLabel);
+ this._updateRadio();
+ ]]></constructor>
+
+ <property name="capability">
+ <getter><![CDATA[
+ if (this.hasAttribute("capability"))
+ return this.getAttribute("capability");
+ return -1;
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("capability", val);
+ this._updateRadio();
+ ]]></setter>
+ </property>
+
+ <property name="host" readonly="true"
+ onget="return this.getAttribute('host');"/>
+
+ <property name="type" readonly="true"
+ onget="return this.getAttribute('type');"/>
+
+ <method name="_updateRadio">
+ <body><![CDATA[
+ let radio = document.getAnonymousElementByAttribute(this, "anonid",
+ "permSetting-" + this.capability);
+ if (radio)
+ radio.radioGroup.selectedItem = radio;
+ else {
+ let radioGroup = document.getAnonymousElementByAttribute(this, "anonid",
+ "radioGroup");
+ radioGroup.selectedIndex = -1;
+ }
+ ]]></body>
+ </method>
+
+ <method name="useDefault">
+ <parameter name="aChecked"/>
+ <parameter name="aUIUpdateOnly"/>
+ <body><![CDATA[
+ let checkbox = document.getAnonymousElementByAttribute(this, "anonid",
+ "useDefault");
+ if (checkbox.checked != aChecked)
+ checkbox.checked = aChecked;
+ let radioGroup = document.getAnonymousElementByAttribute(this, "anonid",
+ "radioGroup");
+ radioGroup.disabled = aChecked;
+ if (aChecked) {
+ if (!aUIUpdateOnly)
+ gPerms.removeItem(this.host, this.type);
+
+ this.capability = gPerms.getDefault(this.type);
+ }
+ this._updateRadio();
+ ]]></body>
+ </method>
+
+ <method name="setCapability">
+ <parameter name="aValue"/>
+ <parameter name="aUIUpdateOnly"/>
+ <body><![CDATA[
+ this.capability = aValue;
+ let radio = document.getAnonymousElementByAttribute(this, "anonid",
+ "permSetting-" + aValue);
+ if (radio && !radio.selected)
+ radio.radioGroup.selectedItem = radio;
+ if (!aUIUpdateOnly)
+ gPerms.updateItem(this.host, this.type, aValue);
+ ]]></body>
+ </method>
+
+ <method name="handleKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ this.useDefault(true);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" phase="capturing"
+ action="return this.handleKeyPress(event);"/>
+ </handlers>
+ </binding>
+
+ <binding id="perm-generic-item"
+ extends="chrome://communicator/content/dataman/dataman.xml#perm-base-item">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="permHost" class="hostLabel" xbl:inherits="value=displayHost"/>
+ <xul:label anonid="permLabel" class="permissionLabel" xbl:inherits="value=label" control="radioGroup"/>
+ </xul:hbox>
+ <xul:hbox role="group" aria-labelledby="permLabel">
+ <xul:checkbox class="indent" anonid="useDefault" label="&perm.UseDefault;"
+ oncommand="document.getBindingParent(this).useDefault(this.checked);"/>
+ <xul:spacer flex="1"/>
+ <xul:radiogroup anonid="radioGroup" orient="horizontal">
+ <xul:radio anonid="permSetting-1" label="&perm.Allow;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.ALLOW_ACTION);"/>
+ <xul:radio anonid="permSetting-2" label="&perm.Block;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.DENY_ACTION);"/>
+ </xul:radiogroup>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="perm-cookie-item"
+ extends="chrome://communicator/content/dataman/dataman.xml#perm-base-item">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="permHost" class="hostLabel" xbl:inherits="value=displayHost"/>
+ <xul:label anonid="permLabel" class="permissionLabel" xbl:inherits="value=label" control="radioGroup"/>
+ </xul:hbox>
+ <xul:hbox role="group" aria-labelledby="permLabel">
+ <xul:checkbox class="indent" anonid="useDefault" label="&perm.UseDefault;"
+ oncommand="document.getBindingParent(this).useDefault(this.checked);"/>
+ <xul:spacer flex="1"/>
+ <xul:radiogroup anonid="radioGroup" orient="horizontal">
+ <xul:radio anonid="permSetting-1" label="&perm.Allow;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.ALLOW_ACTION);"/>
+ <xul:radio anonid="permSetting-8" label="&perm.AllowSession;"
+ oncommand="document.getBindingParent(this).setCapability(Ci.nsICookiePermission.ACCESS_SESSION);"/>
+ <xul:radio anonid="permSetting-2" label="&perm.Block;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.DENY_ACTION);"/>
+ </xul:radiogroup>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="perm-geo-item"
+ extends="chrome://communicator/content/dataman/dataman.xml#perm-base-item">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="permHost" class="hostLabel" xbl:inherits="value=displayHost"/>
+ <xul:label anonid="permLabel" class="permissionLabel" xbl:inherits="value=label" control="radioGroup"/>
+ </xul:hbox>
+ <xul:hbox role="group" aria-labelledby="permLabel">
+ <xul:checkbox class="indent" anonid="useDefault" label="&perm.AskAlways;"
+ oncommand="document.getBindingParent(this).useDefault(this.checked);"/>
+ <xul:spacer flex="1"/>
+ <xul:radiogroup anonid="radioGroup" orient="horizontal">
+ <xul:radio anonid="permSetting-1" label="&perm.Allow;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.ALLOW_ACTION);"/>
+ <xul:radio anonid="permSetting-2" label="&perm.Block;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.DENY_ACTION);"/>
+ </xul:radiogroup>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="perm-password-item" extends="chrome://communicator/content/dataman/dataman.xml#perm-base-item">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="permHost" class="hostLabel" xbl:inherits="value=displayHost"/>
+ <xul:label anonid="permLabel" class="permissionLabel" xbl:inherits="value=label" control="radioGroup"/>
+ </xul:hbox>
+ <xul:hbox role="group" aria-labelledby="permLabel">
+ <xul:checkbox class="indent" anonid="useDefault" hidden="true"/>
+ <xul:spacer flex="1"/>
+ <xul:radiogroup anonid="radioGroup" orient="horizontal">
+ <xul:radio anonid="permSetting-1" label="&perm.AskAlways;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.ALLOW_ACTION);"/>
+ <xul:radio anonid="permSetting-2" label="&perm.NeverSave;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.DENY_ACTION);"/>
+ </xul:radiogroup>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="useDefault">
+ <parameter name="aChecked"/>
+ <body><![CDATA[
+ // just for compat, makes it easier to generically "delete" perms
+ if (aChecked)
+ this.setCapability(Services.perms.ALLOW_ACTION);
+ ]]></body>
+ </method>
+
+ <method name="setCapability">
+ <parameter name="aValue"/>
+ <parameter name="aUIUpdateOnly"/>
+ <body><![CDATA[
+ this.capability = aValue;
+ let radio = document.getAnonymousElementByAttribute(this, "anonid",
+ "permSetting-" + aValue);
+ if (radio && !radio.selected)
+ radio.radioGroup.selectedItem = radio;
+ if (!aUIUpdateOnly)
+ Services.logins.setLoginSavingEnabled(this.host, aValue == Services.perms.ALLOW_ACTION);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="perm-content-item"
+ extends="chrome://communicator/content/dataman/dataman.xml#perm-base-item">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="permHost" class="hostLabel" xbl:inherits="value=displayHost"/>
+ <xul:label anonid="permLabel" class="permissionLabel" xbl:inherits="value=label" control="radioGroup"/>
+ </xul:hbox>
+ <xul:hbox role="group" aria-labelledby="permLabel">
+ <xul:checkbox class="indent" anonid="useDefault" label="&perm.UseDefault;"
+ oncommand="document.getBindingParent(this).useDefault(this.checked);"/>
+ <xul:spacer flex="1"/>
+ <xul:radiogroup anonid="radioGroup" orient="horizontal">
+ <xul:radio anonid="permSetting-1" label="&perm.Allow;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.ALLOW_ACTION);"/>
+ <xul:radio anonid="permSetting-3" label="&perm.AllowSameDomain;"
+ oncommand="document.getBindingParent(this).setCapability(NOFOREIGN);"/>
+ <xul:radio anonid="permSetting-2" label="&perm.Block;"
+ oncommand="document.getBindingParent(this).setCapability(Services.perms.DENY_ACTION);"/>
+ </xul:radiogroup>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ </bindings>
diff --git a/comm/suite/components/dataman/content/dataman.xul b/comm/suite/components/dataman/content/dataman.xul
new file mode 100644
index 0000000000..633119f566
--- /dev/null
+++ b/comm/suite/components/dataman/content/dataman.xul
@@ -0,0 +1,571 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/content/dataman/dataman.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/dataman/dataman.css" type="text/css"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE page [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % datamanDTD SYSTEM "chrome://communicator/locale/dataman/dataman.dtd">
+%datamanDTD;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ id="dataman-page" title="&dataman.windowTitle;"
+ windowtype="data:manager"
+ onload="gDataman.initialize();"
+ onunload="gDataman.shutdown();"
+ onkeypress="gDataman.handleKeyPress(event);"
+ persist="screenX screenY width height sizemode">
+
+ <xhtml:link rel="shortcut icon"
+ href="chrome://communicator/skin/dataman/datamanIcon-16.png"/>
+
+ <script src="chrome://communicator/content/dataman/dataman.js"/>
+
+ <stringbundleset id="datamanBundleSet">
+ <stringbundle id="datamanBundle"
+ src="chrome://communicator/locale/dataman/dataman.properties"/>
+ </stringbundleset>
+
+ <commandset id="datamanCommands">
+ <command id="cmd_selectAll"
+ oncommand="gTabs.selectAll();"/>
+ <command id="cmd_search_domain"
+ oncommand="gDomains.focusSearch();"/>
+ <command id="cmd_search_data"
+ oncommand="gTabs.focusSearch();"/>
+ <command id="cmd_close"
+ oncommand="window.close();"/>
+ </commandset>
+
+ <keyset id="datamanKeys">
+ <key id="key_close"/>
+ <key id="key_selectAll"/>
+ <key id="key_search_domain"
+ command="cmd_search_domain"
+ key="&domain.search.key;"
+ modifiers="accel"/>
+ <key id="key_search_data"
+ command="cmd_search_data"
+ key="&data.search.key;"
+ modifiers="accel"/>
+ </keyset>
+
+ <popupset id="datamanContextSet">
+ <menupopup id="domainTreeContextMenu"
+ onpopupshowing="gDomains.updateContext();">
+ <menuitem id="domain-context-forget"
+ label_domain="&domain.ctx.forgetdomain.label;"
+ accesskey_domain="&domain.ctx.forgetdomain.accesskey;"
+ label_global="&domain.ctx.forgetglobal.label;"
+ accesskey_global="&domain.ctx.forgetglobal.accesskey;"
+ oncommand="gDomains.forget();"/>
+ </menupopup>
+
+ <menupopup id="cookiesTreeContextMenu"
+ onpopupshowing="gCookies.updateContext();">
+ <menuitem id="cookies-context-remove"
+ label="&cookies.ctx.remove.label;"
+ accesskey="&cookies.ctx.remove.accesskey;"
+ oncommand="gCookies.delete();"/>
+ <menuitem id="cookies-context-selectall"
+ label="&cookies.ctx.selectAll.label;"
+ accesskey="&cookies.ctx.selectAll.accesskey;"
+ oncommand="gCookies.selectAll();"/>
+ </menupopup>
+
+ <menupopup id="prefsTreeContextMenu"
+ onpopupshowing="gPrefs.updateContext();">
+ <menuitem id="prefs-context-remove"
+ label="&prefs.ctx.remove.label;"
+ accesskey="&prefs.ctx.remove.accesskey;"
+ oncommand="gPrefs.delete();"/>
+ <menuitem id="prefs-context-selectall"
+ label="&prefs.ctx.selectAll.label;"
+ accesskey="&prefs.ctx.selectAll.accesskey;"
+ oncommand="gPrefs.selectAll();"/>
+ </menupopup>
+
+ <menupopup id="passwordsTreeContextMenu"
+ onpopupshowing="gPasswords.updateContext();">
+ <menuitem id="pwd-context-remove"
+ label="&pwd.ctx.remove.label;"
+ accesskey="&pwd.ctx.remove.accesskey;"
+ oncommand="gPasswords.delete();"/>
+ <menuitem id="pwd-context-copypassword"
+ label="&pwd.ctx.copyPasswordCmd.label;"
+ accesskey="&pwd.ctx.copyPasswordCmd.accesskey;"
+ oncommand="gPasswords.copyPassword();"/>
+ <menuitem id="pwd-context-selectall"
+ label="&pwd.ctx.selectAll.label;"
+ accesskey="&pwd.ctx.selectAll.accesskey;"
+ oncommand="gPasswords.selectAll();"/>
+ </menupopup>
+
+ <menupopup id="storageTreeContextMenu"
+ onpopupshowing="gStorage.updateContext();">
+ <menuitem id="storage-context-remove"
+ label="&storage.ctx.remove.label;"
+ accesskey="&storage.ctx.remove.accesskey;"
+ oncommand="gStorage.delete();"/>
+ <menuitem id="storage-context-selectall"
+ label="&storage.ctx.selectAll.label;"
+ accesskey="&storage.ctx.selectAll.accesskey;"
+ oncommand="gStorage.selectAll();"/>
+ </menupopup>
+
+ <menupopup id="formdataTreeContextMenu"
+ onpopupshowing="gFormdata.updateContext();">
+ <menuitem id="fdata-context-remove"
+ label="&fdata.ctx.remove.label;"
+ accesskey="&fdata.ctx.remove.accesskey;"
+ oncommand="gFormdata.delete();"/>
+ <menuitem id="fdata-context-selectall"
+ label="&fdata.ctx.selectAll.label;"
+ accesskey="&fdata.ctx.selectAll.accesskey;"
+ oncommand="gFormdata.selectAll();"/>
+ </menupopup>
+ </popupset>
+
+ <hbox flex="1">
+ <vbox flex="1">
+ <menulist id="typeSelect"
+ oncommand="gDomains.selectType(this.value);">
+ <menupopup>
+ <menuitem label="&select.all.label;"
+ value="all"/>
+ <menuitem label="&select.cookies.label;"
+ value="Cookies"/>
+ <menuitem label="&select.permissions.label;"
+ value="Permissions"/>
+ <menuitem label="&select.preferences.label;"
+ value="Preferences"/>
+ <menuitem label="&select.passwords.label;"
+ value="Passwords"/>
+ <menuitem label="&select.storage.label;"
+ value="Storage"/>
+ </menupopup>
+ </menulist>
+ <textbox id="domainSearch"
+ clickSelectsAll="true"
+ type="search"
+ aria-controls="domainTree"
+ class="compact"
+ placeholder="&domain.search.placeholder;"
+ oncommand="gDomains.search(this.value);"/>
+ <tree id="domainTree"
+ seltype="single"
+ flex="1"
+ hidecolumnpicker="true"
+ onkeypress="gDomains.handleKeyPress(event);"
+ onselect="gDomains.select();"
+ context="domainTreeContextMenu">
+ <treecols>
+ <treecol id="domainCol"
+ label="&domain.tree.domain.label;"
+ flex="1"
+ sortDirection="ascending"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ </vbox>
+
+ <splitter/>
+
+ <tabbox id="tabbox" flex="6">
+ <tabs onselect="gTabs.select();">
+ <tab id="cookiesTab"
+ label="&tab.cookies.label;"
+ disabled="true"/>
+ <tab id="permissionsTab"
+ label="&tab.permissions.label;"
+ disabled="true"/>
+ <tab id="preferencesTab"
+ label="&tab.preferences.label;"
+ disabled="true"/>
+ <tab id="passwordsTab"
+ label="&tab.passwords.label;"
+ disabled="true"/>
+ <tab id="storageTab"
+ label="&tab.storage.label;"
+ disabled="true"/>
+ <tab id="formdataTab"
+ label="&tab.formdata.label;"
+ disabled="true"/>
+ <tab id="forgetTab"
+ label="&tab.forget.label;"
+ hidden="true"
+ onkeypress="gForget.handleKeyPress(event);"/>
+ </tabs>
+
+ <tabpanels id="tabpanels" flex="1">
+ <vbox id="cookiesPanel">
+ <description>&cookies.description;</description>
+ <tree id="cookiesTree"
+ flex="1"
+ onkeypress="gCookies.handleKeyPress(event);"
+ onselect="gCookies.select();"
+ context="cookiesTreeContextMenu">
+ <treecols onclick="gCookies.sort(event.target, true, true);">
+ <treecol id="cookieHostCol"
+ label="&cookies.tree.host.label;"
+ flex="5"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="cookieNameCol"
+ label="&cookies.tree.name.label;"
+ flex="5"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="cookieExpiresCol"
+ label="&cookies.tree.expires.label;"
+ flex="10"
+ hidden="true"
+ persist="width hidden"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <groupbox>
+ <caption label="&cookies.infobox.label;"/>
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <hbox pack="end">
+ <label value="&cookies.info.name.label;"
+ control="cookieInfoName"/>
+ </hbox>
+ <textbox id="cookieInfoName" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox pack="end">
+ <label value="&cookies.info.value.label;"
+ control="cookieInfoValue"/>
+ </hbox>
+ <textbox id="cookieInfoValue" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox pack="end">
+ <label id="cookieInfoHostLabel"
+ value="&cookies.info.host.label;"
+ value_host="&cookies.info.host.label;"
+ value_domain="&cookies.info.domain.label;"
+ control="cookieInfoHost"/>
+ </hbox>
+ <textbox id="cookieInfoHost" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox pack="end">
+ <label value="&cookies.info.path.label;"
+ control="cookieInfoPath"/>
+ </hbox>
+ <textbox id="cookieInfoPath" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox pack="end">
+ <label value="&cookies.info.sendtype.label;"
+ control="cookieInfoSendType"/>
+ </hbox>
+ <textbox id="cookieInfoSendType" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox pack="end">
+ <label value="&cookies.info.expires.label;"
+ control="cookieInfoExpires"/>
+ </hbox>
+ <textbox id="cookieInfoExpires" readonly="true" class="plain"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <hbox id="cookieButtons" align="center">
+ <button id="cookieRemove"
+ label="&cookies.button.remove.label;"
+ accesskey="&cookies.button.remove.accesskey;"
+ disabled="true"
+ oncommand="gCookies.delete();"/>
+ <checkbox id="cookieBlockOnRemove"
+ label="&cookies.blockOnRemove.label;"
+ accesskey="&cookies.blockOnRemove.accesskey;"
+ flex="1"
+ persist="checked"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="permissionsPanel">
+ <richlistbox id="permList" flex="1"
+ onkeypress="if (this.selectedItem)
+ this.selectedItem.handleKeyPress(event);"/>
+
+ <hbox id="permAddBox" align="center">
+ <hbox id="permSelectionBox" align="center" hidden="true">
+ <textbox id="permHost"
+ clickSelectsAll="true"
+ class="compact"
+ placeholder="&perm.host.placeholder;"
+ oninput="gPerms.addCheck();"/>
+ <menulist id="permType"
+ oncommand="gPerms.addCheck();"/>
+ </hbox>
+ <button id="permAddButton"
+ label="&perm.button.add.label;"
+ accesskey="&perm.button.add.accesskey;"
+ oncommand="gPerms.addButtonClick();"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="preferencesPanel">
+ <description>&prefs.description;</description>
+ <tree id="prefsTree"
+ flex="1"
+ onkeypress="gPrefs.handleKeyPress(event);"
+ onselect="gPrefs.select();"
+ context="prefsTreeContextMenu">
+ <treecols onclick="gPrefs.sort(event.target, true, true);">
+ <treecol id="prefsHostCol"
+ label="&prefs.tree.host.label;"
+ flex="5"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="prefsNameCol"
+ label="&prefs.tree.name.label;"
+ flex="5"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="prefsValueCol"
+ label="&prefs.tree.value.label;"
+ flex="10"
+ persist="width hidden"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox id="prefsButtons">
+ <button id="prefsRemove"
+ label="&prefs.button.remove.label;"
+ accesskey="&prefs.button.remove.accesskey;"
+ disabled="true"
+ oncommand="gPrefs.delete();"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="passwordsPanel">
+ <description>&pwd.description;</description>
+ <tree id="passwordsTree"
+ flex="1"
+ hidecolumnpicker="true"
+ onkeypress="gPasswords.handleKeyPress(event);"
+ onselect="gPasswords.select();"
+ context="passwordsTreeContextMenu">
+ <treecols onclick="gPasswords.sort(event.target, true, true);">
+ <treecol id="pwdHostCol"
+ label="&pwd.tree.host.label;"
+ flex="5"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="pwdUserCol"
+ label="&pwd.tree.username.label;"
+ flex="2"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="pwdPasswordCol"
+ label="&pwd.tree.password.label;"
+ flex="2"
+ hidden="true"
+ persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox id="passwordButtons">
+ <button id="pwdRemove"
+ label="&pwd.button.remove.label;"
+ accesskey="&pwd.button.remove.accesskey;"
+ disabled="true"
+ oncommand="gPasswords.delete();"/>
+ <spacer flex="1"/>
+ <button id="pwdToggle"
+ oncommand="gPasswords.togglePasswordVisible();"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="storagePanel">
+ <description>&storage.description;</description>
+ <tree id="storageTree"
+ flex="1"
+ hidecolumnpicker="true"
+ onkeypress="gStorage.handleKeyPress(event);"
+ onselect="gStorage.select();"
+ context="storageTreeContextMenu">
+ <treecols onclick="gStorage.sort(event.target, true, true);">
+ <treecol id="storageHostCol"
+ label="&storage.tree.host.label;"
+ flex="5"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="storageTypeCol"
+ label="&storage.tree.type.label;"
+ flex="2"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="storageSizeCol"
+ label="&storage.tree.size.label;"
+ flex="2"
+ persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox id="storageButtons">
+ <button id="storageRemove"
+ label="&storage.button.remove.label;"
+ accesskey="&storage.button.remove.accesskey;"
+ disabled="true"
+ oncommand="gStorage.delete();"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="formdataPanel">
+ <textbox id="fdataSearch"
+ clickSelectsAll="true"
+ type="search"
+ aria-controls="formdataTree"
+ class="compact"
+ placeholder="&fdata.search.placeholder;"
+ oncommand="gFormdata.search(this.value);"/>
+ <tree id="formdataTree"
+ flex="1"
+ onkeypress="gFormdata.handleKeyPress(event);"
+ onselect="gFormdata.select();"
+ context="formdataTreeContextMenu">
+ <treecols onclick="gFormdata.sort(event.target, true, true);">
+ <treecol id="fdataFieldCol"
+ label="&fdata.tree.fieldname.label;"
+ flex="5"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="fdataValueCol"
+ label="&fdata.tree.value.label;"
+ flex="10"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="fdataCountCol"
+ label="&fdata.tree.usecount.label;"
+ flex="2"
+ hidden="true"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="fdataFirstCol"
+ label="&fdata.tree.firstused.label;"
+ flex="10"
+ hidden="true"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="fdataLastCol"
+ label="&fdata.tree.lastused.label;"
+ flex="10"
+ hidden="true"
+ persist="width hidden"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox id="formdataButtons">
+ <button id="fdataRemove"
+ label="&fdata.button.remove.label;"
+ accesskey="&fdata.button.remove.accesskey;"
+ disabled="true"
+ oncommand="gFormdata.delete();"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="forgetPanel">
+ <description id="forgetDesc"/>
+ <hbox>
+ <checkbox id="forgetCookies"
+ label="&forget.cookies.label;"
+ accesskey="&forget.cookies.accesskey;"
+ disabled="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetCookiesLabel"
+ value="&forget.cookies.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <checkbox id="forgetPermissions"
+ label="&forget.permissions.label;"
+ accesskey="&forget.permissions.accesskey;"
+ disabled="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetPermissionsLabel"
+ value="&forget.permissions.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <checkbox id="forgetPreferences"
+ label="&forget.preferences.label;"
+ accesskey="&forget.preferences.accesskey;"
+ disabled="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetPreferencesLabel"
+ value="&forget.preferences.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <checkbox id="forgetPasswords"
+ label="&forget.passwords.label;"
+ accesskey="&forget.passwords.accesskey;"
+ disabled="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetPasswordsLabel"
+ value="&forget.passwords.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <checkbox id="forgetStorage"
+ label="&forget.storage.label;"
+ accesskey="&forget.storage.accesskey;"
+ disabled="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetStorageLabel"
+ value="&forget.storage.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <checkbox id="forgetFormdata"
+ label="&forget.formdata.label;"
+ accesskey="&forget.formdata.accesskey;"
+ disabled="true"
+ hidden="true"
+ oncommand="gForget.updateOptions();"/>
+ <label id="forgetFormdataLabel"
+ value="&forget.formdata.label;"
+ hidden="true"/>
+ </hbox>
+ <hbox>
+ <button id="forgetButton"
+ label="&forget.button.label;"
+ accesskey="&forget.button.accesskey;"
+ disabled="true"
+ oncommand="gForget.forget();"/>
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </hbox>
+
+</page>
diff --git a/comm/suite/components/dataman/jar.mn b/comm/suite/components/dataman/jar.mn
new file mode 100644
index 0000000000..a58d3c9c7c
--- /dev/null
+++ b/comm/suite/components/dataman/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/dataman/dataman.css (content/dataman.css)
+ content/communicator/dataman/dataman.js (content/dataman.js)
+ content/communicator/dataman/dataman.xml (content/dataman.xml)
+ content/communicator/dataman/dataman.xul (content/dataman.xul)
diff --git a/comm/suite/components/dataman/moz.build b/comm/suite/components/dataman/moz.build
new file mode 100644
index 0000000000..aebed195ca
--- /dev/null
+++ b/comm/suite/components/dataman/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser.ini",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/dataman/tests/browser.ini b/comm/suite/components/dataman/tests/browser.ini
new file mode 100644
index 0000000000..5cf2249c53
--- /dev/null
+++ b/comm/suite/components/dataman/tests/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+ dataman_storage.appcache
+ dataman_storage.appcache^headers^
+ dataman_storage.html
+
+[browser_dataman_basics.js]
+[browser_dataman_callviews.js]
diff --git a/comm/suite/components/dataman/tests/browser_dataman_basics.js b/comm/suite/components/dataman/tests/browser_dataman_basics.js
new file mode 100644
index 0000000000..0013127f16
--- /dev/null
+++ b/comm/suite/components/dataman/tests/browser_dataman_basics.js
@@ -0,0 +1,831 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test basic functionality of the data manager.
+
+// Happens to match what's used in Data Manager itself.
+var gLocSvc = {
+ fhist: Cc["@mozilla.org/satchel/form-history;1"]
+ .getService(Ci.nsIFormHistory2),
+ idn: Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService),
+};
+
+const DATAMAN_LOADED = "dataman-loaded";
+const TEST_DONE = "dataman-test-done";
+
+const kPreexistingDomains = 13;
+
+function test() {
+ // Preload data.
+ // Note that before this test starts, what is already set are permissions for
+ // addons.thunderbird.net to install addons as well as
+ // permissions for a number of sites used in mochitest to load XUL/XBL.
+ // For the latter, those 13 domains are used/listed: 127.0.0.1, bank1.com,
+ // bank2.com, example.com, example.org, mochi.test, mozilla.com, test,
+ // w3.org, w3c-test.org, xn--exaple-kqf.test, xn--exmple-cua.test,
+ // xn--hxajbheg2az3al.xn--jxalpdlp
+ // We should not touch those permissions so other tests can run, which means
+ // we should avoid using those domains altogether as we can't remove them.
+
+ let now_epoch = parseInt(Date.now() / 1000);
+
+ // Add dummy permission for getpersonas.com, less work (compared to rewriting
+ // the test to work without getpersonas.com)
+ Services.perms.add(Services.io.newURI("http://getpersonas.com/"),
+ "install", Services.perms.ALLOW_ACTION);
+
+ // Add cookie: not secure, non-HTTPOnly, session
+ Services.cookies.add("bar.geckoisgecko.org", "", "name0", "value0",
+ false, false, true, now_epoch + 600, {});
+ // Add cookie: not secure, HTTPOnly, session
+ Services.cookies.add("foo.geckoisgecko.org", "", "name1", "value1",
+ false, true, true, now_epoch + 600, {});
+ // Add cookie: secure, HTTPOnly, session
+ Services.cookies.add("secure.geckoisgecko.org", "", "name2", "value2",
+ true, true, true, now_epoch + 600, {});
+ // Add cookie: secure, non-HTTPOnly, expiry in an hour
+ Services.cookies.add("drumbeat.org", "", "name3", "value3",
+ true, false, false, now_epoch + 3600, {});
+
+ // Add a cookie for a pure IPv6 address.
+ Services.cookies.add("::1", "", "name4", "value4",
+ false, false, true, now_epoch + 600, {});
+
+ // Add a few form history entries
+ gLocSvc.fhist.addEntry("akey", "value0");
+ gLocSvc.fhist.addEntry("ekey", "value1");
+ gLocSvc.fhist.addEntry("ekey", "value2");
+ gLocSvc.fhist.addEntry("bkey", "value3");
+ gLocSvc.fhist.addEntry("bkey", "value4");
+ gLocSvc.fhist.addEntry("ckey", "value5");
+
+ // Add a few passwords
+ let loginInfo1 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ loginInfo1.init("http://www.geckoisgecko.org", "http://www.geckoisgecko.org", null,
+ "dataman", "mysecret", "user", "pwd");
+ Services.logins.addLogin(loginInfo1);
+ let loginInfo2 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ loginInfo2.init("gopher://geckoisgecko.org:4711", null, "foo",
+ "dataman", "mysecret", "", "");
+ Services.logins.addLogin(loginInfo2);
+ let loginInfo3 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ loginInfo3.init("https://[::1]", null, "foo",
+ "dataman", "mysecret", "", "");
+ Services.logins.addLogin(loginInfo3);
+
+ //Services.prefs.setBoolPref("data_manager.debug", true);
+
+ gBrowser.addTab();
+ // Open the Data Manager, testing the menu item.
+ document.getElementById("tasksDataman").click();
+
+ var testIndex = 0;
+ var win;
+
+ let testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == DATAMAN_LOADED) {
+ Services.obs.removeObserver(testObs, DATAMAN_LOADED);
+ ok(true, "Data Manager is loaded");
+
+ win = content.wrappedJSObject;
+ Services.obs.addObserver(testObs, TEST_DONE);
+ // Trigger the first test now!
+ Services.obs.notifyObservers(window, TEST_DONE);
+ }
+ else {
+ // TEST_DONE triggered, run next test
+ info("run test #" + (testIndex + 1) + " of " + testFuncs.length +
+ " (" + testFuncs[testIndex].name + ")");
+ setTimeout(testFuncs[testIndex++], 0, win);
+
+ if (testIndex >= testFuncs.length) {
+ // Finish this up!
+ Services.obs.removeObserver(testObs, TEST_DONE);
+ Services.cookies.removeAll();
+ gLocSvc.fhist.removeAllEntries();
+ setTimeout(finish, 0);
+ }
+ }
+ }
+ };
+ waitForExplicitFinish();
+ Services.obs.addObserver(testObs, DATAMAN_LOADED);
+}
+
+var testFuncs = [
+function test_open_state(aWin) {
+ is(aWin.document.documentElement.id, "dataman-page",
+ "The active tab is the Data Manager");
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 6,
+ "The correct number of domains is listed");
+ is(aWin.gTabs.activePanel, "formdataPanel",
+ "Form data panel is selected");
+
+ aWin.document.getElementById("domainSearch").value = "mo";
+ aWin.document.getElementById("domainSearch").doCommand();
+ is(aWin.gDomains.tree.view.selection.count, 0,
+ "In search, non-matching selection is lost");
+ is(aWin.gDomains.tree.view.rowCount, 3,
+ "In search, the correct number of domains is listed");
+ is(aWin.gDomains.displayedDomains.map(function(aDom) { return aDom.title; })
+ .join(","),
+ "mochi.test,mozilla.com,mozilla.org",
+ "In search, the correct domains are listed");
+
+ aWin.gDomains.tree.view.selection.select(0);
+ aWin.document.getElementById("domainSearch").value = "";
+ aWin.document.getElementById("domainSearch").doCommand();
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 6,
+ "After search, the correct number of domains is listed");
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "After search, number of selections is correct");
+ is(aWin.gDomains.selectedDomain.title, "mochi.test",
+ "After search, matching selection is kept correctly");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_forget_ipv6(aWin) {
+ // The main purpose of IPv6 entries is that things load, delete them ASAP.
+ // Better forget panel tests (more checks) are in test_prefs_panel below.
+ aWin.gDomains.tree.view.selection.select(1);
+ is(aWin.gDomains.selectedDomain.title, "[::1]",
+ "IPv6 domain is selected");
+ aWin.document.getElementById("domain-context-forget").click();
+ is(aWin.gTabs.activePanel, "forgetPanel",
+ "Forget panel is selected");
+
+ aWin.document.getElementById("forgetCookies").click();
+ aWin.document.getElementById("forgetPasswords").click();
+ aWin.document.getElementById("forgetButton").click();
+ is(aWin.document.getElementById("forgetTab").hidden, true,
+ "Forget tab is hidden again");
+ is(aWin.document.getElementById("forgetTab").disabled, true,
+ "Forget panel is disabled again");
+
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 5,
+ "The IPv6 domain has been removed from the list");
+ is(aWin.gDomains.tree.view.selection.count, 0,
+ "No domain is selected");
+
+ aWin.gDomains.tree.view.selection.select(0);
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "* domain is selected again");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_fdata_panel(aWin) {
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("formdataTab");
+ is(aWin.gTabs.activePanel, "formdataPanel",
+ "Form data panel is selected again");
+ is(aWin.gFormdata.tree.view.rowCount, 6,
+ "The correct number of form data entries is listed");
+
+ aWin.gFormdata.tree.view.selection.rangedSelect(0, 1, true); // item 0, 3
+ aWin.document.getElementById("fdataSearch").value = "b"; // item 3, 4 match
+ aWin.document.getElementById("fdataSearch").doCommand();
+ is(aWin.gFormdata.tree.view.selection.count, 1,
+ "In search, non-matching part of selection is lost");
+ is(aWin.gFormdata.displayedFormdata[aWin.gFormdata.tree.currentIndex].value, "value3",
+ "In search, matching part selection is kept correctly");
+ is(aWin.gFormdata.tree.view.rowCount, 2,
+ "In search, the correct number of form data entries is listed");
+ is(aWin.gFormdata.displayedFormdata.map(function(aFd) { return aFd.value; })
+ .join(","),
+ "value3,value4",
+ "In search, the correct domains are listed");
+
+ aWin.document.getElementById("fdataSearch").value = "";
+ aWin.document.getElementById("fdataSearch").doCommand();
+ is(aWin.gFormdata.tree.view.rowCount, 6,
+ "After search, the correct number of form data entries is listed");
+ is(aWin.gFormdata.tree.view.selection.count, 1,
+ "After search, number of selections is correct");
+ is(aWin.gFormdata.displayedFormdata[aWin.gFormdata.tree.currentIndex].value, "value3",
+ "After search, matching selection is kept correctly");
+
+ aWin.gFormdata.tree.view.selection.clearSelection();
+ is(aWin.document.getElementById("fdataRemove").disabled, true,
+ "The remove button is disabled");
+ aWin.gFormdata.tree.view.selection.rangedSelect(0, 1, true); // value0, value3
+ aWin.gFormdata.tree.view.selection.rangedSelect(3, 3, true); // value5
+ aWin.gFormdata.tree.view.selection.rangedSelect(5, 5, true); // value2
+ is(aWin.gFormdata.tree.view.selection.count, 4,
+ "The correct number of items is selected");
+ is(aWin.document.getElementById("fdataRemove").disabled, false,
+ "After selecting, the remove button is enabled");
+
+ gLocSvc.fhist.removeEntry("ckey", "value5");
+ is(aWin.gFormdata.tree.view.rowCount, 5,
+ "After remove, the correct number of form data entries is listed");
+ is(aWin.gFormdata.tree.view.selection.count, 3,
+ "After remove, the correct number of items is selected");
+
+ gLocSvc.fhist.addEntry("dkey", "value6");
+ is(aWin.gFormdata.tree.view.rowCount, 6,
+ "After add, the correct number of form data entries is listed");
+ is(aWin.gFormdata.tree.view.selection.count, 3,
+ "After add, the correct number of items is selected");
+
+ aWin.document.getElementById("fdataValueCol").click();
+ is(aWin.gFormdata.tree.view.selection.count, 3,
+ "After sort, the correct number of items is selected");
+ is(aWin.gDataman.getTreeSelections(aWin.gFormdata.tree)
+ .map(function(aSel) { return aWin.gFormdata.displayedFormdata[aSel].value; })
+ .join(","),
+ "value0,value2,value3",
+ "After sort, correct items are selected");
+
+ // Select only one for testing remove button, as catching the prompt is hard.
+ aWin.gFormdata.tree.view.selection.select(5);
+ aWin.document.getElementById("fdataRemove").click();
+ is(aWin.gFormdata.tree.view.rowCount, 5,
+ "After remove button, the correct number of form data entries is listed");
+ is(aWin.gFormdata.tree.view.selection.count, 1,
+ "After remove button, one item is selected again");
+ is(aWin.gFormdata.tree.currentIndex, 4,
+ "After remove button, correct index is selected");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_cookies_panel(aWin) {
+ aWin.gDomains.tree.view.selection.select(8);
+ is(aWin.gDomains.selectedDomain.title, "geckoisgecko.org",
+ "For cookie tests 1, correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Cookies panel is selected");
+ is(aWin.gCookies.tree.view.rowCount, 3,
+ "The correct number of cookies is listed");
+
+ aWin.gCookies.tree.view.selection.select(0);
+ is(aWin.document.getElementById("cookieInfoSendType").value,
+ "Any type of connection",
+ "Correct send type for first cookie");
+ is(aWin.document.getElementById("cookieInfoExpires").value,
+ "At end of session",
+ "Correct expiry label for first cookie");
+
+ aWin.gCookies.tree.view.selection.select(1);
+ is(aWin.document.getElementById("cookieInfoSendType").value,
+ "Any type of connection, no script access",
+ "Correct send type for second cookie");
+
+ aWin.gCookies.tree.view.selection.select(2);
+ is(aWin.document.getElementById("cookieInfoSendType").value,
+ "Encrypted connections only and no script access",
+ "Correct send type for third cookie");
+
+ aWin.gDomains.tree.view.selection.select(4);
+ is(aWin.gDomains.selectedDomain.title, "drumbeat.org",
+ "For cookie tests 2, correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Cookies panel is selected");
+ is(aWin.gCookies.tree.view.rowCount, 1,
+ "The correct number of cookies is listed");
+ aWin.gCookies.updateContext(); // don't actually open it, would be async
+ is(aWin.document.getElementById("cookies-context-selectall").disabled, false,
+ "The select all context menu item is enabled");
+ is(aWin.document.getElementById("cookies-context-remove").disabled, true,
+ "The remove context menu item is disabled");
+
+ aWin.document.getElementById("cookies-context-selectall").click();
+ is(aWin.document.getElementById("cookieInfoSendType").value,
+ "Encrypted connections only",
+ "Correct send type for fourth cookie");
+ isnot(aWin.document.getElementById("cookieInfoExpires").value,
+ "At end of session",
+ "Expiry label for this cookie is not session");
+ aWin.gCookies.updateContext(); // don't actually open it, would be async
+ is(aWin.document.getElementById("cookies-context-selectall").disabled, true,
+ "After selecting, the select all context menu item is disabled");
+ is(aWin.document.getElementById("cookies-context-remove").disabled, false,
+ "After selecting, the remove context menu item is enabled");
+
+ aWin.document.getElementById("cookies-context-remove").click();
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 4,
+ "The domain has been removed from the list");
+ is(aWin.gTabs.activePanel, null,
+ "No panel is active");
+ is(aWin.gTabs.tabbox.selectedTab.disabled, true,
+ "The selected panel is disabled");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_permissions_panel(aWin) {
+ aWin.gDomains.tree.view.selection.select(8);
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "For permissions tests, correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Permissions panel is selected");
+ Services.perms.add(Services.io.newURI("http://cookie.getpersonas.com/"),
+ "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
+ Services.perms.add(Services.io.newURI("http://cookie2.getpersonas.com/"),
+ "cookie", Services.perms.DENY_ACTION);
+ Services.perms.add(Services.io.newURI("http://geo.getpersonas.com/"),
+ "geo", Services.perms.ALLOW_ACTION);
+ Services.perms.add(Services.io.newURI("http://image.getpersonas.com/"),
+ "image", Services.perms.DENY_ACTION);
+ Services.perms.add(Services.io.newURI("http://indexedDB.getpersonas.com/"),
+ "indexedDB", Services.perms.ALLOW_ACTION);
+ Services.perms.add(Services.io.newURI("http://install.getpersonas.com/"),
+ "install", Services.perms.ALLOW_ACTION);
+ Services.perms.add(Services.io.newURI("http://offline.getpersonas.com/"),
+ "offline-app", Services.perms.ALLOW_ACTION);
+ Services.perms.add(Services.io.newURI("http://popup.getpersonas.com/"),
+ "popup", Services.perms.ALLOW_ACTION);
+ Services.perms.add(Services.io.newURI("http://test.getpersonas.com/"),
+ "test", Services.perms.DENY_ACTION);
+ Services.perms.add(Services.io.newURI("http://xul.getpersonas.com/"),
+ "allowXULXBL", Services.perms.ALLOW_ACTION);
+ Services.logins.setLoginSavingEnabled("password.getpersonas.com", false);
+ is(aWin.gPerms.list.children.length, 12,
+ "The correct number of permissions is displayed in the list");
+ for (let i = 1; i < aWin.gPerms.list.children.length; i++) {
+ let perm = aWin.gPerms.list.children[i];
+ switch (perm.type) {
+ case "allowXULXBL":
+ is(perm.getAttribute("label"), "Use XUL/XBL Markup",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 2,
+ "Set back to correct default");
+ break;
+ case "cookie":
+ is(perm.getAttribute("label"), "Set Cookies",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, perm.host == "cookie.getpersonas.com" ? 8 : 2,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 1,
+ "Set back to correct default");
+ break;
+ case "geo":
+ is(perm.getAttribute("label"), "Share Location",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 2,
+ "Set back to correct default");
+ break;
+ case "image":
+ is(perm.getAttribute("label"), "Load Images",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 2,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 1,
+ "Set back to correct default");
+ break;
+ case "indexedDB":
+ is(perm.getAttribute("label"), "Store Local Databases",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 2,
+ "Set back to correct default");
+ break;
+ case "install":
+ is(perm.getAttribute("label"), "Install Add-ons",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 2,
+ "Set back to correct default");
+ break;
+ case "offline-app":
+ is(perm.getAttribute("label"), "Offline Web Applications",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 1,
+ "Set back to correct default");
+ break;
+ case "password":
+ is(perm.getAttribute("label"), "Save Passwords",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 2,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 1,
+ "Set back to correct default");
+ break;
+ case "popup":
+ is(perm.getAttribute("label"), "Open Popup Windows",
+ "Correct label for type: " + perm.type);
+ is(perm.capability, 1,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 1,
+ "Set back to correct default");
+ break;
+ is(perm.getAttribute("label"), perm.type,
+ "Correct default label for type: " + perm.type);
+ is(perm.capability, 2,
+ "Correct capability for: " + perm.host);
+ perm.useDefault(true);
+ is(perm.capability, 0,
+ "Set to correct default");
+ break;
+ }
+ }
+
+ aWin.gDomains.tree.view.selection.select(0); // Switch to * domain.
+ aWin.gDomains.tree.view.selection.select(8); // Switch back to rebuild the perm list.
+ is(aWin.gPerms.list.children.length, 1,
+ "After the test, the correct number of permissions is displayed in the list");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_permissions_add(aWin) {
+ aWin.gDomains.tree.view.selection.select(0);
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "For add permissions tests, * domain is selected again");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Permissions panel is selected");
+ is(aWin.gPerms.list.disabled, true,
+ "The permissions list is disabled");
+ is(aWin.gPerms.addButton.disabled, false,
+ "The add permissions button is enabled");
+ aWin.gPerms.addButton.click();
+ is(aWin.gPerms.addSelBox.hidden, false,
+ "The addition box is shown");
+ is(aWin.gPerms.addHost.value, "",
+ "The host is empty");
+ is(aWin.gPerms.addType.value, "",
+ "No type is selected");
+ is(aWin.gPerms.addButton.disabled, true,
+ "The add permissions button is disabled");
+ aWin.gPerms.addHost.value = "foo";
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("formdataTab");
+ is(aWin.gTabs.activePanel, "formdataPanel",
+ "Successfully switched to form data panel");
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("permissionsTab");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Successfully switched back to permissions panel");
+ is(aWin.gPerms.addButton.disabled, false,
+ "The add permissions button is enabled again");
+ is(aWin.gPerms.addSelBox.hidden, true,
+ "The addition box is hidden");
+ aWin.gPerms.addButton.click();
+ is(aWin.gPerms.addHost.value, "",
+ "The host is empty again");
+ is(aWin.gPerms.addType.value, "",
+ "No type is selected still");
+ aWin.gPerms.addHost.value = "data.permfoobar.com";
+ aWin.gPerms.addType.value = "cookie";
+ aWin.gPerms.addType.click();
+ is(aWin.gPerms.addButton.disabled, false,
+ "With host and type set, the add permissions button is enabled");
+ aWin.gPerms.addButton.click();
+ is(aWin.gPerms.list.disabled, false,
+ "After adding, the permissions list is enabled");
+ is(aWin.gPerms.list.children.length, 1,
+ "A permission is displayed in the list");
+ let perm = aWin.gPerms.list.children[0];
+ is(perm.type, "cookie",
+ "Added permission has correct type");
+ is(perm.host, "data.permfoobar.com",
+ "Added permission has correct host");
+ is(perm.capability, 1,
+ "Added permission has correct value (default)");
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("formdataTab");
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("permissionsTab");
+ is(aWin.gPerms.list.disabled, true,
+ "After switching between panels, the permissions list is disabled again");
+ aWin.gDomains.tree.view.selection.select(8);
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Switched to correct domain for another add test");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Permissions panel is selected");
+ aWin.gPerms.addButton.click();
+ is(aWin.gPerms.addHost.value, "getpersonas.com",
+ "On add, the host is set correctly");
+ is(aWin.gPerms.addType.value, "",
+ "Again, no type is selected");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_prefs_panel(aWin) {
+ Services.contentPrefs2.setPref("my.drumbeat.org", "data_manager.test", "foo", null);
+ Services.contentPrefs2.setPref("drumbeat.org", "data_manager.test", "bar", null);
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 5,
+ "The domain for prefs tests has been added from the list");
+ aWin.gDomains.tree.view.selection.select(4);
+ is(aWin.gDomains.selectedDomain.title, "drumbeat.org",
+ "For prefs tests, correct domain is selected");
+ is(aWin.gTabs.activePanel, "preferencesPanel",
+ "Preferences panel is selected");
+ is(aWin.gPrefs.tree.view.rowCount, 2,
+ "The correct number of prefs is listed");
+
+ aWin.gDomains.updateContext(); // don't actually open it, would be async
+ is(aWin.document.getElementById("domain-context-forget").disabled, false,
+ "The domain's forget context menu item is enabled");
+
+ aWin.document.getElementById("domain-context-forget").click();
+ is(aWin.gTabs.activePanel, "forgetPanel",
+ "Forget panel is selected");
+ is(aWin.document.getElementById("forgetTab").disabled, false,
+ "Forget panel is enabled");
+ is(aWin.document.getElementById("forgetTab").hidden, false,
+ "Forget panel is unhidden");
+
+ aWin.gDomains.tree.view.selection.select(3);
+ isnot(aWin.gDomains.selectedDomain.title, "drumbeat.org",
+ "Switching away goes to a different domain: " + aWin.gDomains.selectedDomain.title);
+ isnot(aWin.gTabs.activePanel, "forgetPanel",
+ "Forget panel is not selected any more: " + aWin.gTabs.activePanel);
+ is(aWin.document.getElementById("forgetTab").disabled, true,
+ "Forget panel is disabled");
+ is(aWin.document.getElementById("forgetTab").hidden, true,
+ "Forget panel is disabled");
+
+ aWin.gDomains.tree.view.selection.select(4);
+ is(aWin.gDomains.selectedDomain.title, "drumbeat.org",
+ "Correct domain is selected again");
+ aWin.document.getElementById("domain-context-forget").click();
+ is(aWin.gTabs.activePanel, "forgetPanel",
+ "Forget panel is selected again");
+ is(aWin.document.getElementById("forgetTab").disabled, false,
+ "Forget panel is enabled again");
+ is(aWin.document.getElementById("forgetTab").hidden, false,
+ "Forget panel is unhidden again");
+
+ is(aWin.document.getElementById("forgetPreferences").disabled, false,
+ "Forget preferences checkbox is enabled");
+ is(aWin.document.getElementById("forgetButton").disabled, true,
+ "Forget button is disabled");
+ aWin.document.getElementById("forgetPreferences").click();
+ is(aWin.document.getElementById("forgetPreferences").checked, true,
+ "Forget preferences checkbox is checked");
+ is(aWin.document.getElementById("forgetButton").disabled, false,
+ "Forget button is enabled");
+
+ aWin.document.getElementById("forgetButton").click();
+ is(aWin.document.getElementById("forgetButton").hidden, true,
+ "Forget button is hidden");
+ is(aWin.document.getElementById("forgetPreferences").hidden, true,
+ "Forget preferences checkbox is hidden");
+ is(aWin.document.getElementById("forgetPreferencesLabel").hidden, false,
+ "Forget preferences label is shown");
+ is(aWin.document.getElementById("forgetTab").hidden, true,
+ "Forget tab is hidden again");
+ is(aWin.document.getElementById("forgetTab").disabled, true,
+ "Forget panel is disabled again");
+
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 4,
+ "The domain for prefs tests has been removed from the list");
+ is(aWin.gDomains.tree.view.selection.count, 0,
+ "No domain is selected");
+
+ aWin.gDomains.updateContext(); // don't actually open it, would be async
+ is(aWin.document.getElementById("domain-context-forget").disabled, true,
+ "The domain's forget context menu item is disabled");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_passwords_panel(aWin) {
+ aWin.gDomains.tree.view.selection.select(7);
+ is(aWin.gDomains.selectedDomain.title, "geckoisgecko.org",
+ "For passwords tests, correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Cookies panel is selected");
+
+ aWin.gDomains.updateContext(); // don't actually open it, would be async
+ is(aWin.document.getElementById("domain-context-forget").disabled, false,
+ "The domain's forget context menu item is enabled");
+
+ aWin.document.getElementById("domain-context-forget").click();
+ is(aWin.gTabs.activePanel, "forgetPanel",
+ "Forget panel is selected");
+ is(aWin.document.getElementById("forgetTab").disabled, false,
+ "Forget panel is enabled");
+ is(aWin.document.getElementById("forgetTab").hidden, false,
+ "Forget panel is unhidden");
+ is(aWin.document.getElementById("forgetPreferences").hidden, false,
+ "Forget preferences checkbox is shown");
+ is(aWin.document.getElementById("forgetPreferences").disabled, true,
+ "Forget preferences checkbox is disabled");
+ is(aWin.document.getElementById("forgetPreferencesLabel").hidden, true,
+ "Forget preferences label is hidden");
+ is(aWin.document.getElementById("forgetCookies").hidden, false,
+ "Forget cookies checkbox is shown");
+ is(aWin.document.getElementById("forgetCookies").disabled, false,
+ "Forget cookies checkbox is enabled");
+ is(aWin.document.getElementById("forgetCookiesLabel").hidden, true,
+ "Forget cookies label is hidden");
+ is(aWin.document.getElementById("forgetPasswords").hidden, false,
+ "Forget passwords checkbox is shown");
+ is(aWin.document.getElementById("forgetPasswords").disabled, false,
+ "Forget passwords checkbox is enabled");
+ is(aWin.document.getElementById("forgetPasswordsLabel").hidden, true,
+ "Forget passwords label is hidden");
+ is(aWin.document.getElementById("forgetButton").hidden, false,
+ "Forget button is shown");
+ is(aWin.document.getElementById("forgetButton").disabled, true,
+ "Forget button is disabled");
+
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("passwordsTab");
+ is(aWin.gTabs.activePanel, "passwordsPanel",
+ "Passwords panel is selected");
+ is(aWin.gPasswords.tree.view.rowCount, 2,
+ "The correct number of passwords is listed");
+ is(aWin.document.getElementById("pwdRemove").disabled, true,
+ "The remove button is disabled");
+
+ aWin.gPasswords.tree.view.selection.select(0);
+ is(aWin.document.getElementById("pwdRemove").disabled, false,
+ "After selecting, the remove button is enabled");
+
+ aWin.document.getElementById("pwdRemove").click();
+ is(aWin.gPasswords.tree.view.rowCount, 1,
+ "After deleting, the correct number of passwords is listed");
+ is(aWin.gPasswords.tree.view.selection.count, 1,
+ "After deleting, one password is selected again");
+ is(aWin.gPasswords.tree.currentIndex, 0,
+ "After deleting, correct index is selected");
+ is(aWin.document.getElementById("pwdRemove").disabled, false,
+ "After deleting, the remove button is still enabled");
+
+ aWin.gPasswords.tree.view.selection.select(0);
+ aWin.document.getElementById("pwdRemove").click();
+ is(aWin.document.getElementById("pwdRemove").disabled, true,
+ "After deleting last password, the remove button is disabled");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "After deleting last password, cookies panel is selected again");
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_idn(aWin) {
+ // Use a domain with an existing permission.
+ let testDomain = "xn--hxajbheg2az3al.xn--jxalpdlp";
+ let idnDomain = gLocSvc.idn.convertToDisplayIDN(testDomain, {});
+ isnot(testDomain, idnDomain, "Using a valid IDN domain");
+ // Add IDN cookie.
+ Services.cookies.add(testDomain, "", "name0", "value0", false, false, true,
+ parseInt(Date.now() / 1000) + 600, {});
+
+ aWin.document.getElementById("domainSearch").value = "xn--";
+ aWin.document.getElementById("domainSearch").doCommand();
+ is(aWin.gDomains.tree.view.rowCount, 1,
+ "Search for 'xn--' returns one result");
+ is(aWin.gDomains.displayedDomains.map(function(aDom) { return aDom.title; })
+ .join(","),
+ "xn--exaple-kqf.test", "In xn-- search, only the non-decodable domain is listed");
+ aWin.document.getElementById("domainSearch").value = idnDomain.charAt(3);
+ aWin.document.getElementById("domainSearch").doCommand();
+ is(aWin.gDomains.tree.view.rowCount, 1,
+ "IDN search returns a result");
+ is(aWin.gDomains.displayedDomains.map(function(aDom) { return aDom.title; })
+ .join(","),
+ testDomain, "In IDN search, the correct domain is listed");
+ aWin.document.getElementById("domainSearch").value = "";
+ aWin.document.getElementById("domainSearch").doCommand();
+
+ aWin.gDomains.tree.view.selection.select(kPreexistingDomains + 3);
+ is(aWin.gDomains.selectedDomain.title, testDomain,
+ "For IDN tests, correct domain is selected");
+ is(aWin.gDomains.selectedDomain.displayTitle, idnDomain,
+ "The display title of that domain is correct");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Cookies panel is selected");
+ is(aWin.gCookies.tree.view.getCellText(0, aWin.gCookies.tree.columns["cookieHostCol"]),
+ idnDomain,
+ "Correct domain displayed for IDN cookie");
+ aWin.gCookies.tree.view.selection.select(0);
+ aWin.document.getElementById("cookieRemove").click();
+
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "After deleting, correctly switched to permissions panel");
+ let perm = aWin.gPerms.list.children[0];
+ is(perm.host, "bug413909." + testDomain,
+ "Permission has correct host");
+ is(perm.getAttribute("displayHost"), "bug413909." + idnDomain,
+ "Permission has correct display host");
+
+ // Add pref with decoded IDN name.
+ Services.contentPrefs2.setPref(testDomain, "data_manager.test", "foo", null);
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("preferencesTab");
+ is(aWin.gTabs.activePanel, "preferencesPanel",
+ "Successfully switched to preferences panel for IDN tests");
+ // Add pref with encoded IDN name while panel is shown (different code path).
+ Services.contentPrefs2.setPref(idnDomain, "data_manager.test2", "bar", null);
+ is(aWin.gPrefs.tree.view.getCellText(0, aWin.gPrefs.tree.columns["prefsHostCol"]),
+ idnDomain,
+ "Correct domain displayed for punycode IDN preference");
+ is(aWin.gPrefs.tree.view.getCellText(1, aWin.gPrefs.tree.columns["prefsHostCol"]),
+ idnDomain,
+ "Correct domain displayed for utf8 IDN preference");
+ aWin.gPrefs.tree.view.selection.select(0);
+ aWin.document.getElementById("prefsRemove").click();
+ aWin.document.getElementById("prefsRemove").click();
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "After deleting, correctly switched back to permissions panel");
+
+ // Add IDN password (usually have encoded names)
+ let loginInfo1 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ loginInfo1.init("http://" + idnDomain, "http://" + idnDomain, null,
+ "dataman", "mysecret", "user", "pwd");
+ Services.logins.addLogin(loginInfo1);
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("passwordsTab");
+ is(aWin.gTabs.activePanel, "passwordsPanel",
+ "Successfully switched to passwords panel for IDN tests");
+ is(aWin.gPasswords.tree.view.getCellText(0, aWin.gPasswords.tree.columns["pwdHostCol"]),
+ "http://" + idnDomain,
+ "Correct domain displayed for IDN password");
+ aWin.gPasswords.tree.view.selection.select(0);
+ aWin.document.getElementById("pwdRemove").click();
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "After deleting, correctly switched back to permissions panel");
+
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_storage_load(aWin) {
+ // Load the page that fills in several web storage entries.
+ Services.perms.add(Services.io.newURI("http://mochi.test:8888/"),
+ "offline-app", Services.perms.ALLOW_ACTION);
+
+ // Get the http address from the current chrome test path
+ let rootDir = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://mochi.test:8888/");
+ let testURL = rootDir + "dataman_storage.html";
+ let storagetab = gBrowser.addTab(testURL);
+ let stWin = storagetab.linkedBrowser.contentWindow.wrappedJSObject;
+ let dmStorageListener = {
+ handleEvent: function dmStorageHandler(aEvent) {
+ let tab = aEvent.target;
+ if (tab == storagetab) {
+ gBrowser.tabContainer.removeEventListener("TabClose", this);
+ // Force DOM Storage to write its data to the disk.
+ Services.obs.notifyObservers(null, "domstorage-flush-timer");
+ Services.perms.remove("mochi.test", "offline-app");
+ Services.obs.notifyObservers(window, TEST_DONE);
+ }
+ },
+ };
+ gBrowser.tabContainer.addEventListener("TabClose", dmStorageListener);
+},
+
+function test_storage_wait(aWin) {
+ // Wait to make sure that DOM Storage flushing has actually worked.
+ setTimeout(function foo() {
+ Services.obs.notifyObservers(window, TEST_DONE); }, 1000);
+},
+
+function test_storage(aWin) {
+ aWin.gStorage.reloadList();
+ info("appcache groups: " + aWin.gLocSvc.appcache.getGroups().length);
+ aWin.gDomains.tree.view.selection.select(8);
+ is(aWin.gDomains.selectedDomain.title, "mochi.test",
+ "For storage tests, correct domain is selected");
+ is(aWin.document.getElementById("storageTab").disabled, false,
+ "Storage panel is enabled");
+ aWin.gTabs.tabbox.selectedTab = aWin.document.getElementById("storageTab");
+ is(aWin.gTabs.activePanel, "storagePanel",
+ "Storage panel is selected");
+ is(aWin.gStorage.tree.view.rowCount, 3,
+ "The correct number of storages is listed");
+ is(aWin.gStorage.displayedStorages
+ .map(function(aStorage) { return aStorage.type; })
+ .sort().join(","),
+ "appCache,indexedDB,localStorage",
+ "The correct types of storage are listed");
+
+ for (let i = aWin.gStorage.tree.view.rowCount - 1; i >= 0; i--) {
+ let remType = aWin.gStorage.displayedStorages[0].type;
+ info("Removing " + remType);
+ aWin.gStorage.tree.view.selection.select(0);
+ aWin.document.getElementById("storageRemove").click();
+ is(aWin.gStorage.tree.view.rowCount, i,
+ remType + " entry removed");
+ }
+
+ isnot(aWin.gTabs.activePanel, "storagePanel",
+ "Storage panel is not selected any more");
+
+ Services.obs.notifyObservers(window, TEST_DONE);
+},
+
+function test_close(aWin) {
+ function dmWindowClosedListener() {
+ aWin.removeEventListener("unload", dmWindowClosedListener);
+ isnot(content.document.documentElement.id, "dataman-page",
+ "The active tab is not the Data Manager");
+ Services.obs.notifyObservers(window, TEST_DONE);
+ }
+ aWin.addEventListener("unload", dmWindowClosedListener);
+ aWin.close();
+}
+];
diff --git a/comm/suite/components/dataman/tests/browser_dataman_callviews.js b/comm/suite/components/dataman/tests/browser_dataman_callviews.js
new file mode 100644
index 0000000000..e7ace5ee5a
--- /dev/null
+++ b/comm/suite/components/dataman/tests/browser_dataman_callviews.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test loading views in data manager.
+
+const DATAMAN_LOADED = "dataman-loaded";
+
+// See browser_dataman_basics.js.
+const kPreexistingDomains = 12;
+
+var testIndex = 0;
+
+function test() {
+ // Add cookies.
+ Services.cookies.add("getpersonas.com", "", "name0", "value0", false, false,
+ true, parseInt(Date.now() / 1000) + 600, {});
+ Services.cookies.add("drumbeat.org", "", "name1", "value1", false, false,
+ true, parseInt(Date.now() / 1000) + 600, {});
+
+ //Services.prefs.setBoolPref("data_manager.debug", true);
+
+ var win;
+
+ gBrowser.addTab();
+ toDataManager("example.org");
+
+ let testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == DATAMAN_LOADED) {
+ // Run next test
+ info("run test #" + (testIndex + 1) + " of " + testFuncs.length +
+ " (" + testFuncs[testIndex].name + ")");
+
+ ok(true, "Step " + (testIndex + 1) + ": Data Manager is loaded");
+ win = content.wrappedJSObject;
+
+ testFuncs[testIndex++](win);
+
+ if (testIndex >= testFuncs.length) {
+ // Finish this up!
+ Services.obs.removeObserver(testObs, DATAMAN_LOADED);
+ Services.cookies.remove("getpersonas.com", "name0", "value0", false);
+ Services.cookies.remove("drumbeat.org", "name1", "value1", false);
+ finish();
+ }
+ }
+ }
+ };
+ waitForExplicitFinish();
+ Services.obs.addObserver(testObs, DATAMAN_LOADED);
+}
+
+var testFuncs = [
+function test_load_basic(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "example.org",
+ "Step " + testIndex + ": The correct domain is selected");
+ toDataManager("getpersonas.com|cookies");
+},
+
+function test_switch_panel(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Step " + testIndex + ": Cookies panel is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("www.getpersonas.com:443|permissions");
+},
+
+function test_load_with_panel(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("getpersonas.com|preferences");
+},
+
+function test_load_disabled_panel(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Step " + testIndex + ": Cookies panel is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("getpersonas.com|unknown");
+},
+
+function test_load_inexistent_panel(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Step " + testIndex + ": Cookies panel is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("unknowndomainexample.com");
+},
+
+function test_load_unknown_domain(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("|cookies");
+},
+
+function test_load_datatype(aWin) {
+ is(aWin.gDomains.selectfield.value, "Cookies",
+ "Step " + testIndex + ": The correct menulist item is selected");
+ is(aWin.gDomains.tree.view.rowCount, 2,
+ "Step " + testIndex + ": The correct number of domains is listed");
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "drumbeat.org",
+ "Step " + testIndex + ": The selected domain is correct");
+ is(aWin.gTabs.activePanel, "cookiesPanel",
+ "Step " + testIndex + ": Cookies panel is selected");
+ aWin.gDomains.tree.view.selection.select(1);
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The second domain is correct as well");
+ toDataManager("|permissions");
+},
+
+function test_switch_datatype(aWin) {
+ is(aWin.gDomains.selectfield.value, "Permissions",
+ "Step " + testIndex + ": The correct menulist item is selected");
+ is(aWin.gDomains.tree.view.rowCount, kPreexistingDomains + 3,
+ "Step " + testIndex + ": The correct number of domains is listed");
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "Step " + testIndex + ": The selected domain is correct");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ toDataManager("www.getpersonas.com");
+},
+
+function test_escape_datatype(aWin) {
+ is(aWin.gDomains.selectfield.value, "all",
+ "Step " + testIndex + ": The correct menulist item is selected");
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ aWin.close();
+ gBrowser.addTab();
+ toDataManager("sub.getpersonas.com:8888|permissions|add|popup");
+},
+
+function test_load_add_perm_existdomain(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "getpersonas.com",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ is(aWin.gPerms.addSelBox.hidden, false,
+ "Step " + testIndex + ": The addition box is shown");
+ is(aWin.gPerms.addHost.value, "sub.getpersonas.com:8888",
+ "Step " + testIndex + ": The correct host and port has been entered");
+ is(aWin.gPerms.addType.value, "popup",
+ "Step " + testIndex + ": The correct permission type has been selected");
+ toDataManager("foo.geckoisgecko.org|permissions|add|image");
+},
+
+function test_switch_add_perm_newdomain(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ is(aWin.gPerms.addSelBox.hidden, false,
+ "Step " + testIndex + ": The addition box is shown");
+ is(aWin.gPerms.addHost.value, "foo.geckoisgecko.org",
+ "Step " + testIndex + ": The correct host has been entered");
+ is(aWin.gPerms.addType.value, "image",
+ "Step " + testIndex + ": The correct permission type has been selected");
+ toDataManager("drumbeat.org|permissions|add|cookie");
+},
+
+function test_switch_add_perm_nopermdomain(aWin) {
+ is(aWin.gDomains.tree.view.selection.count, 1,
+ "Step " + testIndex + ": One domain is selected");
+ is(aWin.gDomains.selectedDomain.title, "*",
+ "Step " + testIndex + ": The correct domain is selected");
+ is(aWin.gTabs.activePanel, "permissionsPanel",
+ "Step " + testIndex + ": Permissions panel is selected");
+ is(aWin.gPerms.addSelBox.hidden, false,
+ "Step " + testIndex + ": The addition box is shown");
+ is(aWin.gPerms.addHost.value, "drumbeat.org",
+ "Step " + testIndex + ": The correct host has been entered");
+ is(aWin.gPerms.addType.value, "cookie",
+ "Step " + testIndex + ": The correct permission type has been selected");
+ aWin.close();
+}
+];
diff --git a/comm/suite/components/dataman/tests/dataman_storage.appcache b/comm/suite/components/dataman/tests/dataman_storage.appcache
new file mode 100644
index 0000000000..bc05ca8016
--- /dev/null
+++ b/comm/suite/components/dataman/tests/dataman_storage.appcache
@@ -0,0 +1,5 @@
+CACHE MANIFEST
+# allow caching of the test itself.
+CACHE:
+browser_dataman_basics.js
+dataman_storage.html
diff --git a/comm/suite/components/dataman/tests/dataman_storage.appcache^headers^ b/comm/suite/components/dataman/tests/dataman_storage.appcache^headers^
new file mode 100644
index 0000000000..5efde3c5b0
--- /dev/null
+++ b/comm/suite/components/dataman/tests/dataman_storage.appcache^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+
diff --git a/comm/suite/components/dataman/tests/dataman_storage.html b/comm/suite/components/dataman/tests/dataman_storage.html
new file mode 100644
index 0000000000..38ea3c3c38
--- /dev/null
+++ b/comm/suite/components/dataman/tests/dataman_storage.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html manifest="dataman_storage.appcache">
+<head>
+<script>
+var seenEvents = 0;
+function eventSeen(e){
+ seenEvents++;
+ document.getElementById("eventCnt").textContent = seenEvents;
+ if (seenEvents > 1)
+ setTimeout(close, 1000);
+}
+</script>
+</head>
+<body>
+<h1>Test</h1>
+<p id="eventCnt">*</p>
+<script>
+document.body.addEventListener('storage', eventSeen);
+document.addEventListener('idb-done', eventSeen);
+
+localStorage.setItem("localtest", "foo"); // issues no storage event (!?)
+globalStorage['mochi.test'].setItem("globaltest", "bar"); // issues a storage event
+
+var request = mozIndexedDB.open("test", "test-decription");
+request.onsuccess = function(e) {
+ var db = e.target.result;
+ var setVrequest = db.setVersion("1.0");
+ setVrequest.onsuccess = function(e) {
+ var store = db.createObjectStore("test", {keyPath: "foo"});
+ db.transaction(["test"], IDBTransaction.READ_WRITE, 0)
+ .objectStore("test").put({"foo": "bar"});
+ // create, define and dispatch the test-done event
+ var event = document.createEvent('Event');
+ event.initEvent('idb-done', true, true);
+ document.dispatchEvent(event);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/comm/suite/components/downloads/DownloadsCommon.jsm b/comm/suite/components/downloads/DownloadsCommon.jsm
new file mode 100644
index 0000000000..81fdcccfa3
--- /dev/null
+++ b/comm/suite/components/downloads/DownloadsCommon.jsm
@@ -0,0 +1,800 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "DownloadsCommon",
+];
+
+/**
+ * Handles the Downloads panel shared methods and data access.
+ *
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadsCommon
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ *
+ * DownloadsData
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing
+ * to build a consistent view of the available data.
+ */
+
+// Globals
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } =
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ DownloadHistory: "resource://gre/modules/DownloadHistory.jsm",
+ Downloads: "resource://gre/modules/Downloads.jsm",
+ DownloadUIHelper: "resource://gre/modules/DownloadUIHelper.jsm",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
+ let { ConsoleAPI } =
+ ChromeUtils.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevelPref: "browser.download.loglevel",
+ prefix: "Downloads"
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+const kDownloadsStringBundleUrl =
+ "chrome://communicator/locale/downloads/downloadmanager.properties";
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringFormatting = {
+ fileExecutableSecurityWarning: true
+};
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringPluralForm = {
+ otherDownloads3: true
+};
+
+const kPartialDownloadSuffix = ".part";
+
+const kPrefBranch = Services.prefs.getBranch("browser.download.");
+
+const PREF_DM_BEHAVIOR = "browser.download.manager.behavior";
+const PROGRESS_DIALOG_URL = "chrome://communicator/content/downloads/progressDialog.xul";
+
+var PrefObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+ getPref(name) {
+ try {
+ switch (typeof this.prefs[name]) {
+ case "boolean":
+ return kPrefBranch.getBoolPref(name);
+ }
+ } catch (ex) { }
+ return this.prefs[name];
+ },
+ observe(aSubject, aTopic, aData) {
+ if (this.prefs.hasOwnProperty(aData)) {
+ delete this[aData];
+ this[aData] = this.getPref(aData);
+ }
+ },
+ register(prefs) {
+ this.prefs = prefs;
+ kPrefBranch.addObserver("", this, true);
+ for (let key in prefs) {
+ let name = key;
+ XPCOMUtils.defineLazyGetter(this, name, function() {
+ return PrefObserver.getPref(name);
+ });
+ }
+ },
+};
+
+// PrefObserver.register({
+ // prefName: defaultValue
+// });
+
+
+// DownloadsCommon
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ */
+var DownloadsCommon = {
+ // The following legacy constants are still returned by stateOfDownload, but
+ // individual properties of the Download object should normally be used.
+ DOWNLOAD_NOTSTARTED: -1,
+ DOWNLOAD_DOWNLOADING: 0,
+ DOWNLOAD_FINISHED: 1,
+ DOWNLOAD_FAILED: 2,
+ DOWNLOAD_CANCELED: 3,
+ DOWNLOAD_PAUSED: 4,
+ DOWNLOAD_BLOCKED_PARENTAL: 6,
+ DOWNLOAD_DIRTY: 8,
+ DOWNLOAD_BLOCKED_POLICY: 9,
+
+ // The following are the possible values of the "attention" property.
+ ATTENTION_NONE: "",
+ ATTENTION_SUCCESS: "success",
+ ATTENTION_WARNING: "warning",
+ ATTENTION_SEVERE: "severe",
+
+ /**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+ get strings() {
+ let strings = {};
+ let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kDownloadsStringsRequiringFormatting) {
+ strings[stringName] = function() {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else if (stringName in kDownloadsStringsRequiringPluralForm) {
+ strings[stringName] = function(aCount) {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ let formattedString = sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ return PluralForm.get(aCount, formattedString);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ delete this.strings;
+ return this.strings = strings;
+ },
+
+ /**
+ * Get access to one of the DownloadsData or HistoryDownloadsData objects
+ * depending on whether history downloads should be included.
+ *
+ * @param window
+ * The browser window which owns the download button.
+ * @param [optional] history
+ * True to include history downloads when the window is public.
+ */
+ // does not apply in SM
+ getData(window, history = false) {
+ if (history) {
+ return HistoryDownloadsData;
+ }
+ return DownloadsData;
+ },
+
+ /**
+ * Initializes the Downloads Manager common code.
+ */
+ init() {
+ const { DownloadsData } =
+ ChromeUtils.import("resource://gre/modules/Downloads.jsm");
+ const { DownloadIntegration } =
+ ChromeUtils.import("resource://gre/modules/DownloadIntegration.jsm");
+ DownloadIntegration.shouldPersistDownload = function() { return true; };
+ DownloadsData.initializeDataLink();
+ },
+
+ /**
+ * Returns the legacy state integer value for the provided Download object.
+ */
+ stateOfDownload(download) {
+ // Collapse state using the correct priority.
+ if (!download.stopped) {
+ return DownloadsCommon.DOWNLOAD_DOWNLOADING;
+ }
+ if (download.succeeded) {
+ return DownloadsCommon.DOWNLOAD_FINISHED;
+ }
+ if (download.error) {
+ if (download.error.becauseBlockedByParentalControls) {
+ return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
+ }
+ if (download.error.becauseBlockedByReputationCheck) {
+ return DownloadsCommon.DOWNLOAD_DIRTY;
+ }
+ return DownloadsCommon.DOWNLOAD_FAILED;
+ }
+ if (download.canceled) {
+ if (download.hasPartialData) {
+ return DownloadsCommon.DOWNLOAD_PAUSED;
+ }
+ return DownloadsCommon.DOWNLOAD_CANCELED;
+ }
+ return DownloadsCommon.DOWNLOAD_NOTSTARTED;
+ },
+
+ /**
+ * Returns the state as a string for the provided Download object.
+ */
+ stateOfDownloadText(download) {
+ // Don't duplicate the logic so just call stateOfDownload.
+ let state = this.stateOfDownload(download);
+ let s = DownloadsCommon.strings;
+ let title = s.unblockHeaderUnblock;
+ let verboseState;
+
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ verboseState = s.statePaused;
+ break;
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ verboseState = s.stateDownloading;
+ break;
+ case DownloadsCommon.DOWNLOAD_FINISHED:
+ verboseState = s.stateCompleted;
+ break;
+ case DownloadsCommon.DOWNLOAD_FAILED:
+ verboseState = s.stateFailed;
+ break;
+ case DownloadsCommon.DOWNLOAD_CANCELED:
+ verboseState = s.stateCanceled;
+ break;
+ // Security Zone Policy
+ case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
+ // Security Zone Policy
+ verboseState = s.stateBlockedParentalControls;
+ break;
+ // Security Zone Policy
+ case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY:
+ verboseState = s.stateBlockedPolicy;
+ break;
+ // possible virus/spyware
+ case DownloadsCommon.DOWNLOAD_DIRTY:
+ verboseState = s.stateDirty;
+ break;
+ // Currently not returned.
+ case DownloadsCommon.DOWNLOAD_UPLOADING:
+ verboseState = s.stateNotStarted;
+ break;
+ case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+ verboseState = s.stateNotStarted;
+ break;
+ // Whoops!
+ default:
+ verboseState = s.stateUnknown;
+ break;
+ }
+
+ return verboseState;
+ },
+
+ /**
+ * Returns the transfer progress text for the provided Download object.
+ */
+ getTransferredBytes(download) {
+ let currentBytes;
+ let totalBytes;
+ // Download in progress.
+ // Download paused / canceled and has partial data.
+ if (!download.stopped ||
+ (download.canceled && download.hasPartialData)) {
+ currentBytes = download.currentBytes,
+ totalBytes = download.hasProgress ? download.totalBytes : -1;
+ // Download done but file missing.
+ } else if (download.succeeded && !download.exists) {
+ currentBytes = download.totalBytes ? download.totalBytes : -1;
+ totalBytes = -1;
+ // For completed downloads, show the file size
+ } else if (download.succeeded && download.target.size !== undefined) {
+ currentBytes = download.target.size;
+ totalBytes = -1;
+ // Some local files saves e.g. from attachments also have no size.
+ // They only have a target in downloads.json but no target.path.
+ // FIX ME later.
+ } else {
+ currentBytes = -1;
+ totalBytes = -1;
+ }
+
+ // We do not want to show 0 of xxx bytes.
+ if (currentBytes == 0) {
+ currentBytes = -1;
+ }
+
+ if (totalBytes == 0) {
+ totalBytes = -1;
+ }
+
+ // We tried everything.
+ if (currentBytes == -1 && totalBytes == -1) {
+ return "";
+ }
+
+ return DownloadUtils.getTransferTotal(currentBytes, totalBytes);
+ },
+
+ /**
+ * Returns the time remaining text for the provided Download object.
+ * For calculation a variable is stored in it.
+ */
+ getTimeRemaining(download) {
+ // If you do changes here please check progressDialog.js.
+ if (!download.stopped) {
+ let lastSec = (download.lastSec == null) ? Infinity : download.lastSec;
+ // Calculate the time remaining if we have valid values
+ let seconds = (download.speed > 0) && (download.totalBytes > 0)
+ ? (download.totalBytes - download.currentBytes) / download.speed
+ : -1;
+ let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, lastSec);
+ // Store it back for next calculation.
+ download.lastSec = newLast;
+ return timeLeft;
+ }
+ return "";
+ },
+
+ /**
+ * Opens a downloaded file.
+ *
+ * @param aFile
+ * the downloaded file to be opened.
+ * @param aMimeInfo
+ * the mime type info object. May be null.
+ * @param aOwnerWindow
+ * the window with which this action is associated.
+ */
+ openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw new Error("aFile must be a nsIFile object");
+ }
+ if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) {
+ throw new Error("Invalid value passed for aMimeInfo");
+ }
+ if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) {
+ throw new Error("aOwnerWindow must be a dom-window object");
+ }
+
+ let isWindowsExe = AppConstants.platform == "win" &&
+ aFile.leafName.toLowerCase().endsWith(".exe");
+
+ let promiseShouldLaunch;
+ // Don't prompt on Windows for .exe since there will be a native prompt.
+ if (aFile.isExecutable() && !isWindowsExe) {
+ // We get a prompter for the provided window here, even though anchoring
+ // to the most recently active window should work as well.
+ promiseShouldLaunch =
+ DownloadUIHelper.getPrompter(aOwnerWindow)
+ .confirmLaunchExecutable(aFile.path);
+ } else {
+ promiseShouldLaunch = Promise.resolve(true);
+ }
+
+ promiseShouldLaunch.then(shouldLaunch => {
+ if (!shouldLaunch) {
+ return;
+ }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
+ }
+ } catch (ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ } catch (ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Show a downloaded file in the system file manager.
+ *
+ * @param aFile
+ * a downloaded file.
+ */
+ showDownloadedFile(aFile) {
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw new Error("aFile must be a nsIFile object");
+ }
+ try {
+ // Show the directory containing the file and select the file.
+ aFile.reveal();
+ } catch (ex) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix
+ // or the file doesn't exist), try using the parent if we have it.
+ let parent = aFile.parent;
+ if (parent) {
+ this.showDirectory(parent);
+ }
+ }
+ },
+
+ /**
+ * Show the specified folder in the system file manager.
+ *
+ * @param aDirectory
+ * a directory to be opened with system file manager.
+ */
+ showDirectory(aDirectory) {
+ if (!(aDirectory instanceof Ci.nsIFile)) {
+ throw new Error("aDirectory must be a nsIFile object");
+ }
+ try {
+ aDirectory.launch();
+ } catch (ex) {
+ // If launch fails (probably because it's not implemented), let
+ // the OS handler try to open the directory.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aDirectory));
+ }
+ },
+
+ /**
+ * Displays an alert message box which asks the user if they want to
+ * unblock the downloaded file or not.
+ *
+ * @param options
+ * An object with the following properties:
+ * {
+ * verdict:
+ * The detailed reason why the download was blocked, according to
+ * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
+ * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
+ * assumed.
+ * window:
+ * The window with which this action is associated.
+ * dialogType:
+ * String that determines which actions are available:
+ * - "unblock" to offer just "unblock".
+ * - "chooseUnblock" to offer "unblock" and "confirmBlock".
+ * - "chooseOpen" to offer "open" and "confirmBlock".
+ * }
+ *
+ * @return {Promise}
+ * @resolves String representing the action that should be executed:
+ * - "open" to allow the download and open the file.
+ * - "unblock" to allow the download without opening the file.
+ * - "confirmBlock" to delete the blocked data permanently.
+ * - "cancel" to do nothing and cancel the operation.
+ */
+ async confirmUnblockDownload({ verdict, window,
+ dialogType }) {
+ let s = DownloadsCommon.strings;
+
+ // All the dialogs have an action button and a cancel button, while only
+ // some of them have an additonal button to remove the file. The cancel
+ // button must always be the one at BUTTON_POS_1 because this is the value
+ // returned by confirmEx when using ESC or closing the dialog (bug 345067).
+ let title = s.unblockHeaderUnblock;
+ let firstButtonText = s.unblockButtonUnblock;
+ let firstButtonAction = "unblock";
+ let buttonFlags =
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
+
+ switch (dialogType) {
+ case "unblock":
+ // Use only the unblock action. The default is to cancel.
+ buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+ break;
+ case "chooseUnblock":
+ // Use the unblock and remove file actions. The default is remove file.
+ buttonFlags +=
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+ Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+ break;
+ case "chooseOpen":
+ // Use the unblock and open file actions. The default is open file.
+ title = s.unblockHeaderOpen;
+ firstButtonText = s.unblockButtonOpen;
+ firstButtonAction = "open";
+ buttonFlags +=
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+ Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+ break;
+ default:
+ Cu.reportError("Unexpected dialog type: " + dialogType);
+ return "cancel";
+ }
+
+ let message;
+ switch (verdict) {
+ case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
+ message = s.unblockTypeUncommon2;
+ break;
+ case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+ message = s.unblockTypePotentiallyUnwanted2;
+ break;
+ default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
+ message = s.unblockTypeMalware;
+ break;
+ }
+ message += "\n\n" + s.unblockTip2;
+
+ Services.ww.registerNotification(function onOpen(subj, topic) {
+ if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+ // Make sure to listen for "DOMContentLoaded" because it is fired
+ // before the "load" event.
+ subj.addEventListener("DOMContentLoaded", function() {
+ if (subj.document.documentURI ==
+ "chrome://global/content/commonDialog.xul") {
+ Services.ww.unregisterNotification(onOpen);
+ let dialog = subj.document.getElementById("commonDialog");
+ if (dialog) {
+ // Change the dialog to use a warning icon.
+ dialog.classList.add("alert-dialog");
+ }
+ }
+ }, {once: true});
+ }
+ });
+
+ let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
+ firstButtonText, null,
+ s.unblockButtonConfirmBlock, null, {});
+ return [firstButtonAction, "cancel", "confirmBlock"][rv];
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => {
+ return DownloadsLogger.log.bind(DownloadsLogger);
+});
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => {
+ return DownloadsLogger.error.bind(DownloadsLogger);
+});
+
+// DownloadsData
+
+/**
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing to
+ * build a consistent view of the available data.
+ *
+ * Note that using this object does not automatically initialize the list of
+ * downloads. This is useful to display a neutral progress indicator in
+ * the main browser window until the autostart timeout elapses.
+ *
+ * This powers the DownloadsData and HistoryDownloadsData singleton objects.
+ */
+ function DownloadsDataCtor({ isHistory } = {}) {
+
+ // Contains all the available Download objects and their integer state.
+ this.oldDownloadStates = new Map();
+
+ // For the history downloads list we don't need to register this as a view,
+ // but we have to ensure that the DownloadsData object is initialized before
+ // we register more views. This ensures that the view methods of DownloadsData
+ // are invoked before those of views registered on HistoryDownloadsData,
+ // allowing the endTime property to be set correctly.
+ if (isHistory) {
+ DownloadsData.initializeDataLink();
+ this._promiseList = DownloadsData._promiseList
+ .then(() => DownloadHistory.getList());
+ return;
+ }
+
+ // This defines "initializeDataLink" and "_promiseList" synchronously, then
+ // continues execution only when "initializeDataLink" is called, allowing the
+ // underlying data to be loaded only when actually needed.
+ this._promiseList = (async () => {
+ await new Promise(resolve => this.initializeDataLink = resolve);
+ let list = await Downloads.getList(Downloads.ALL);
+
+ await list.addView(this);
+ this._downloadsLoaded = true;
+
+ return list;
+ })();
+}
+
+DownloadsDataCtor.prototype = {
+ /**
+ * Starts receiving events for current downloads.
+ */
+ initializeDataLink() {},
+
+ /**
+ * Used by sound logic when download ends.
+ */
+ _sound: null,
+ /**
+ * Promise resolved with the underlying DownloadList object once we started
+ * receiving events for current downloads.
+ */
+ _promiseList: null,
+
+ _downloadsLoaded: null,
+
+ /**
+ * Iterator for all the available Download objects. This is empty until the
+ * data has been loaded using the JavaScript API for downloads.
+ */
+ get downloads() {
+ return this.oldDownloadStates.keys();
+ },
+
+ /**
+ * True if there are finished downloads that can be removed from the list.
+ */
+ get canRemoveFinished() {
+ for (let download of this.downloads) {
+ // Stopped, paused, and failed downloads with partial data are removed.
+ if (download.stopped && !(download.canceled && download.hasPartialData)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Asks the back-end to remove finished downloads from the list. This method
+ * is only called after the data link has been initialized.
+ */
+ removeFinished() {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.removeFinished())
+ .catch(Cu.reportError);
+ },
+
+ // Integration with the asynchronous Downloads back-end
+
+ // Download view
+ onDownloadAdded: function(download)
+ {
+ // Download objects do not store the end time of downloads, as the Downloads
+ // API does not need to persist this information for all platforms. Once a
+ // download terminates on a Desktop browser, it becomes a history download,
+ // for which the end time is stored differently, as a Places annotation.
+ download.endTime = Date.now();
+ this.oldDownloadStates.set(download,
+ DownloadsCommon.stateOfDownload(download));
+
+ download.displayName =
+ download.target.path ? OS.Path.basename(download.target.path)
+ : download.source.url;
+ this.onDownloadChanged(download);
+ if (!this._downloadsLoaded)
+ return;
+
+ var behavior = download.source.isPrivate ? 1 :
+ Services.prefs.getIntPref(PREF_DM_BEHAVIOR);
+ switch (behavior) {
+ case 0:
+ Cc["@mozilla.org/suite/suiteglue;1"]
+ .getService(Ci.nsISuiteGlue)
+ .showDownloadManager(true);
+ break;
+ case 1:
+ Services.ww.openWindow(null, PROGRESS_DIALOG_URL, null,
+ "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
+ { wrappedJSObject: download });
+ break;
+ }
+
+ return; // No UI for behavior >= 2
+ },
+
+ onDownloadChanged(download) {
+ let oldState = this.oldDownloadStates.get(download);
+ let newState = DownloadsCommon.stateOfDownload(download);
+ this.oldDownloadStates.set(download, newState);
+
+ if (oldState != newState &&
+ (download.succeeded ||
+ (download.canceled && !download.hasPartialData) ||
+ download.error)) {
+ // Store the end time that may be displayed by the views.
+ download.endTime = Date.now();
+
+ // This state transition code should actually be located in a Downloads
+ // API module (bug 941009).
+ // This might end with an exception if it is an unsupported uri scheme.
+ DownloadHistory.updateMetaData(download);
+
+ if (download.succeeded) {
+ this.playDownloadSound();
+ }
+ }
+ },
+
+ onDownloadRemoved(download) {
+ this.oldDownloadStates.delete(download);
+ },
+
+ // Download summary
+ onSummaryChanged: function() {
+
+ if (!gTaskbarProgress)
+ return;
+
+ const nsITaskbarProgress = Ci.nsITaskbarProgress;
+ var currentBytes = gDownloadsSummary.progressCurrentBytes;
+ var totalBytes = gDownloadsSummary.progressTotalBytes;
+ var state = gDownloadsSummary.allHaveStopped ?
+ currentBytes ? nsITaskbarProgress.STATE_PAUSED :
+ nsITaskbarProgress.STATE_NO_PROGRESS :
+ currentBytes < totalBytes ? nsITaskbarProgress.STATE_NORMAL :
+ nsITaskbarProgress.STATE_INDETERMINATE;
+ switch (state) {
+ case nsITaskbarProgress.STATE_NO_PROGRESS:
+ case nsITaskbarProgress.STATE_INDETERMINATE:
+ gTaskbarProgress.setProgressState(state, 0, 0);
+ break;
+ default:
+ gTaskbarProgress.setProgressState(state, currentBytes, totalBytes);
+ break;
+ }
+ },
+
+ // Play a download sound.
+ playDownloadSound: function()
+ {
+ if (Services.prefs.getBoolPref("browser.download.finished_download_sound")) {
+ if (!this._sound)
+ this._sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
+ try {
+ let url = Services.prefs.getStringPref("browser.download.finished_sound_url");
+ this._sound.play(Services.io.newURI(url));
+ } catch (e) {
+ this._sound.beep();
+ }
+ }
+ },
+
+ // Registration of views
+
+ /**
+ * Adds an object to be notified when the available download data changes.
+ * The specified object is initialized with the currently available downloads.
+ *
+ * @param aView
+ * DownloadsView object to be added. This reference must be passed to
+ * removeView before termination.
+ */
+ addView(aView) {
+ this._promiseList.then(list => list.addView(aView))
+ .catch(Cu.reportError);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsView object to be removed.
+ */
+ removeView(aView) {
+ this._promiseList.then(list => list.removeView(aView))
+ .catch(Cu.reportError);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() {
+ return new DownloadsDataCtor({ isHistory: true });
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
+ return new DownloadsDataCtor();
+});
diff --git a/comm/suite/components/downloads/DownloadsTaskbar.jsm b/comm/suite/components/downloads/DownloadsTaskbar.jsm
new file mode 100644
index 0000000000..6cefeedcca
--- /dev/null
+++ b/comm/suite/components/downloads/DownloadsTaskbar.jsm
@@ -0,0 +1,182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et filetype=javascript
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = [
+ "DownloadsTaskbar",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } =
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function() {
+ if (!("@mozilla.org/windows-taskbar;1" in Cc)) {
+ return null;
+ }
+ let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"]
+ .getService(Ci.nsIWinTaskbar);
+ return winTaskbar.available && winTaskbar;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function() {
+ return ("@mozilla.org/widget/macdocksupport;1" in Cc) &&
+ Cc["@mozilla.org/widget/macdocksupport;1"]
+ .getService(Ci.nsITaskbarProgress);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gGtkTaskbarProgress", function() {
+ return ("@mozilla.org/widget/taskbarprogress/gtk;1" in Cc) &&
+ Cc["@mozilla.org/widget/taskbarprogress/gtk;1"]
+ .getService(Ci.nsIGtkTaskbarProgress);
+});
+
+// DownloadsTaskbar
+
+/**
+ * Handles the download progress indicator in the taskbar.
+ */
+var DownloadsTaskbar = {
+ /**
+ * Underlying DownloadSummary providing the aggregate download information, or
+ * null if the indicator has never been initialized.
+ */
+ _summary: null,
+
+ /**
+ * nsITaskbarProgress object to which download information is dispatched.
+ * This can be null if the indicator has never been initialized or if the
+ * indicator is currently hidden on Windows.
+ */
+ _taskbarProgress: null,
+
+ /**
+ * This method is called after a new browser window is opened, and ensures
+ * that the download progress indicator is displayed in the taskbar.
+ *
+ * On Windows, the indicator is attached to the first browser window that
+ * calls this method. When the window is closed, the indicator is moved to
+ * another browser window, if available, in no particular order. When there
+ * are no browser windows visible, the indicator is hidden.
+ *
+ * On macOS, the indicator is initialized globally when this method is
+ * called for the first time. Subsequent calls have no effect.
+ *
+ * @param aBrowserWindow
+ * nsIDOMWindow object of the newly opened browser window to which the
+ * indicator may be attached.
+ */
+ registerIndicator(aWindow) {
+ if (!this._taskbarProgress) {
+ if (gMacTaskbarProgress) {
+ // On macOS, we have to register the global indicator only once.
+ this._taskbarProgress = gMacTaskbarProgress;
+ // Free the XPCOM reference on shutdown, to prevent detecting a leak.
+ Services.obs.addObserver(() => {
+ this._taskbarProgress = null;
+ gMacTaskbarProgress = null;
+ }, "quit-application-granted");
+ } else {
+ // On Windows, the indicator is currently hidden because we have no
+ // previous window, thus we should attach the indicator now.
+ // In gtk3, the window itself implements the progress interface.
+ this.attachIndicator(aWindow);
+ }
+ }
+
+ // Ensure that the DownloadSummary object will be created asynchronously.
+ if (!this._summary) {
+ Downloads.getSummary(Downloads.ALL).then(summary => {
+ // In case the method is re-entered, we simply ignore redundant
+ // invocations of the callback, instead of keeping separate state.
+ if (this._summary) {
+ return undefined;
+ }
+ this._summary = summary;
+ return this._summary.addView(this);
+ }).catch(Cu.reportError);
+ }
+ },
+
+ /**
+ * On Windows and linux attach the taskbar indicator to the specified window.
+ */
+ attachIndicator(aWindow) {
+ // If there is already a taskbarProgress this usually means the download
+ // manager became active. So clear the taskbar state first.
+ if (this._taskbarProgress) {
+ this._taskbarProgress.setProgressState(Ci.nsITaskbarProgress.STATE_NO_PROGRESS);
+ }
+
+ if (gWinTaskbar) {
+ // Activate the indicator on the specified window.
+ let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow).docShell;
+ this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell);
+ } else if (gGtkTaskbarProgress) {
+ // In gtk3, the window itself implements the progress interface.
+ if (!this._taskbarProgress) {
+ this._taskbarProgress = gGtkTaskbarProgress;
+ }
+
+ this._taskbarProgress.setPrimaryWindow(aWindow);
+ } else {
+ // macOS, not gtk3 or unsupported OS.
+ return;
+ }
+
+ // If the DownloadSummary object has already been created, we should update
+ // the state of the new indicator, otherwise it will be updated as soon as
+ // the DownloadSummary view is registered.
+ if (this._summary) {
+ this.onSummaryChanged();
+ }
+
+ aWindow.addEventListener("unload", () => {
+ let windows = Services.wm.getEnumerator(null);
+ let newActiveWindow = null;
+ if (windows.hasMoreElements()) {
+ newActiveWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+ }
+ if (newActiveWindow) {
+ // Move the progress indicator to the other window.
+ this.attachIndicator(newActiveWindow);
+ } else {
+ // The last window has been closed. We remove the reference to
+ // the taskbar progress object.
+ this._taskbarProgress = null;
+ }
+ });
+ },
+
+ // DownloadSummary view
+ onSummaryChanged() {
+ // If the last window has been closed, we have no indicator any more.
+ if (!this._taskbarProgress) {
+ return;
+ }
+
+ if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) {
+ this._taskbarProgress.setProgressState(
+ Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0);
+ } else {
+ // For a brief moment before completion, some download components may
+ // report more transferred bytes than the total number of bytes. Thus,
+ // ensure that we never break the expectations of the progress indicator.
+ let progressCurrentBytes = Math.min(this._summary.progressTotalBytes,
+ this._summary.progressCurrentBytes);
+ this._taskbarProgress.setProgressState(
+ Ci.nsITaskbarProgress.STATE_NORMAL,
+ progressCurrentBytes,
+ this._summary.progressTotalBytes);
+ }
+ },
+};
diff --git a/comm/suite/components/downloads/content/DownloadProgressListener.js b/comm/suite/components/downloads/content/DownloadProgressListener.js
new file mode 100644
index 0000000000..b5bab95727
--- /dev/null
+++ b/comm/suite/components/downloads/content/DownloadProgressListener.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * DownloadProgressListener "class" is used to help update download items shown
+ * in the Download Manager UI such as displaying amount transferred, transfer
+ * rate, and time left for each download.
+ */
+function DownloadProgressListener() {}
+
+DownloadProgressListener.prototype = {
+ onDownloadAdded: function(aDownload) {
+ gDownloadTreeView.addDownload(aDownload);
+
+ // Update window title in-case we don't get all progress notifications
+ onUpdateProgress();
+ },
+
+ onDownloadChanged: function(aDownload) {
+ gDownloadTreeView.updateDownload(aDownload);
+
+ // Update window title
+ onUpdateProgress();
+ },
+
+ onDownloadRemoved: function(aDownload) {
+ gDownloadTreeView.removeDownload(aDownload);
+ }
+};
diff --git a/comm/suite/components/downloads/content/downloadmanager.js b/comm/suite/components/downloads/content/downloadmanager.js
new file mode 100644
index 0000000000..d390e655dd
--- /dev/null
+++ b/comm/suite/components/downloads/content/downloadmanager.js
@@ -0,0 +1,634 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ Downloads: "resource://gre/modules/Downloads.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ FileUtils: "resource://gre/modules/FileUtils.jsm",
+});
+
+var gDownloadTree;
+var gDownloadTreeView;
+var gDownloadList;
+var gDownloadStatus;
+var gDownloadListener;
+var gSearchBox;
+
+function dmStartup()
+{
+ Downloads.getList(Downloads.PUBLIC).then(dmAsyncStartup);
+}
+
+function dmAsyncStartup(aList)
+{
+ gDownloadList = aList;
+
+ gDownloadTree = document.getElementById("downloadTree");
+ gDownloadStatus = document.getElementById("statusbar-display");
+ gSearchBox = document.getElementById("search-box");
+
+ // Insert as first controller on the whole window
+ window.controllers.insertControllerAt(0, dlTreeController);
+
+ // We need to keep the view object around globally to access "local"
+ // non-nsITreeView methods
+ gDownloadTreeView = new DownloadTreeView();
+ gDownloadTree.view = gDownloadTreeView;
+
+ // The DownloadProgressListener (DownloadProgressListener.js) handles
+ // progress notifications.
+ gDownloadListener = new DownloadProgressListener();
+ gDownloadList.addView(gDownloadListener);
+
+ // correct keybinding command attributes which don't do our business yet
+ var key = document.getElementById("key_delete");
+ if (key.hasAttribute("command"))
+ key.setAttribute("command", "cmd_stop");
+ key = document.getElementById("key_delete2");
+ if (key.hasAttribute("command"))
+ key.setAttribute("command", "cmd_stop");
+
+ gDownloadTree.focus();
+
+ if (gDownloadTree.view.rowCount > 0)
+ gDownloadTree.view.selection.select(0);
+}
+
+function dmShutdown()
+{
+ gDownloadList.removeView(gDownloadListener);
+ window.controllers.removeController(dlTreeController);
+}
+
+function searchDownloads(aInput)
+{
+ gDownloadTreeView.searchView(aInput);
+}
+
+function sortDownloads(aEventTarget)
+{
+ var column = aEventTarget;
+ var colID = column.id;
+ var sortDirection = null;
+
+ // If the target is a menuitem, handle it and forward to a column
+ if (/^menu_SortBy/.test(colID)) {
+ colID = colID.replace(/^menu_SortBy/, "");
+ column = document.getElementById(colID);
+ var sortedColumn = gDownloadTree.columns.getSortedColumn();
+ if (sortedColumn && sortedColumn.id == colID)
+ sortDirection = sortedColumn.element.getAttribute("sortDirection");
+ else
+ sortDirection = "ascending";
+ }
+ else if (colID == "menu_Unsorted") {
+ // calling .sortView() with an "unsorted" colID returns us to original order
+ colID = "unsorted";
+ column = null;
+ sortDirection = "ascending";
+ }
+ else if (colID == "menu_SortAscending" || colID == "menu_SortDescending") {
+ sortDirection = colID.replace(/^menu_Sort/, "").toLowerCase();
+ var sortedColumn = gDownloadTree.columns.getSortedColumn();
+ if (sortedColumn) {
+ colID = sortedColumn.id;
+ column = sortedColumn.element;
+ }
+ }
+
+ // Abort if this is still no column
+ if (column && column.localName != "treecol")
+ return;
+
+ // Abort on cyler columns, we don't sort them
+ if (column && column.getAttribute("cycler") == "true")
+ return;
+
+ if (!sortDirection) {
+ // If not set above already, toggle the current direction
+ sortDirection = column.getAttribute("sortDirection") == "ascending" ?
+ "descending" : "ascending";
+ }
+
+ // Clear attributes on all columns, we're setting them again after sorting
+ for (let node = document.getElementById("Name"); node; node = node.nextSibling) {
+ node.removeAttribute("sortActive");
+ node.removeAttribute("sortDirection");
+ }
+
+ // Actually sort the tree view
+ gDownloadTreeView.sortView(colID, sortDirection);
+
+ if (column) {
+ // Set attributes to the sorting we did
+ column.setAttribute("sortActive", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ }
+}
+
+async function removeDownload(aDownload)
+{
+ // Remove the associated history element first, if any, so that the views
+ // that combine history and session downloads won't resurrect the history
+ // download into the view just before it is deleted permanently.
+ try {
+ await PlacesUtils.history.remove(aDownload.source.url);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ let list = await Downloads.getList(Downloads.ALL);
+ await list.remove(aDownload);
+ await aDownload.finalize(true);
+}
+
+function cancelDownload(aDownload)
+{
+ // This is the correct way to avoid race conditions when cancelling.
+ aDownload.cancel().catch(() => {});
+ aDownload.removePartialData().catch(Cu.reportError);
+}
+
+function openDownload(aDownload)
+{
+ let file = new FileUtils.File(aDownload.target.path);
+ DownloadsCommon.openDownloadedFile(file, null, window);
+}
+
+function showDownload(aDownload)
+{
+ let file;
+
+ if (aDownload.succeeded &&
+ aDownload.target.exists) {
+ file = new FileUtils.File(aDownload.target.path);
+ } else {
+ file = new FileUtils.File(aDownload.target.partFilePath);
+ }
+ DownloadsCommon.showDownloadedFile(file);
+}
+
+function showProperties(aDownload)
+{
+ openDialog("chrome://communicator/content/downloads/progressDialog.xul",
+ null, "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
+ { wrappedJSObject: aDownload }, true);
+}
+
+function onTreeSelect(aEvent)
+{
+ var selectionCount = gDownloadTreeView.selection.count;
+ if (selectionCount == 1) {
+ var selItemData = gDownloadTreeView.getRowData(gDownloadTree.currentIndex);
+ gDownloadStatus.label = selItemData.target.path;
+ } else {
+ gDownloadStatus.label = "";
+ }
+
+ window.updateCommands("tree-select");
+}
+
+function onUpdateViewColumns(aMenuItem)
+{
+ while (aMenuItem) {
+ // Each menuitem should be checked if its column is not hidden.
+ var colID = aMenuItem.id.replace(/^menu_Toggle/, "");
+ var column = document.getElementById(colID);
+ aMenuItem.setAttribute("checked", !column.hidden);
+ aMenuItem = aMenuItem.nextSibling;
+ }
+}
+
+function toggleColumn(aMenuItem)
+{
+ var colID = aMenuItem.id.replace(/^menu_Toggle/, "");
+ var column = document.getElementById(colID);
+ column.setAttribute("hidden", !column.hidden);
+}
+
+function onUpdateViewSort(aMenuItem)
+{
+ var unsorted = true;
+ var ascending = true;
+ while (aMenuItem) {
+ switch (aMenuItem.id) {
+ case "": // separator
+ break;
+ case "menu_Unsorted":
+ if (unsorted) // this would work even if Unsorted was last
+ aMenuItem.setAttribute("checked", "true");
+ break;
+ case "menu_SortAscending":
+ aMenuItem.setAttribute("disabled", unsorted);
+ if (!unsorted && ascending)
+ aMenuItem.setAttribute("checked", "true");
+ break;
+ case "menu_SortDescending":
+ aMenuItem.setAttribute("disabled", unsorted);
+ if (!unsorted && !ascending)
+ aMenuItem.setAttribute("checked", "true");
+ break;
+ default:
+ var colID = aMenuItem.id.replace(/^menu_SortBy/, "");
+ var column = document.getElementById(colID);
+ var direction = column.getAttribute("sortDirection");
+ if (column.getAttribute("sortActive") == "true" && direction) {
+ // We've found a sorted column. Remember its direction.
+ ascending = direction == "ascending";
+ unsorted = false;
+ aMenuItem.setAttribute("checked", "true");
+ }
+ }
+ aMenuItem = aMenuItem.nextSibling;
+ }
+}
+
+// This is called by the progress listener.
+var gLastComputedMean = -1;
+var gLastActiveDownloads = 0;
+function onUpdateProgress()
+{
+ var dls = gDownloadTreeView.getActiveDownloads();
+ var numActiveDownloads = dls.length;
+
+ // Use the default title and reset "last" values if there's no downloads
+ if (numActiveDownloads == 0) {
+ document.title = document.documentElement.getAttribute("statictitle");
+ gLastComputedMean = -1;
+ gLastActiveDownloads = 0;
+
+ return;
+ }
+
+ // Establish the mean transfer speed and amount downloaded.
+ var mean = 0;
+ var base = 0;
+ for (var dl of dls) {
+ if (dl.totalBytes > 0) {
+ mean += dl.currentBytes;
+ base += dl.totalBytes;
+ }
+ }
+
+ // Calculate the percent transferred, unless we don't have a total file size
+ var dlbundle = document.getElementById("dmBundle");
+ if (base != 0)
+ mean = Math.floor((mean / base) * 100);
+
+ // Update title of window
+ if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) {
+ gLastComputedMean = mean;
+ gLastActiveDownloads = numActiveDownloads;
+
+ var title;
+ if (base == 0)
+ title = dlbundle.getFormattedString("downloadsTitleFiles",
+ [numActiveDownloads]);
+ else
+ title = dlbundle.getFormattedString("downloadsTitlePercent",
+ [numActiveDownloads, mean]);
+
+ // Get the correct plural form and insert number of downloads and percent
+ title = PluralForm.get(numActiveDownloads, title);
+
+ document.title = title;
+ }
+}
+
+function handlePaste() {
+ let trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(null);
+
+ let flavors = ["text/x-moz-url", "text/unicode"];
+ flavors.forEach(trans.addDataFlavor);
+
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+
+ // Getting the data or creating the nsIURI might fail
+ try {
+ let data = {};
+ trans.getAnyTransferData({}, data, {});
+ let [url, name] = data.value.QueryInterface(Ci
+ .nsISupportsString).data.split("\n");
+
+ if (!url)
+ return;
+
+ DownloadURL(url, name || url, document);
+ } catch (ex) {}
+}
+
+var dlTreeController = {
+ supportsCommand: function(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_play":
+ case "cmd_pause":
+ case "cmd_resume":
+ case "cmd_retry":
+ case "cmd_cancel":
+ case "cmd_remove":
+ case "cmd_stop":
+ case "cmd_open":
+ case "cmd_show":
+ case "cmd_openReferrer":
+ case "cmd_copyLocation":
+ case "cmd_properties":
+ case "cmd_paste":
+ case "cmd_selectAll":
+ case "cmd_clearList":
+ return true;
+ }
+ return false;
+ },
+
+ isCommandEnabled: function(aCommand)
+ {
+ var selectionCount = 0;
+ if (gDownloadTreeView && gDownloadTreeView.selection)
+ selectionCount = gDownloadTreeView.selection.count;
+
+ var selItemData = [];
+ if (selectionCount) {
+ // walk all selected rows
+ let start = {};
+ let end = {};
+ let numRanges = gDownloadTreeView.selection.getRangeCount();
+ for (let rg = 0; rg < numRanges; rg++) {
+ gDownloadTreeView.selection.getRangeAt(rg, start, end);
+ for (let row = start.value; row <= end.value; row++)
+ selItemData.push(gDownloadTreeView.getRowData(row));
+ }
+ }
+
+ switch (aCommand) {
+ case "cmd_play":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (dldata.succeeded || (!dldata.stopped && !dldata.hasPartialData))
+ return false;
+ }
+ return true;
+ case "cmd_pause":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (dldata.stopped || !dldata.hasPartialData)
+ return false;
+ }
+ return true;
+ case "cmd_resume":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (!dldata.stopped || !dldata.hasPartialData)
+ return false;
+ }
+ return true;
+ case "cmd_open":
+ return selectionCount == 1 &&
+ selItemData[0].succeeded &&
+ selItemData[0].target.exists;
+ case "cmd_show":
+ // target.exists is only set if the download finished and the target
+ // is still located there.
+ // For simplicity we just assume the target is there if the download
+ // has not succeeded e.g. is still in progress. This might be wrong
+ // but showDownload will deal with it.
+ return selectionCount == 1 &&
+ ((selItemData[0].succeeded &&
+ selItemData[0].target.exists) ||
+ !selItemData[0].succeeded);
+ case "cmd_cancel":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (dldata.stopped && !dldata.hasPartialData)
+ return false;
+ }
+ return true;
+ case "cmd_retry":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (dldata.succeeded || !dldata.stopped || dldata.hasPartialData)
+ return false;
+ }
+ return true;
+ case "cmd_remove":
+ if (!selectionCount)
+ return false;
+ for (let dldata of selItemData) {
+ if (!dldata.stopped)
+ return false;
+ }
+ return true;
+ case "cmd_openReferrer":
+ return selectionCount == 1 && !!selItemData[0].source.referrer;
+ case "cmd_stop":
+ case "cmd_copyLocation":
+ return selectionCount > 0;
+ case "cmd_properties":
+ return selectionCount == 1;
+ case "cmd_selectAll":
+ return gDownloadTreeView.rowCount != selectionCount;
+ case "cmd_clearList":
+ // Since active downloads always sort before removable downloads,
+ // we only need to check that the last download has stopped.
+ return gDownloadTreeView.rowCount &&
+ !gDownloadTreeView.getRowData(gDownloadTreeView.rowCount - 1).isActive;
+ case "cmd_paste":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(aCommand) {
+ var selectionCount = 0;
+ if (gDownloadTreeView && gDownloadTreeView.selection)
+ selectionCount = gDownloadTreeView.selection.count;
+
+ var selItemData = [];
+ if (selectionCount) {
+ // walk all selected rows
+ let start = {};
+ let end = {};
+ let numRanges = gDownloadTreeView.selection.getRangeCount();
+ for (let rg = 0; rg < numRanges; rg++) {
+ gDownloadTreeView.selection.getRangeAt(rg, start, end);
+ for (let row = start.value; row <= end.value; row++)
+ selItemData.push(gDownloadTreeView.getRowData(row));
+ }
+ }
+
+ switch (aCommand) {
+ case "cmd_play":
+ for (let dldata of selItemData) {
+ if (!dldata.stopped)
+ dldata.cancel();
+ else if (!dldata.succeeded)
+ dldata.start();
+ }
+ break;
+ case "cmd_pause":
+ for (let dldata of selItemData)
+ dldata.cancel();
+ break;
+ case "cmd_resume":
+ case "cmd_retry":
+ for (let dldata of selItemData) {
+ // Errors when retrying are already reported as download failures.
+ dldata.start();
+ }
+ break;
+ case "cmd_cancel":
+ for (let dldata of selItemData)
+ cancelDownload(dldata);
+ break;
+ case "cmd_remove":
+ for (let dldata of selItemData)
+ removeDownload(dldata).catch(Cu.reportError);
+ break;
+ case "cmd_stop":
+ for (let dldata of selItemData) {
+ if (dldata.isActive)
+ cancelDownload(dldata);
+ else
+ gDownloadList.remove(dldata);
+ }
+ break;
+ case "cmd_open":
+ openDownload(selItemData[0]);
+ break;
+ case "cmd_show":
+ showDownload(selItemData[0]);
+ break;
+ case "cmd_openReferrer":
+ openUILink(selItemData[0].source.referrer);
+ break;
+ case "cmd_copyLocation":
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ var uris = [];
+ for (let dldata of selItemData)
+ uris.push(dldata.source.url);
+ clipboard.copyString(uris.join("\n"), document);
+ break;
+ case "cmd_properties":
+ showProperties(selItemData[0]);
+ break;
+ case "cmd_selectAll":
+ gDownloadTreeView.selection.selectAll();
+ break;
+ case "cmd_clearList":
+ // Remove each download starting from the end until we hit a download
+ // that is in progress
+ for (let idx = gDownloadTreeView.rowCount - 1; idx >= 0; idx--) {
+ let dldata = gDownloadTreeView.getRowData(idx);
+ if (!dldata.isActive) {
+ gDownloadList.remove(dldata);
+ }
+ }
+
+ if (!gSearchBox.value)
+ break;
+
+ // Clear the input as if the user did it and move focus to the list
+ gSearchBox.value = "";
+ searchDownloads("");
+ gDownloadTree.focus();
+ break;
+ case "cmd_paste":
+ handlePaste();
+ break;
+ }
+ },
+
+ onEvent: function(aEvent){
+ switch (aEvent) {
+ case "tree-select":
+ this.onCommandUpdate();
+ }
+ },
+
+ onCommandUpdate: function() {
+ var cmds = ["cmd_play", "cmd_pause", "cmd_resume", "cmd_retry",
+ "cmd_cancel", "cmd_remove", "cmd_stop", "cmd_open", "cmd_show",
+ "cmd_openReferrer", "cmd_copyLocation", "cmd_properties",
+ "cmd_selectAll", "cmd_clearList"];
+ for (let command in cmds)
+ goUpdateCommand(cmds[command]);
+ }
+};
+
+var gDownloadDNDObserver = {
+ onDragStart: function (aEvent)
+ {
+ if (!gDownloadTreeView ||
+ !gDownloadTreeView.selection ||
+ !gDownloadTreeView.selection.count)
+ return;
+
+ var selItemData = gDownloadTreeView.getRowData(gDownloadTree.currentIndex);
+ var file = new FileUtils.File(selItemData.target.path);
+
+ if (!file.exists())
+ return;
+
+ var url = Services.io.newFileURI(file).spec;
+ var dt = aEvent.dataTransfer;
+ dt.mozSetDataAt("application/x-moz-file", file, 0);
+ dt.setData("text/uri-list", url + "\r\n");
+ dt.setData("text/plain", url + "\n");
+ dt.effectAllowed = "copyMove";
+ if (gDownloadTreeView.selection.count == 1)
+ dt.setDragImage(gDownloadStatus, 16, 16);
+ },
+
+ onDragOver: function (aEvent)
+ {
+ if (disallowDrop(aEvent))
+ return;
+
+ var types = aEvent.dataTransfer.types;
+ if (types.includes("text/uri-list") ||
+ types.includes("text/x-moz-url") ||
+ types.includes("text/plain"))
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ onDrop: function(aEvent)
+ {
+ if (disallowDrop(aEvent))
+ return;
+
+ var dt = aEvent.dataTransfer;
+ var url = dt.getData("URL");
+ var name;
+ if (!url) {
+ url = dt.getData("text/x-moz-url") || dt.getData("text/plain");
+ [url, name] = url.split("\n");
+ }
+ if (url) {
+ let doc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
+ saveURL(url, name || url, null, true, true, null, doc);
+ }
+ }
+};
+
+function disallowDrop(aEvent)
+{
+ var dt = aEvent.dataTransfer;
+ var file = dt.mozGetDataAt("application/x-moz-file", 0);
+ // If this is a local file, Don't try to download it again.
+ return file && file instanceof Ci.nsIFile;
+}
diff --git a/comm/suite/components/downloads/content/downloadmanager.xul b/comm/suite/components/downloads/content/downloadmanager.xul
new file mode 100644
index 0000000000..5633d284b6
--- /dev/null
+++ b/comm/suite/components/downloads/content/downloadmanager.xul
@@ -0,0 +1,452 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/downloads/downloadmanager.css" type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % downloadsDTD SYSTEM "chrome://communicator/locale/downloads/downloadmanager.dtd">
+%downloadsDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+]>
+
+<window id="downloadManager"
+ title="&downloadManager.title;" statictitle="&downloadManager.title;"
+ onload="dmStartup();" onunload="dmShutdown();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="500" height="400" screenX="10" screenY="10"
+ persist="width height screenX screenY sizemode"
+ toggletoolbar="true"
+ lightweightthemes="true"
+ lightweightthemesfooter="status-bar"
+ drawtitle="true"
+ windowtype="Download:Manager">
+
+ <script src="chrome://communicator/content/downloads/downloadmanager.js"/>
+ <script src="chrome://communicator/content/downloads/DownloadProgressListener.js"/>
+ <script src="chrome://communicator/content/downloads/treeView.js"/>
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <broadcaster id="Communicator:WorkMode"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="dmBundle"
+ src="chrome://communicator/locale/downloads/downloadmanager.properties"/>
+ </stringbundleset>
+
+ <commandset id="dlWinCommands">
+ <commandset id="tasksCommands">
+ <!-- File Menu -->
+ <command id="cmd_close" oncommand="window.close()"/>
+ <!-- Search Box -->
+ <command id="cmd_search_focus"
+ oncommand="gSearchBox.focus();"/>
+ </commandset>
+ <commandset id="commandUpdate_Downloads"
+ commandupdater="true"
+ events="focus,tree-select"
+ oncommandupdate="dlTreeController.onCommandUpdate()"/>
+
+ <commandset id="downloadCommands">
+ <command id="cmd_play"
+ oncommand="goDoCommand('cmd_play');"/>
+ <command id="cmd_pause"
+ oncommand="goDoCommand('cmd_pause');"/>
+ <command id="cmd_resume"
+ oncommand="goDoCommand('cmd_resume');"/>
+ <command id="cmd_retry"
+ oncommand="goDoCommand('cmd_retry');"/>
+ <command id="cmd_cancel"
+ oncommand="goDoCommand('cmd_cancel');"/>
+ <command id="cmd_remove"
+ oncommand="goDoCommand('cmd_remove');"/>
+ <command id="cmd_stop"
+ oncommand="goDoCommand('cmd_stop');"/>
+ <command id="cmd_open"
+ oncommand="goDoCommand('cmd_open');"/>
+ <command id="cmd_show"
+ oncommand="goDoCommand('cmd_show');"/>
+ <command id="cmd_openReferrer"
+ oncommand="goDoCommand('cmd_openReferrer');"/>
+ <command id="cmd_copyLocation"
+ oncommand="goDoCommand('cmd_copyLocation');"/>
+ <command id="cmd_properties"
+ oncommand="goDoCommand('cmd_properties');"/>
+ <command id="cmd_clearList"
+ oncommand="goDoCommand('cmd_clearList');"/>
+ </commandset>
+ </commandset>
+
+ <keyset id="tasksKeys">
+ <!-- File Menu -->
+ <key id="key_open"
+ keycode="VK_RETURN"
+ command="cmd_open"/>
+ <key id="key_close"/>
+ <!-- Edit Menu -->
+ <key id="key_cut"/>
+ <key id="key_copy"/>
+ <key id="key_paste"
+ command="cmd_paste"/>
+ <key id="key_play"
+ key=" "
+ command="cmd_play"/>
+ <key id="key_delete"/>
+ <key id="key_delete2"/>
+ <key id="key_selectAll"/>
+ <!-- Search Box -->
+ <key id="key_search_focus"
+ command="cmd_search_focus"
+ key="&search.key;"
+ modifiers="accel"/>
+ </keyset>
+
+ <popupset id="downloadPopupset">
+ <menupopup id="downloadContext">
+ <menuitem id="dlContext-pause"
+ label="&cmd.pause.label;"
+ accesskey="&cmd.pause.accesskey;"
+ command="cmd_pause"/>
+ <menuitem id="dlContext-resume"
+ label="&cmd.resume.label;"
+ accesskey="&cmd.resume.accesskey;"
+ command="cmd_resume"/>
+ <menuitem id="dlContext-retry"
+ label="&cmd.retry.label;"
+ accesskey="&cmd.retry.accesskey;"
+ command="cmd_retry"/>
+ <menuitem id="dlContext-cancel"
+ label="&cmd.cancel.label;"
+ accesskey="&cmd.cancel.accesskey;"
+ command="cmd_cancel"/>
+ <menuitem id="dlContext-remove"
+ label="&cmd.remove.label;"
+ accesskey="&cmd.remove.accesskey;"
+ command="cmd_remove"/>
+ <menuseparator/>
+ <menuitem id="dlContext-open"
+ label="&cmd.open.label;"
+ accesskey="&cmd.open.accesskey;"
+ command="cmd_open"
+ default="true"/>
+ <menuitem id="dlContext-show"
+ label="&cmd.show.label;"
+ accesskey="&cmd.show.accesskey;"
+ command="cmd_show"/>
+ <menuitem id="dlContext-openReferrer"
+ label="&cmd.goToDownloadPage.label;"
+ accesskey="&cmd.goToDownloadPage.accesskey;"
+ command="cmd_openReferrer"/>
+ <menuitem id="dlContext-copyLocation"
+ label="&cmd.copyDownloadLink.label;"
+ accesskey="&cmd.copyDownloadLink.accesskey;"
+ command="cmd_copyLocation"/>
+ <menuitem id="dlContext-properties"
+ label="&cmd.properties.label;"
+ accesskey="&cmd.properties.accesskey;"
+ command="cmd_properties"/>
+ <menuseparator/>
+ <menuitem id="context-selectall"/>
+ </menupopup>
+ </popupset>
+
+ <vbox id="titlebar"/>
+
+ <toolbox id="download-toolbox">
+ <menubar id="download-menubar"
+ grippytooltiptext="&menuBar.tooltip;">
+ <menu id="menu_File">
+ <menupopup id="menu_FilePopup">
+ <menuitem id="dlMenu_open"
+ label="&cmd.open.label;"
+ accesskey="&cmd.open.accesskey;"
+ key="key_open"
+ command="cmd_open"/>
+ <menuitem id="dlMenu_show"
+ label="&cmd.show.label;"
+ accesskey="&cmd.show.accesskey;"
+ command="cmd_show"/>
+ <menuitem id="dlMenu_openReferrer"
+ label="&cmd.goToDownloadPage.label;"
+ accesskey="&cmd.goToDownloadPage.accesskey;"
+ command="cmd_openReferrer"/>
+ <menuitem id="dlMenu_properties"
+ label="&cmd.properties.label;"
+ accesskey="&cmd.properties.accesskey;"
+ command="cmd_properties"/>
+ <menuseparator/>
+ <menuitem id="menu_close"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_Edit">
+ <menupopup id="menu_EditPopup">
+ <menuitem id="dlMenu_pause"
+ label="&cmd.pause.label;"
+ accesskey="&cmd.pause.accesskey;"
+ command="cmd_pause"/>
+ <menuitem id="dlMenu_resume"
+ label="&cmd.resume.label;"
+ accesskey="&cmd.resume.accesskey;"
+ command="cmd_resume"/>
+ <menuitem id="dlMenu_retry"
+ label="&cmd.retry.label;"
+ accesskey="&cmd.retry.accesskey;"
+ command="cmd_retry"/>
+ <menuitem id="dlMenu_cancel"
+ label="&cmd.cancel.label;"
+ accesskey="&cmd.cancel.accesskey;"
+ command="cmd_cancel"/>
+ <menuseparator/>
+ <menuitem id="dlMenu_remove"
+ label="&cmd.remove.label;"
+ accesskey="&cmd.remove.accesskey;"
+ command="cmd_remove"/>
+ <menuitem id="dlMenu_copyLocation"
+ label="&cmd.copyDownloadLink.label;"
+ accesskey="&cmd.copyDownloadLink.accesskey;"
+ command="cmd_copyLocation"/>
+ <menuseparator/>
+ <menuitem id="dlMenu_clearList"
+ label="&cmd.clearList.label;"
+ accesskey="&cmd.clearList.accesskey;"
+ command="cmd_clearList"/>
+ <menuitem id="menu_selectAll"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_View">
+ <menupopup id="menu_ViewPopup">
+ <menu id="menu_ViewColumns"
+ label="&view.columns.label;"
+ accesskey="&view.columns.accesskey;">
+ <menupopup onpopupshowing="onUpdateViewColumns(this.firstChild);"
+ oncommand="toggleColumn(event.target);">
+ <menuitem id="menu_ToggleName" type="checkbox" disabled="true"
+ label="&col.name.label;"
+ accesskey="&col.name.accesskey;"/>
+ <menuitem id="menu_ToggleStatus" type="checkbox"
+ label="&col.status.label;"
+ accesskey="&col.status.accesskey;"/>
+ <menuitem id="menu_ToggleActionPlay" type="checkbox"
+ label="&col.actionPlay.label;"
+ accesskey="&col.actionPlay.accesskey;"/>
+ <menuitem id="menu_ToggleActionStop" type="checkbox"
+ label="&col.actionStop.label;"
+ accesskey="&col.actionStop.accesskey;"/>
+ <menuitem id="menu_ToggleProgress" type="checkbox"
+ label="&col.progress.label;"
+ accesskey="&col.progress.accesskey;"/>
+ <menuitem id="menu_ToggleTimeRemaining" type="checkbox"
+ label="&col.timeremaining.label;"
+ accesskey="&col.timeremaining.accesskey;"/>
+ <menuitem id="menu_ToggleTransferred" type="checkbox"
+ label="&col.transferred.label;"
+ accesskey="&col.transferred.accesskey;"/>
+ <menuitem id="menu_ToggleTransferRate" type="checkbox"
+ label="&col.transferrate.label;"
+ accesskey="&col.transferrate.accesskey;"/>
+ <menuitem id="menu_ToggleTimeElapsed" type="checkbox"
+ label="&col.timeelapsed.label;"
+ accesskey="&col.timeelapsed.accesskey;"/>
+ <menuitem id="menu_ToggleStartTime" type="checkbox"
+ label="&col.starttime.label;"
+ accesskey="&col.starttime.accesskey;"/>
+ <menuitem id="menu_ToggleEndTime" type="checkbox"
+ label="&col.endtime.label;"
+ accesskey="&col.endtime.accesskey;"/>
+ <menuitem id="menu_ToggleProgressPercent" type="checkbox"
+ label="&col.progresstext.label;"
+ accesskey="&col.progresstext.accesskey;"/>
+ <menuitem id="menu_ToggleSource" type="checkbox"
+ label="&col.source.label;"
+ accesskey="&col.source.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_ViewSortBy" label="&view.sortBy.label;"
+ accesskey="&view.sortBy.accesskey;">
+ <menupopup onpopupshowing="onUpdateViewSort(this.firstChild);"
+ oncommand="sortDownloads(event.target);">
+ <menuitem id="menu_Unsorted" type="radio" name="columns"
+ label="&view.unsorted.label;"
+ accesskey="&view.unsorted.accesskey;"/>
+ <menuseparator/>
+ <menuitem id="menu_SortByName" type="radio" name="columns"
+ label="&col.name.label;"
+ accesskey="&col.name.accesskey;"/>
+ <menuitem id="menu_SortByStatus" type="radio" name="columns"
+ label="&col.status.label;"
+ accesskey="&col.status.accesskey;"/>
+ <menuitem id="menu_SortByProgress" type="radio" name="columns"
+ label="&col.progress.label;"
+ accesskey="&col.progress.accesskey;"/>
+ <menuitem id="menu_SortByTimeRemaining" type="radio" name="columns"
+ label="&col.timeremaining.label;"
+ accesskey="&col.timeremaining.accesskey;"/>
+ <menuitem id="menu_SortByTransferred" type="radio" name="columns"
+ label="&col.transferred.label;"
+ accesskey="&col.transferred.accesskey;"/>
+ <menuitem id="menu_SortByTransferRate" type="radio" name="columns"
+ label="&col.transferrate.label;"
+ accesskey="&col.transferrate.accesskey;"/>
+ <menuitem id="menu_SortByTimeElapsed" type="radio" name="columns"
+ label="&col.timeelapsed.label;"
+ accesskey="&col.timeelapsed.accesskey;"/>
+ <menuitem id="menu_SortByStartTime" type="radio" name="columns"
+ label="&col.starttime.label;"
+ accesskey="&col.starttime.accesskey;"/>
+ <menuitem id="menu_SortByEndTime" type="radio" name="columns"
+ label="&col.endtime.label;"
+ accesskey="&col.endtime.accesskey;"/>
+ <menuitem id="menu_SortByProgressPercent" type="radio" name="columns"
+ label="&col.progresstext.label;"
+ accesskey="&col.progresstext.accesskey;"/>
+ <menuitem id="menu_SortBySource" type="radio" name="columns"
+ label="&col.source.label;"
+ accesskey="&col.source.accesskey;"/>
+ <menuseparator/>
+ <menuitem id="menu_SortAscending" type="radio" name="direction"
+ label="&view.sortAscending.label;"
+ accesskey="&view.sortAscending.accesskey;"/>
+ <menuitem id="menu_SortDescending" type="radio" name="direction"
+ label="&view.sortDescending.label;"
+ accesskey="&view.sortDescending.accesskey;"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ <menu id="tasksMenu">
+ <menupopup id="taskPopup">
+ <menuitem id="dlMenu_find"
+ label="&search.label;"
+ accesskey="&search.accesskey;"
+ hidden="true"
+ command="cmd_search_focus"
+ key="key_search_focus"/>
+ <menuseparator hidden="true"/>
+ </menupopup>
+ </menu>
+ <menu id="windowMenu"/>
+ <menu id="menu_Help"/>
+ </menubar>
+ <toolbar class="chromeclass-toolbar"
+ id="downloadToolbar"
+ align="center"
+ grippytooltiptext="&searchBar.tooltip;">
+ <textbox id="search-box"
+ clickSelectsAll="true"
+ type="search"
+ hidden="true"
+ aria-controls="downloadTree"
+ class="compact"
+ placeholder="&search.placeholder;"
+ oncommand="searchDownloads(this.value);"/>
+ <spacer flex="1"/>
+ <button id="clearListButton" command="cmd_clearList"
+ label="&cmd.clearList.label;"
+ accesskey="&cmd.clearList.accesskey;"
+ tooltiptext="&cmd.clearList.tooltip;"/>
+ </toolbar>
+ </toolbox>
+
+ <tree id="downloadTree"
+ flex="1" type="downloads"
+ class="plain"
+ context="downloadContext"
+ enableColumnDrag="true"
+ onselect="onTreeSelect(event);">
+ <treecols context="" onclick="sortDownloads(event.target)">
+ <treecol id="Name"
+ label="&col.name.label;"
+ tooltiptext="&col.name.tooltip;"
+ flex="3"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Status" hidden="true"
+ label="&col.status.label;"
+ tooltiptext="&col.status.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="ActionPlay" cycler="true"
+ label="&col.actionPlay.label;"
+ tooltiptext="&col.actionPlay.tooltip;"
+ class="treecol-image" fixed="true"
+ persist="hidden ordinal"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="ActionStop" cycler="true"
+ label="&col.actionStop.label;"
+ tooltiptext="&col.actionStop.tooltip;"
+ class="treecol-image" fixed="true"
+ persist="hidden ordinal"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Progress" type="progressmeter"
+ label="&col.progress.label;"
+ tooltiptext="&col.progress.tooltip;"
+ flex="3"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="ProgressPercent" hidden="true"
+ label="&col.progresstext.label;"
+ tooltiptext="&col.progresstext.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="TimeRemaining"
+ label="&col.timeremaining.label;"
+ tooltiptext="&col.timeremaining.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Transferred"
+ label="&col.transferred.label;"
+ tooltiptext="&col.transferred.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="TransferRate"
+ label="&col.transferrate.label;"
+ tooltiptext="&col.transferrate.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="TimeElapsed" hidden="true"
+ label="&col.timeelapsed.label;"
+ tooltiptext="&col.timeelapsed.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="StartTime" hidden="true"
+ label="&col.starttime.label;"
+ tooltiptext="&col.starttime.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="EndTime" hidden="true"
+ label="&col.endtime.label;"
+ tooltiptext="&col.endtime.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Source" hidden="true"
+ label="&col.source.label;"
+ tooltiptext="&col.source.tooltip;"
+ flex="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ </treecols>
+ <treechildren ondblclick="goDoCommand('cmd_open');"
+ ondragstart="gDownloadDNDObserver.onDragStart(event);"
+ ondragover="gDownloadDNDObserver.onDragOver(event);"
+ ondrop="gDownloadDNDObserver.onDrop(event);"/>
+ </tree>
+ <statusbar id="status-bar" class="chromeclass-status">
+ <statusbarpanel id="statusbar-display" flex="1"/>
+ <statusbarpanel class="statusbarpanel-iconic" id="offline-status"/>
+ </statusbar>
+
+</window>
diff --git a/comm/suite/components/downloads/content/progressDialog.js b/comm/suite/components/downloads/content/progressDialog.js
new file mode 100644
index 0000000000..dae93f7fe9
--- /dev/null
+++ b/comm/suite/components/downloads/content/progressDialog.js
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+});
+
+var gDownload;
+var gDownloadBundle;
+
+var gDlList;
+var gDlStatus;
+var gDlListener;
+var gDlSize;
+var gTimeLeft;
+var gProgressMeter;
+var gProgressText;
+var gCloseWhenDone;
+
+function progressStartup() {
+ gDownload = window.arguments[0].wrappedJSObject;
+ Downloads.getList(gDownload.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC).then(progressAsyncStartup);
+}
+
+function progressAsyncStartup(aList) {
+ gDlList = aList;
+
+ // cache elements to save .getElementById() calls
+ gDownloadBundle = document.getElementById("dmBundle");
+ gDlStatus = document.getElementById("dlStatus");
+ gDlSize = document.getElementById("dlSize");
+ gTimeLeft = document.getElementById("timeLeft");
+ gProgressMeter = document.getElementById("progressMeter");
+ gProgressText = document.getElementById("progressText");
+ gCloseWhenDone = document.getElementById("closeWhenDone");
+
+ // Insert as first controller on the whole window
+ window.controllers.insertControllerAt(0, ProgressDlgController);
+
+ if (gDownload.isPrivate)
+ gCloseWhenDone.hidden = true;
+ else
+ gCloseWhenDone.checked = Services.prefs.getBoolPref("browser.download.progress.closeWhenDone");
+
+ if (gDownload.succeeded) {
+ if (gCloseWhenDone.checked && !window.arguments[1])
+ window.close();
+ }
+
+ var fName = document.getElementById("fileName");
+ var fSource = document.getElementById("fileSource");
+ fName.label = gDownload.displayName;
+ fName.tooltipText = gDownload.target.path;
+ var uri = Services.io.newURI(gDownload.source.url);
+ var fromString;
+ try {
+ fromString = uri.host;
+ }
+ catch (e) { }
+ if (!fromString)
+ fromString = uri.prePath;
+ fSource.label = gDownloadBundle.getFormattedString("fromSource", [fromString]);
+ fSource.tooltipText = gDownload.source.url;
+
+ // The DlProgressListener handles progress notifications.
+ gDlListener = new DlProgressListener();
+ gDlList.addView(gDlListener);
+
+ updateDownload();
+ updateButtons();
+ window.updateCommands("dlstate-change");
+}
+
+function progressShutdown() {
+ gDlList.removeView(gDlListener);
+ window.controllers.removeController(ProgressDlgController);
+ if (!gCloseWhenDone.hidden)
+ Services.prefs.setBoolPref("browser.download.progress.closeWhenDone",
+ gCloseWhenDone.checked);
+}
+
+function updateDownload() {
+ if (gDownload.hasProgress) {
+ gProgressText.value = gDownloadBundle.getFormattedString("percentFormat",
+ [gDownload.progress]);
+ gProgressText.hidden = false;
+ gProgressMeter.value = gDownload.progress;
+ gProgressMeter.mode = "determined";
+ } else {
+ gProgressText.hidden = true;
+ gProgressMeter.mode = "undetermined";
+ }
+ if (gDownload.stopped) {
+ gProgressMeter.style.opacity = 0.5;
+ } else {
+ gProgressMeter.style.opacity = 1;
+ }
+ // Update window title
+ let statusString = DownloadsCommon.stateOfDownloadText(gDownload);
+
+ if (gDownload.hasProgress) {
+ document.title = gDownloadBundle.getFormattedString("progressTitlePercent",
+ [gDownload.progress,
+ gDownload.displayName,
+ statusString]);
+ }
+ else {
+ document.title = gDownloadBundle.getFormattedString("progressTitle",
+ [gDownload.displayName,
+ statusString]);
+ }
+
+ // download size / transferred bytes
+ gDlSize.value = DownloadsCommon.getTransferredBytes(gDownload);
+
+ // time remaining
+ gTimeLeft.value = DownloadsCommon.getTimeRemaining(gDownload);
+
+ // download status
+ gDlStatus.value = statusString;
+
+}
+
+function updateButtons() {
+ document.getElementById("pauseButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_pause");
+ document.getElementById("resumeButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_resume");
+ document.getElementById("retryButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_retry");
+ document.getElementById("cancelButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_cancel");
+}
+
+/**
+ * DlProgressListener "class" is used to help update download items shown
+ * in the progress dialog such as displaying amount transferred, transfer
+ * rate, and time left for the download.
+ *
+ * This class implements the downloadProgressListener interface.
+ */
+function DlProgressListener() {}
+
+DlProgressListener.prototype = {
+ onDownloadChanged: function(aDownload) {
+ if (aDownload == gDownload) {
+ if (gCloseWhenDone.checked && aDownload.succeeded) {
+ window.close();
+ }
+ updateDownload();
+ updateButtons();
+ window.updateCommands("dlstate-change");
+ }
+ },
+
+ onDownloadRemoved: function(aDownload) {
+ if (aDownload == gDownload)
+ window.close();
+ }
+};
+
+var ProgressDlgController = {
+ supportsCommand: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_pause":
+ case "cmd_resume":
+ case "cmd_retry":
+ case "cmd_cancel":
+ case "cmd_open":
+ case "cmd_show":
+ case "cmd_openReferrer":
+ case "cmd_copyLocation":
+ return true;
+ }
+ return false;
+ },
+
+ isCommandEnabled: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_pause":
+ return !gDownload.stopped && gDownload.hasPartialData;
+ case "cmd_resume":
+ return gDownload.stopped && gDownload.hasPartialData;
+ case "cmd_open":
+ return gDownload.succeeded && gDownload.target.exists;
+ case "cmd_show":
+ return gDownload.target.exists;
+ case "cmd_cancel":
+ return !gDownload.stopped || gDownload.hasPartialData;
+ case "cmd_retry":
+ return !gDownload.succeeded && gDownload.stopped && !gDownload.hasPartialData;
+ case "cmd_openReferrer":
+ return !!gDownload.source.referrer;
+ case "cmd_copyLocation":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_pause":
+ gDownload.cancel();
+ break;
+ case "cmd_resume":
+ case "cmd_retry":
+ gDownload.start();
+ break;
+ case "cmd_cancel":
+ cancelDownload(gDownload);
+ break;
+ case "cmd_open":
+ openDownload(gDownload);
+ break;
+ case "cmd_show":
+ showDownload(gDownload);
+ break;
+ case "cmd_openReferrer":
+ openUILink(gDownload.source.referrer);
+ break;
+ case "cmd_copyLocation":
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(gDownload.source.url);
+ break;
+ }
+ },
+
+ onEvent: function(aEvent) {
+ },
+
+ onCommandUpdate: function() {
+ var cmds = ["cmd_pause", "cmd_resume", "cmd_retry", "cmd_cancel",
+ "cmd_open", "cmd_show", "cmd_openReferrer", "cmd_copyLocation"];
+ for (let command in cmds)
+ goUpdateCommand(cmds[command]);
+ }
+};
diff --git a/comm/suite/components/downloads/content/progressDialog.xul b/comm/suite/components/downloads/content/progressDialog.xul
new file mode 100644
index 0000000000..cb8178f6fd
--- /dev/null
+++ b/comm/suite/components/downloads/content/progressDialog.xul
@@ -0,0 +1,108 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/downloads/downloadmanager.css" type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE window SYSTEM "chrome://communicator/locale/downloads/progressDialog.dtd">
+
+<window id="dlProgressWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="progressStartup();" onunload="progressShutdown();"
+ title="&progress.title;"
+ persist="screenX screenY"
+ style="width:40em;">
+
+ <script src="chrome://communicator/content/downloads/downloadmanager.js"/>
+ <script src="chrome://communicator/content/downloads/progressDialog.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="dmBundle"
+ src="chrome://communicator/locale/downloads/downloadmanager.properties"/>
+ </stringbundleset>
+
+ <commandset id="dlProgressCommands">
+ <commandset id="commandUpdate_DlProgress"
+ commandupdater="true"
+ events="focus,dlstate-change"
+ oncommandupdate="ProgressDlgController.onCommandUpdate();"/>
+
+ <commandset id="downloadCommands">
+ <command id="cmd_pause"
+ oncommand="goDoCommand('cmd_pause');"/>
+ <command id="cmd_resume"
+ oncommand="goDoCommand('cmd_resume');"/>
+ <command id="cmd_retry"
+ oncommand="goDoCommand('cmd_retry');"/>
+ <command id="cmd_cancel"
+ oncommand="goDoCommand('cmd_cancel');"/>
+ <command id="cmd_open"
+ oncommand="goDoCommand('cmd_open');"/>
+ <command id="cmd_show"
+ oncommand="goDoCommand('cmd_show');"/>
+ <command id="cmd_openReferrer"
+ oncommand="goDoCommand('cmd_openReferrer');"/>
+ <command id="cmd_copyLocation"
+ oncommand="goDoCommand('cmd_copyLocation');"/>
+ <command id="cmd_close" oncommand="window.close();"/>
+ </commandset>
+ </commandset>
+
+ <keyset>
+ <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/>
+ <key keycode="VK_ESCAPE" command="cmd_close"/>
+ <key key="." modifiers="meta" command="cmd_close"/>
+ </keyset>
+
+ <hbox align="end">
+ <vbox flex="1" align="start">
+ <button id="fileName" crop="center" label="" type="menu">
+ <menupopup id="file-popup">
+ <menuitem id="dlContext-open"
+ label="&cmd.open.label;"
+ accesskey="&cmd.open.accesskey;"
+ command="cmd_open"/>
+ <menuitem id="dlContext-show"
+ label="&cmd.show.label;"
+ accesskey="&cmd.show.accesskey;"
+ command="cmd_show"/>
+ </menupopup>
+ </button>
+ <button id="fileSource" crop="center" label="" type="menu">
+ <menupopup id="source-popup">
+ <menuitem id="dlContext-openReferrer"
+ label="&cmd.goToDownloadPage.label;"
+ accesskey="&cmd.goToDownloadPage.accesskey;"
+ command="cmd_openReferrer"/>
+ <menuitem id="dlContext-copyLocation"
+ label="&cmd.copyDownloadLink.label;"
+ accesskey="&cmd.copyDownloadLink.accesskey;"
+ command="cmd_copyLocation"/>
+ </menupopup>
+ </button>
+ <label id="dlSize" value=""/>
+ <label id="timeLeft" value=""/>
+ <label id="dlStatus" value=""/>
+ </vbox>
+ <button id="pauseButton" class="mini-button"
+ command="cmd_pause" tooltiptext="&cmd.pause.tooltip;"/>
+ <button id="resumeButton" class="mini-button"
+ command="cmd_resume" tooltiptext="&cmd.resume.tooltip;"/>
+ <button id="retryButton" class="mini-button"
+ command="cmd_retry" tooltiptext="&cmd.retry.tooltip;"/>
+ <button id="cancelButton" class="mini-button"
+ command="cmd_cancel" tooltiptext="&cmd.cancel.tooltip;"/>
+ </hbox>
+ <hbox id="progressBox">
+ <progressmeter id="progressMeter" mode="determined" flex="1"/>
+ <label id="progressText" value=""/>
+ </hbox>
+ <checkbox id="closeWhenDone"
+ label="&closeWhenDone.label;"
+ accesskey="&closeWhenDone.accesskey;"/>
+</window>
diff --git a/comm/suite/components/downloads/content/treeView.js b/comm/suite/components/downloads/content/treeView.js
new file mode 100644
index 0000000000..03e1c48a11
--- /dev/null
+++ b/comm/suite/components/downloads/content/treeView.js
@@ -0,0 +1,483 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+ DownloadHistory: "resource://gre/modules/DownloadHistory.jsm",
+});
+
+function DownloadTreeView() {
+ this._dlList = [];
+ this._searchTerms = [];
+ this.dateTimeFormatter =
+ new Services.intl.DateTimeFormat(undefined,
+ {dateStyle: "short",
+ timeStyle: "long"});
+}
+
+DownloadTreeView.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
+
+ // ***** nsITreeView attributes and methods *****
+ get rowCount() {
+ return this._dlList.length;
+ },
+
+ selection: null,
+
+ getRowProperties: function(aRow) {
+ let dl = this._dlList[aRow];
+ // (in)active
+ let properties = dl.isActive ? "active": "inactive";
+ // resumable
+ if (dl.hasPartialData)
+ properties += " resumable";
+
+ // Download states
+ let state = DownloadsCommon.stateOfDownload(dl);
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ properties += " paused";
+ break;
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ properties += " downloading";
+ break;
+ case DownloadsCommon.DOWNLOAD_FINISHED:
+ properties += " finished";
+ break;
+ case DownloadsCommon.DOWNLOAD_FAILED:
+ properties += " failed";
+ break;
+ case DownloadsCommon.DOWNLOAD_CANCELED:
+ properties += " canceled";
+ break;
+ case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
+ case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY: // Security Zone Policy
+ case DownloadsCommon.DOWNLOAD_DIRTY: // possible virus/spyware
+ properties += " blocked";
+ break;
+ }
+
+ return properties;
+ },
+ getCellProperties: function(aRow, aColumn) {
+ // Append all row properties to the cell
+ return this.getRowProperties(aRow);
+ },
+ getColumnProperties: function(aColumn) { return ""; },
+ isContainer: function(aRow) { return false; },
+ isContainerOpen: function(aRow) { return false; },
+ isContainerEmpty: function(aRow) { return false; },
+ isSeparator: function(aRow) { return false; },
+ isSorted: function() { return false; },
+ canDrop: function(aIdx, aOrientation) { return false; },
+ drop: function(aIdx, aOrientation) { },
+ getParentIndex: function(aRow) { return -1; },
+ hasNextSibling: function(aRow, aAfterIdx) { return false; },
+ getLevel: function(aRow) { return 0; },
+
+ getImageSrc: function(aRow, aColumn) {
+ if (aColumn.id == "Name")
+ return "moz-icon://" + this._dlList[aRow].target.path + "?size=16";
+ return "";
+ },
+
+ getProgressMode: function(aRow, aColumn) {
+ if (aColumn.id == "Progress")
+ return this._dlList[aRow].progressMode;
+ return Ci.nsITreeView.PROGRESS_NONE;
+ },
+
+ getCellValue: function(aRow, aColumn) {
+ if (aColumn.id == "Progress")
+ return this._dlList[aRow].progress;
+ return "";
+ },
+
+ getCellText: function(aRow, aColumn) {
+ let dl = this._dlList[aRow];
+ switch (aColumn.id) {
+ case "Name":
+ return dl.displayName;
+ case "Status":
+ return DownloadsCommon.stateOfDownloadText(dl);
+ case "Progress":
+ if (dl.isActive)
+ return dl.progress;
+ return DownloadsCommon.stateOfDownloadText(dl);
+ case "ProgressPercent":
+ return dl.succeeded ? 100 : dl.progress;
+ case "TimeRemaining":
+ return DownloadsCommon.getTimeRemaining(dl);
+ case "Transferred":
+ return DownloadsCommon.getTransferredBytes(dl);
+ case "TransferRate":
+ let state = DownloadsCommon.stateOfDownload(dl);
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ let [rate, unit] = DownloadUtils.convertByteUnits(dl.speed);
+ return this._dlbundle.getFormattedString("speedFormat", [rate, unit]);
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ return this._dlbundle.getString("statePaused");
+ case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+ return this._dlbundle.getString("stateNotStarted");
+ }
+ return "";
+ case "TimeElapsed":
+ // With no end time persisted in the downloads backend this is
+ // utterly useless unless the download is progressing.
+ if (DownloadsCommon.stateOfDownload(dl) ==
+ DownloadsCommon.DOWNLOAD_DOWNLOADING && dl.startTime) {
+ let seconds = (Date.now() - dl.startTime) / 1000;
+ let [time1, unit1, time2, unit2] =
+ DownloadUtils.convertTimeUnits(seconds);
+ if (seconds < 3600 || time2 == 0) {
+ return this._dlbundle.getFormattedString("timeSingle", [time1, unit1]);
+ }
+ return this._dlbundle.getFormattedString("timeDouble", [time1, unit1, time2, unit2]);
+ }
+ return "";
+ case "StartTime":
+ if (dl.startTime) {
+ return this.dateTimeFormatter.format(dl.startTime);
+ }
+ return "";
+ case "EndTime":
+ // This might end with an exception if it is an unsupported uri
+ // scheme.
+ let metaData = DownloadHistory.getPlacesMetaDataFor(dl.source.url);
+
+ if (metaData.endTime) {
+ return this.dateTimeFormatter.format(metaData.endTime);
+ }
+ return "";
+ case "Source":
+ return dl.source.url;
+ }
+ return "";
+ },
+
+ setTree: function(aTree) {
+ this._tree = aTree;
+ this._dlbundle = document.getElementById("dmBundle");
+ },
+
+ toggleOpenState: function(aRow) { },
+ cycleHeader: function(aColumn) { },
+ selectionChanged: function() { },
+ cycleCell: function(aRow, aColumn) {
+ var dl = this._dlList[aRow];
+ switch (aColumn.id) {
+ case "ActionPlay":
+ if (dl.stopped) {
+ if (!dl.succeeded)
+ dl.start();
+ } else {
+ if (dl.hasPartialData)
+ dl.cancel();
+ }
+ break;
+ case "ActionStop":
+ if (dl.isActive)
+ cancelDownload(dl);
+ else
+ removeDownload(dl);
+ break;
+ }
+ },
+ isEditable: function(aRow, aColumn) { return false; },
+ isSelectable: function(aRow, aColumn) { return false; },
+ setCellValue: function(aRow, aColumn, aText) { },
+ setCellText: function(aRow, aColumn, aText) { },
+
+ // ***** local public methods *****
+
+ addDownload: function(aDownload) {
+ aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE;
+ aDownload.lastSec = Infinity;
+ let state = DownloadsCommon.stateOfDownload(aDownload);
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ aDownload.endTime = Date.now();
+ // At this point, we know if we are an indeterminate download or not.
+ aDownload.progressMode = aDownload.hasProgress ?
+ Ci.nsITreeView.PROGRESS_UNDETERMINED :
+ Ci.nsITreeView.PROGRESS_NORMAL;
+ case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ aDownload.isActive = 1;
+ break;
+ default:
+ aDownload.isActive = 0;
+ break;
+ }
+
+ // prepend in natural sorting
+ aDownload.listIndex = this._lastListIndex--;
+
+ // Prepend data to the download list
+ this._dlList.unshift(aDownload);
+
+ // Tell the tree we added 1 row at index 0
+ this._tree.rowCountChanged(0, 1);
+
+ // Data has changed, so re-sorting might be needed
+ this.sortView("", "", aDownload, 0);
+
+ window.updateCommands("tree-select");
+ },
+
+ updateDownload: function(aDownload) {
+ var row = this._dlList.indexOf(aDownload);
+ if (row == -1) {
+ // No download row found to update, but as it's obviously going on,
+ // add it to the list now (can happen with very fast, e.g. local dls)
+ this.onDownloadAdded(aDownload);
+ return;
+ }
+ let state = DownloadsCommon.stateOfDownload(aDownload);
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ // At this point, we know if we are an indeterminate download or not.
+ aDownload.progressMode = aDownload.hasProgress ?
+ Ci.nsITreeView.PROGRESS_NORMAL : Ci.nsITreeView.PROGRESS_UNDETERMINED;
+ case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ aDownload.isActive = 1;
+ break;
+ default:
+ aDownload.isActive = 0;
+ aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE;
+ // This preference may not be set, so defaulting to two.
+ var flashCount = 2;
+ try {
+ flashCount = Services.prefs.getIntPref(PREF_FLASH_COUNT);
+ } catch (e) { }
+ getAttentionWithCycleCount(flashCount);
+ break;
+ }
+
+ // Repaint the tree row
+ this._tree.invalidateRow(row);
+
+ // Data has changed, so re-sorting might be needed
+ this.sortView("", "", aDownload, row);
+
+ window.updateCommands("tree-select");
+ },
+
+ removeDownload: function(aDownload) {
+ var row = this._dlList.indexOf(aDownload);
+ // Make sure we have an item to remove
+ if (row == -1)
+ return;
+
+ var index = this.selection.currentIndex;
+ var wasSingleSelection = this.selection.count == 1;
+
+ // Remove data from the download list
+ this._dlList.splice(row, 1);
+
+ // Tell the tree we removed 1 row at the given row index
+ this._tree.rowCountChanged(row, -1);
+
+ // Update selection if only removed download was selected
+ if (wasSingleSelection && this.selection.count == 0) {
+ index = Math.min(index, this.rowCount - 1);
+ if (index >= 0)
+ this.selection.select(index);
+ }
+
+ window.updateCommands("tree-select");
+ },
+
+ searchView: function(aInput) {
+ // Stringify the previous search
+ var prevSearch = this._searchTerms.join(" ");
+
+ // Array of space-separated lower-case search terms
+ this._searchTerms = aInput.trim().toLowerCase().split(/\s+/);
+
+ // Don't rebuild the download list if the search didn't change
+ if (this._searchTerms.join(" ") == prevSearch)
+ return;
+
+ // Cache the current selection
+ this._cacheSelection();
+
+ // Rebuild the tree with set search terms
+ //this.initTree();
+
+ // Restore the selection
+ this._restoreSelection();
+ },
+
+ sortView: function(aColumnID, aDirection, aDownload, aRow) {
+ var sortAscending = aDirection != "descending";
+
+ if (aColumnID == "" && aDirection == "") {
+ // Re-sort in already selected/cached order
+ var sortedColumn = this._tree.columns.getSortedColumn();
+ if (sortedColumn) {
+ aColumnID = sortedColumn.id;
+ sortAscending = sortedColumn.element.getAttribute("sortDirection") != "descending";
+ }
+ // no need for else, use default case of switch, sortAscending is true
+ }
+
+ // Compare function for two _dlList items
+ var compfunc = function(a, b) {
+ // Active downloads are always at the beginning
+ // i.e. 0 for .isActive is larger (!) than 1
+ if (a.isActive < b.isActive)
+ return 1;
+ if (a.isActive > b.isActive)
+ return -1;
+ // Same active/inactive state, sort normally
+ var comp_a = null;
+ var comp_b = null;
+ switch (aColumnID) {
+ case "Name":
+ comp_a = a.displayName.toLowerCase();
+ comp_b = b.displayName.toLowerCase();
+ break;
+ case "Status":
+ comp_a = DownloadsCommon.stateOfDownload(a);
+ comp_b = DownloadsCommon.stateOfDownload(b);
+ break;
+ case "Progress":
+ case "ProgressPercent":
+ // Use original sorting for inactive entries
+ // Use only one isActive to be sure we do the same
+ comp_a = a.isActive ? a.progress : a.listIndex;
+ comp_b = a.isActive ? b.progress : b.listIndex;
+ break;
+ case "TimeRemaining":
+ comp_a = a.isActive ? a.lastSec : a.listIndex;
+ comp_b = a.isActive ? b.lastSec : b.listIndex;
+ break;
+ case "Transferred":
+ comp_a = a.currentBytes;
+ comp_b = b.currentBytes;
+ break;
+ case "TransferRate":
+ comp_a = a.isActive ? a.speed : a.listIndex;
+ comp_b = a.isActive ? b.speed : b.listIndex;
+ break;
+ case "TimeElapsed":
+ comp_a = (a.endTime && a.startTime && (a.endTime > a.startTime))
+ ? a.endTime - a.startTime
+ : 0;
+ comp_b = (b.endTime && b.startTime && (b.endTime > b.startTime))
+ ? b.endTime - b.startTime
+ : 0;
+ break;
+ case "StartTime":
+ comp_a = a.startTime;
+ comp_b = b.startTime;
+ break;
+ case "EndTime":
+ comp_a = a.endTime;
+ comp_b = b.endTime;
+ break;
+ case "Source":
+ comp_a = a.source.url;
+ comp_b = b.source.url;
+ break;
+ case "unsorted": // Special case for reverting to original order
+ default:
+ comp_a = a.listIndex;
+ comp_b = b.listIndex;
+ }
+ if (comp_a > comp_b)
+ return sortAscending ? 1 : -1;
+ if (comp_a < comp_b)
+ return sortAscending ? -1 : 1;
+ return 0;
+ }
+
+ // Cache the current selection
+ this._cacheSelection();
+
+ // Do the actual sorting of the array
+ this._dlList.sort(compfunc);
+
+ var row = this._dlList.indexOf(aDownload);
+ if (row == -1)
+ // Repaint the tree
+ this._tree.invalidate();
+ else if (row == aRow)
+ // No effect
+ this._selectionCache = null;
+ else if (row < aRow)
+ // Download moved up from aRow to row
+ this._tree.invalidateRange(row, aRow);
+ else
+ // Download moved down from aRow to row
+ this._tree.invalidateRange(aRow, row)
+
+ // Restore the selection
+ this._restoreSelection();
+ },
+
+ getRowData: function(aRow) {
+ return this._dlList[aRow];
+ },
+
+ getActiveDownloads: function() {
+ return this._dlList.filter(dld => !dld.stopped);
+ },
+
+ // ***** local member vars *****
+
+ _tree: null,
+ _dlBundle: null,
+ _lastListIndex: 0,
+ _selectionCache: null,
+
+ // ***** local helper functions *****
+
+ // Cache IDs of selected downloads for later restoration
+ _cacheSelection: function() {
+ // Abort if there's already something cached
+ if (this._selectionCache)
+ return;
+
+ this._selectionCache = [];
+ if (this.selection.count < 1)
+ return;
+
+ // Walk all selected rows and cache their download IDs
+ var start = {};
+ var end = {};
+ var numRanges = this.selection.getRangeCount();
+ for (let rg = 0; rg < numRanges; rg++){
+ this.selection.getRangeAt(rg, start, end);
+ for (let row = start.value; row <= end.value; row++){
+ this._selectionCache.push(this._dlList[row]);
+ }
+ }
+ },
+
+ // Restore selection from cached IDs (as possible)
+ _restoreSelection: function() {
+ // Abort if the cache is empty
+ if (!this._selectionCache)
+ return;
+
+ this.selection.clearSelection();
+ for (let dl of this._selectionCache) {
+ // Find out what row this is now and if possible, add it to the selection
+ var row = this._dlList.indexOf(dl);
+ if (row != -1)
+ this.selection.rangedSelect(row, row, true);
+ }
+ // Work done, clear the cache
+ this._selectionCache = null;
+ },
+};
diff --git a/comm/suite/components/downloads/content/uploadProgress.js b/comm/suite/components/downloads/content/uploadProgress.js
new file mode 100644
index 0000000000..0cd4d27817
--- /dev/null
+++ b/comm/suite/components/downloads/content/uploadProgress.js
@@ -0,0 +1,189 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+
+const kInterval = 750; // Default to .75 seconds.
+
+var gPersist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+var gSource = window.arguments[0].QueryInterface(Ci.nsIFileURL);
+var gTarget = window.arguments[1].QueryInterface(Ci.nsIURL);
+var gFileName = gSource.file.leafName;
+var gFileSize = gSource.file.fileSize;
+var gPercent = -1;
+var gStartTime;
+var gLastUpdate;
+var gLastSeconds;
+var gBundle;
+var gStatus;
+var gTime;
+var gSize;
+var gProgress;
+var gMeter;
+
+function onLoad()
+{
+ gBundle = document.getElementById("dmBundle");
+ gStatus = document.getElementById("status");
+ gTime = document.getElementById("timeElapsed");
+ gSize = document.getElementById("size");
+ gProgress = document.getElementById("progressText");
+ gMeter = document.getElementById("progress");
+ var status = gBundle.getString("stateNotStarted");
+ document.title =
+ gBundle.getFormattedString("progressTitle", [gFileName, status]);
+ gStatus.value = status;
+ gTime.value = gBundle.getFormattedString("timeSingle",
+ DownloadUtils.convertTimeUnits(0));
+ gSize.value = DownloadUtils.getTransferTotal(0, gFileSize);
+ document.getElementById("target").value =
+ gBundle.getFormattedString("toTarget", [gTarget.resolve(".")]);
+ document.getElementById("source").value =
+ gBundle.getFormattedString("fromSource", [gSource.file.leafName]);
+ gPersist.progressListener = gProgressListener;
+ gPersist.saveURI(gSource, null, null, 0, null, null, gTarget, null);
+ document.documentElement.getButton("cancel").focus();
+}
+
+function onUnload()
+{
+ if (gPersist)
+ gPersist.cancel(Cr.NS_BINDING_ABORTED);
+ gPersist = null;
+}
+
+function setPercent(aPercent, aStatus)
+{
+ gPercent = aPercent;
+ document.title = gBundle.getFormattedString("progressTitlePercent",
+ [aPercent, gFileName, aStatus]);
+ gProgress.value = gBundle.getFormattedString("percentFormat", [aPercent]);
+ gMeter.mode = "normal";
+ gMeter.value = aPercent;
+}
+
+var gProgressListener = {
+ // ----- nsIWebProgressListener methods -----
+
+ // Look for STATE_STOP and close dialog to indicate completion when it happens.
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aRequest instanceof Ci.nsIChannel &&
+ aRequest.URI.equals(gTarget) &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ gPersist = null;
+ var status = gBundle.getString("stateCompleted");
+ setPercent(100, status);
+ gStatus.value = status;
+ gSize.value = DownloadUtils.getTransferTotal(gFileSize, gFileSize);
+ setTimeout(window.close, kInterval);
+ }
+ },
+
+ // Handle progress notifications.
+ onProgressChange: function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ return this.onProgressChange64(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress);
+ },
+
+ onProgressChange64: function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ if (aRequest instanceof Ci.nsIChannel &&
+ aRequest.URI.equals(gTarget)) {
+ // Get current time.
+ var now = Date.now();
+
+ // If interval hasn't elapsed, ignore it.
+ if (!gStartTime)
+ gStartTime = now;
+ else if (now - gLastUpdate < kInterval && aCurTotalProgress < gFileSize)
+ return;
+
+ // Update this time.
+ gLastUpdate = now;
+
+ // Update elapsed time.
+ var elapsed = (now - gStartTime) / 1000;
+
+ // Calculate percentage.
+ var status = gBundle.getString("stateUploading");
+ var percent = -1;
+ if (gFileSize > 0)
+ percent = Math.floor(aCurTotalProgress * 100 / gFileSize);
+ if (percent != gPercent)
+ setPercent(percent, status);
+
+ // Update time remaining.
+ var rate = elapsed && aCurTotalProgress / elapsed;
+ if (rate && gFileSize) {
+ var timeLeft;
+ [timeLeft, gLastSeconds] =
+ DownloadUtils.getTimeLeft((gFileSize - aCurTotalProgress) / rate,
+ gLastSeconds);
+ status = gBundle.getFormattedString("statusActive", [status, timeLeft]);
+ }
+ gStatus.value = status;
+
+ // Update dialog's display of elapsed time.
+ var timeUnits = DownloadUtils.convertTimeUnits(elapsed);
+ var timeString = timeUnits[2] ? "timeDouble" : "timeSingle";
+ gTime.value = gBundle.getFormattedString(timeString, timeUnits);
+
+ // Update size (nn KB of mm KB at xx.x KB/sec)
+ var size = DownloadUtils.getTransferTotal(aCurTotalProgress, gFileSize);
+ if (elapsed)
+ size = gBundle.getFormattedString("sizeSpeed", [size,
+ gBundle.getFormattedString("speedFormat",
+ DownloadUtils.convertByteUnits(rate))]);
+ gSize.value = size;
+ }
+ },
+
+ // Look for error notifications and display alert to user.
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {
+ // Check for error condition (only if dialog is still open).
+ if (!Cr.isSuccessCode(aStatus)) {
+ // Display error alert (using text supplied by back-end).
+ Services.prompt.alert(window, document.title, aMessage);
+ // Close the dialog.
+ window.close();
+ }
+ },
+
+ // Ignore onLocationChange and onSecurityChange notifications.
+ onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) {
+ },
+
+ onSecurityChange: function( aWebProgress, aRequest, aState ) {
+ },
+
+ // ---------- nsISupports methods ----------
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener2,
+ Ci.nsIWebProgressListener,
+ Ci.nsIInterfaceRequestor]),
+
+ // ---------- nsIInterfaceRequestor methods ----------
+
+ getInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIPrompt) ||
+ aIID.equals(Ci.nsIAuthPrompt)) {
+ var prompt;
+ if (aIID.equals(Ci.nsIPrompt))
+ prompt = Services.ww.getNewPrompter(window);
+ else
+ prompt = Services.ww.getNewAuthPrompter(window);
+ return prompt;
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
diff --git a/comm/suite/components/downloads/content/uploadProgress.xul b/comm/suite/components/downloads/content/uploadProgress.xul
new file mode 100644
index 0000000000..43e95d5432
--- /dev/null
+++ b/comm/suite/components/downloads/content/uploadProgress.xul
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE dialog>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="cancel"
+ onload="onLoad();"
+ onunload="onUnload();"
+ style="width: 40em;">
+
+ <script src="chrome://communicator/content/downloads/uploadProgress.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="dmBundle"
+ src="chrome://communicator/locale/downloads/downloadmanager.properties"/>
+ </stringbundleset>
+
+ <label id="source" value="" crop="center"/>
+ <label id="target" value="" crop="center"/>
+ <label id="size" value=""/>
+ <label id="timeElapsed" value=""/>
+ <label id="status" value=""/>
+ <hbox>
+ <progressmeter id="progress" mode="undetermined" value="0" flex="1"/>
+ <label id="progressText" value="" style="width: 4ch; text-align: right;"/>
+ </hbox>
+</dialog>
diff --git a/comm/suite/components/downloads/jar.mn b/comm/suite/components/downloads/jar.mn
new file mode 100644
index 0000000000..9abbb0cd7b
--- /dev/null
+++ b/comm/suite/components/downloads/jar.mn
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+% content communicator %content/communicator/ contentaccessible=yes
+ content/communicator/downloads/downloadmanager.js (content/downloadmanager.js)
+ content/communicator/downloads/downloadmanager.xul (content/downloadmanager.xul)
+ content/communicator/downloads/DownloadProgressListener.js (content/DownloadProgressListener.js)
+ content/communicator/downloads/progressDialog.xul (content/progressDialog.xul)
+ content/communicator/downloads/progressDialog.js (content/progressDialog.js)
+ content/communicator/downloads/uploadProgress.xul (content/uploadProgress.xul)
+ content/communicator/downloads/uploadProgress.js (content/uploadProgress.js)
+ content/communicator/downloads/treeView.js (content/treeView.js)
diff --git a/comm/suite/components/downloads/moz.build b/comm/suite/components/downloads/moz.build
new file mode 100644
index 0000000000..cc6cc0f1d6
--- /dev/null
+++ b/comm/suite/components/downloads/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+EXTRA_JS_MODULES += [
+ "DownloadsCommon.jsm",
+ "DownloadsTaskbar.jsm",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("SeaMonkey", "Downloads")
diff --git a/comm/suite/components/downloads/tests/chrome/chrome.ini b/comm/suite/components/downloads/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..dd5f9fc31f
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/chrome.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+
+[test_action_keys_respect_focus.xul]
+[test_basic_functionality.xul]
+[test_cleanup_search.xul]
+[test_clear_button_disabled.xul]
+[test_close_download_manager.xul]
+[test_delete_key_cancels.xul]
+[test_delete_key_removes.xul]
+[test_drag.xul]
+[test_enter_dblclick_opens.xul]
+[test_multi_select.xul]
+[test_multiword_search.xul]
+[test_open_properties.xul]
+[test_removeDownload_updates_ui.xul]
+[test_search_clearlist.xul]
+[test_search_keys.xul]
+[test_select_all.xul]
+[test_space_key_pauses_resumes.xul]
+[test_space_key_retries.xul]
+[test_ui_stays_open_on_alert_clickback.xul]
diff --git a/comm/suite/components/downloads/tests/chrome/test_action_keys_respect_focus.xul b/comm/suite/components/downloads/tests/chrome/test_action_keys_respect_focus.xul
new file mode 100644
index 0000000000..765e0a3a9c
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_action_keys_respect_focus.xul
@@ -0,0 +1,376 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jens Hatlak <jh@junetz.de> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test for bug 474622 to check that action keys (Del, Backspace, Return)
+ * respect focus, i.e. work as expected in the Search field, Clear List button
+ * and download list.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+var openInvokeCount = 0;
+var removeInvokeCount = 0;
+var resumeInvokeCount = 0;
+var testedFunctions = {
+ openDownload : null,
+ removeDownload : null,
+ resumeDownload : null
+};
+
+function getCounter(aFn)
+{
+ switch (aFn) {
+ case "openDownload":
+ return () => openInvokeCount++;
+ case "removeDownload":
+ return () => removeInvokeCount++;
+ case "resumeDownload":
+ return () => resumeInvokeCount++;
+ }
+}
+
+function backupTestedFunction(aFn, aWin)
+{
+ ok(true, "(info) backupTestedFunction('" + aFn + "')");
+
+ [testedFunctions[aFn], aWin[aFn]] = [aWin[aFn], getCounter(aFn)];
+}
+function restoreTestedFunction(aFn, aWin)
+{
+ aWin[aFn] = testedFunctions[aFn];
+ testedFunctions[aFn] = null;
+
+ ok(true, "(info) restoreTestedFunction('" + aFn + "')");
+}
+
+function keyPressObs(aWin, aKey)
+{
+ this.mWin = aWin;
+ this.mKey = aKey;
+}
+keyPressObs.prototype = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if ("timer-callback" == aTopic)
+ synthesizeKey(this.mKey, {}, this.mWin);
+ }
+};
+var searchAndPressKey = function(aKey, aWin, aValue) {
+ var searchbox = aWin.document.getElementById("search-box");
+ searchbox.focus();
+ if (aValue != null)
+ searchbox.value = aValue;
+
+ // Press given key after a short delay to allow focus() to complete
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(new keyPressObs(aWin, aKey), 500,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+function dlObs(aWin)
+{
+ this.mWin = aWin;
+ this.wasPaused = false;
+ this.wasResumed = false;
+ this.wasFinished = false;
+}
+dlObs.prototype = {
+ onDownloadStateChange: function(aState, aDownload)
+ {
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING &&
+ !this.wasPaused)
+ {
+ this.wasPaused = true;
+ this.mWin.pauseDownload(aDownload.id);
+ return;
+ }
+
+ var searchbox = this.mWin.document.getElementById("search-box");
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_PAUSED &&
+ !this.wasResumed)
+ {
+ this.wasResumed = true;
+
+ // Fill Search with an added space (test continues in testObs)
+ backupTestedFunction("resumeDownload", this.mWin);
+ searchAndPressKey(" ", this.mWin, "paused");
+ } else
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
+ !this.wasFinished)
+ {
+ this.wasFinished = true;
+
+ // The formerly paused download was resumed successfully, is now complete
+ // and still selected. Since it is a real download it can be opened.
+
+ // Init Search (test continues in testObs)
+ backupTestedFunction("openDownload", this.mWin);
+ searchAndPressKey("VK_RETURN", this.mWin, "delete me");
+
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ dm.removeListener(this);
+ }
+ },
+ onStateChange: function(a, b, c, d, e) { },
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onSecurityChange: function(a, b, c, d) { }
+};
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ function addDownload()
+ {
+ function createURI(aObj)
+ {
+ return (aObj instanceof Ci.nsIFile) ? Services.io.newFileURI(aObj) :
+ Services.io.newURI(aObj);
+ }
+
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ var destFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ // The "paused" part of this filename will be searched for later.
+ destFile.append("download.paused");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var dl = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
+ createURI("http://example.com/httpd.js"),
+ createURI(destFile), null, null,
+ Math.round(Date.now() * 1000), null, persist, false);
+
+ persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);
+ persist.saveURI(dl.source, null, null, 0, null, null, dl.targetFile, null);
+
+ return dl;
+ }
+
+ // Empty any old downloads
+ dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // Make a file name for the downloads
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("cleanUp");
+ var filePath = Services.io.newFileURI(file).spec;
+
+ var stmt = dm.DBConnection.createStatement(
+ "INSERT INTO moz_downloads (name, target, source, state) " +
+ "VALUES (?1, ?2, ?3, ?4)");
+
+ try {
+ for (let site of ["delete.me", "i.live"]) {
+ stmt.bindByIndex(0, "Finished Download");
+ stmt.bindByIndex(1, filePath);
+ stmt.bindByIndex(2, "http://" + site + "/file");
+ stmt.bindByIndex(3, dm.DOWNLOAD_FINISHED);
+
+ // Add it!
+ stmt.execute();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+ const IS_MAC = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime)
+ .OS == "Darwin";
+
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ var downloadTree = win.document.getElementById("downloadTree");
+ var searchbox = win.document.getElementById("search-box");
+ var clearList = win.document.getElementById("clearListButton");
+
+ // The list must have built, so figure out what test to do
+ switch (testPhase++) {
+ case 0:
+ // Init Search
+ searchbox.value = "delete me";
+ searchbox.doCommand();
+
+ break;
+ case 1:
+ // Clear Search
+ backupTestedFunction("removeDownload", win);
+ searchAndPressKey("VK_DELETE", win);
+
+ break;
+ case 2:
+ is(removeInvokeCount, 0, "Search box: Del didn't remove download");
+
+ // Search has been cleared, init again
+ searchbox.value = "live";
+ searchbox.doCommand();
+
+ break;
+ case 3:
+ // Clear Search
+ searchAndPressKey("VK_BACK_SPACE", win);
+
+ break;
+ case 4:
+ is(removeInvokeCount, 0, "Search box: Backspace didn't remove download");
+ restoreTestedFunction("removeDownload", win);
+
+ // Add paused download (test continues in dlObs)
+ dm.addListener(new dlObs(win));
+ addDownload();
+
+ break;
+ case 5:
+ // Back from dlObs
+ is(resumeInvokeCount, 0, "Search box: Space didn't resume download");
+
+ // Focus download tree and select first (paused) download
+ downloadTree.focus();
+ downloadTree.view.selection.select(0);
+
+ // Simulate Resume download
+ synthesizeKey(" ", {}, win);
+ is(resumeInvokeCount, 1, "Download list: Space resumed download");
+
+ // Resume download for real (test continues in dlObs)
+ restoreTestedFunction("resumeDownload", win);
+ synthesizeKey(" ", {}, win);
+
+ break;
+ case 6:
+ // Back from dlObs
+ is(openInvokeCount, 0, "Search box: Return didn't open download");
+
+ // Search has been changed, init again to get formerly paused download
+ searchbox.value = "paused";
+ searchbox.doCommand();
+
+ break;
+ case 7:
+ // Focus download tree and select first (formerly paused) download
+ downloadTree.focus();
+ downloadTree.view.selection.select(0);
+
+ // Simulate Open download
+ synthesizeKey("VK_RETURN", {}, win);
+ is(openInvokeCount, 1, "Download list: Return opened download");
+
+ // Clear List: Return (execute Clear List)
+ // MacOSX: VK_RETURN doesn't work on this button (See bug 506850).
+ if (IS_MAC)
+ // Workaround not to time out.
+ clearList.doCommand();
+ else {
+ clearList.focus();
+ synthesizeKey("VK_RETURN", {}, win);
+ }
+
+ break;
+ case 8:
+ if (IS_MAC)
+ is(openInvokeCount, 1, "Clear List: doCommand() didn't open download (MacOSX)");
+ else
+ is(openInvokeCount, 1, "Clear List: Enter didn't open download (Linux, Windows)");
+ restoreTestedFunction("openDownload", win);
+
+ // We're done here
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ win.close();
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_basic_functionality.xul b/comm/suite/components/downloads/tests/chrome/test_basic_functionality.xul
new file mode 100644
index 0000000000..ffedc65227
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_basic_functionality.xul
@@ -0,0 +1,281 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Make sure the download manager can display downloads in the right order and
+ * contains the expected data. The list has one of each download state ordered
+ * by the start/end times.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+var dmFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+dmFile.append("dm-ui-test.file");
+dmFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var gTestPath = Services.io.newFileURI(dmFile).spec;
+
+// Downloads are sorted by endTime, so make sure the end times are distinct
+const DownloadData = [
+ /* Active states first */
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859238,
+ state: Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859237,
+ state: Ci.nsIDownloadManager.DOWNLOAD_PAUSED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859236,
+ state: Ci.nsIDownloadManager.DOWNLOAD_SCANNING,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859235,
+ state: Ci.nsIDownloadManager.DOWNLOAD_QUEUED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ /* Finished states */
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859234,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859233,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FAILED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859232,
+ state: Ci.nsIDownloadManager.DOWNLOAD_CANCELED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859231,
+ state: Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859230,
+ state: Ci.nsIDownloadManager.DOWNLOAD_DIRTY,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859229,
+ endTime: 1180493839859229,
+ state: Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+function test_numberOfTreeItems(aWin)
+{
+ var doc = aWin.document;
+ var dlTree = doc.getElementById("downloadTree");
+ is(dlTree.view.rowCount, DownloadData.length,
+ "There is the correct number of tree items");
+}
+
+function test_properDownloadData(aWin)
+{
+ // This also tests the ordering of the display
+ var doc = aWin.document;
+ var dlTree = doc.getElementById("downloadTree");
+ var view = dlTree.view;
+ var colName = dlTree.columns.getNamedColumn("Name");
+ var colState = dlTree.columns.getNamedColumn("Status");
+ var colTarget = dlTree.columns.getNamedColumn("Name");
+ var colSource = dlTree.columns.getNamedColumn("Source");
+ var stateString;
+ var statusBar = doc.getElementById("statusbar-display");
+
+ for (var i = 0; i < view.rowCount; i++) {
+ view.selection.select(i);
+ is(view.getCellText(i, colName), DownloadData[i].name,
+ "Download names match up");
+ switch (DownloadData[i].state) {
+ case Ci.nsIDownloadManager.DOWNLOAD_PAUSED:
+ stateString = "Paused";
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+ stateString = "Downloading";
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
+ stateString = "Finished";
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
+ stateString = "Failed";
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
+ stateString = "Canceled";
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
+ case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY: // Security Zone Policy
+ case Ci.nsIDownloadManager.DOWNLOAD_DIRTY: // possible virus/spyware
+ stateString = "Blocked";
+ break;
+ default:
+ stateString = "Not Started";
+ break;
+ }
+ is(view.getCellText(i, colState), stateString,
+ "Download states match up");
+
+ var filePath = Services.io.newURI(DownloadData[i].target)
+ .QueryInterface(Ci.nsIFileURL)
+ .file.clone()
+ .QueryInterface(Ci.nsIFile)
+ .path;
+ is(statusBar.label, filePath,
+ "Download targets match up");
+ is(view.getCellText(i, colSource), DownloadData[i].source,
+ "Download sources match up");
+ }
+}
+
+var testFuncs = [
+ test_numberOfTreeItems
+ , test_properDownloadData
+];
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // First, we populate the database with some fake data
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " +
+ "state, currBytes, maxBytes, preferredAction, autoResume) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, " +
+ ":currBytes, :maxBytes, :preferredAction, :autoResume)");
+ for (let dl of DownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ stmt.finalize();
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+
+ // Now we can run our tests
+ for (let t of testFuncs)
+ t(win);
+
+ win.close();
+ dmFile.remove(false);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+ };
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_cleanup_search.xul b/comm/suite/components/downloads/tests/chrome/test_cleanup_search.xul
new file mode 100644
index 0000000000..37394168dc
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_cleanup_search.xul
@@ -0,0 +1,172 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test for bug 414850 to make sure only downloads that are shown when
+ * searching are cleared and afterwards, the default list is shown.
+ *
+ * Test bug 430486 to make sure the Clear list button is disabled only when
+ * there are no download items visible.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // Make a file name for the downloads
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("cleanUp");
+ var filePath = Services.io.newFileURI(file).spec;
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, target, source, state) " +
+ "VALUES (?1, ?2, ?3, ?4)");
+
+ try {
+ for (let site of ["delete.me", "i.live"]) {
+ stmt.bindByIndex(0, "Super Pimped Download");
+ stmt.bindByIndex(1, filePath);
+ stmt.bindByIndex(2, "http://" + site + "/file");
+ stmt.bindByIndex(3, dm.DOWNLOAD_FINISHED);
+
+ // Add it!
+ stmt.execute();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+ var downloadView = win.document.getElementById("downloadTree").view;
+ var searchbox = win.document.getElementById("search-box");
+ var clearList = win.document.getElementById("clearListButton");
+
+ // The list must have built, so figure out what test to do
+ switch (testPhase++) {
+ case 0:
+ // Make sure the button is initially enabled
+ is(clearList.disabled, false, "Clear list is enabled for default 2 item view");
+
+ // Search for multiple words in any order in all places
+ searchbox.value = "delete me";
+ searchbox.doCommand();
+
+ break;
+ case 1:
+ // Search came back with 1 item
+ is(downloadView.rowCount, 1, "Search found the item to delete");
+ is(clearList.disabled, false, "Clear list is enabled for search matching 1 item");
+
+ // Clear the list that has the single matched item
+ clearList.doCommand();
+
+ break;
+ case 2:
+ // Done rebuilding with one item left
+ is(downloadView.rowCount, 1, "Clear list rebuilt the list with one");
+ is(clearList.disabled, false, "Clear list still enabled for 1 item in default view");
+
+ // Clear the whole list
+ clearList.doCommand();
+
+ break;
+ case 3:
+ // There's nothing left
+ is(downloadView.rowCount, 0, "Clear list killed everything");
+ is(clearList.disabled, true, "Clear list is disabled for no items");
+
+ // We're done!
+ win.close();
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+ };
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_clear_button_disabled.xul b/comm/suite/components/downloads/tests/chrome/test_clear_button_disabled.xul
new file mode 100644
index 0000000000..0b713a31c3
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_clear_button_disabled.xul
@@ -0,0 +1,201 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Anoop Saldanha <poonaatsoc@gmail.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This tests 437422. This test basically intends to checks if the clear list
+ * button is disabled when:
+ * 1. an invalid search string has been entered into the search box.
+ * 2. active downloads are present in the dm ui
+ * 3. we have both case (1) and (2)
+ */
+-->
+
+<window title="Download Manager Test"
+ onload="runTest();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+const nsIDownloadManager = Ci.nsIDownloadManager;
+var dmFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+dmFile.append("dm-ui-test.file");
+dmFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var gTestPath = Services.io.newFileURI(dmFile).spec;
+
+const DoneDownloadData = [
+ { name: "Dead",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: nsIDownloadManager.DOWNLOAD_CANCELED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+const ActiveDownloadData = [
+ { name: "Patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: nsIDownloadManager.DOWNLOAD_DOWNLOADING,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+function runTest()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " +
+ "state, currBytes, maxBytes, preferredAction, autoResume) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, " +
+ ":currBytes, :maxBytes, :preferredAction, :autoResume)");
+ for (let dl of DoneDownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ //stmt.finalize();
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ var doc = aSubject.document;
+ var searchbox = doc.getElementById("search-box");
+ var clearButton = doc.getElementById("clearListButton");
+
+ switch (testPhase++) {
+ case 0:
+ // Ensure that the clear list button is enabled at first
+ ok(!clearButton.disabled,
+ "The clear list button is not disabled initially.");
+
+ // Now, insert an nonsensical search string - nothing should show up,
+ // and the button should be disabled in the next test phase
+ searchbox.value = "Nonsensical";
+ searchbox.doCommand();
+
+ break;
+ case 1:
+ ok(clearButton.disabled,
+ "The clear list button is disabled with a nonsensical search " +
+ "term entered");
+
+ // Clear the search box
+ searchbox.value = "";
+ searchbox.doCommand();
+ break;
+
+ case 2:
+ // Populate the download manager with an active download now, and
+ // rebuild the list
+ stmt.reset();
+ for (let dl of ActiveDownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ stmt.finalize();
+ dm.cleanUp();
+
+ break;
+ case 3:
+ ok(clearButton.disabled,
+ "The clear list button is disabled when we only have an active " +
+ "download");
+
+ // Now, insert an nonsensical search string - only the active download
+ // should show up, and the button should be disabled in the next test
+ // phase
+ searchbox.value = "Nonsensical";
+ searchbox.doCommand();
+ break;
+ case 4:
+ ok(clearButton.disabled,
+ "The clear list button is disabled with a nonsensical search " +
+ "term entered and one active download");
+
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+ };
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_close_download_manager.xul b/comm/suite/components/downloads/tests/chrome/test_close_download_manager.xul
new file mode 100644
index 0000000000..94251a3771
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_close_download_manager.xul
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Anoop Saldanha <poonaatsoc@gmail.com>
+ *
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This test basically checks if the download manager
+ * closes when you press the esc key and accel + w.
+ */
+-->
+
+<window title="Download Manager Test"
+ onload="runTest();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+const dmui = Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsIDownloadManagerUI);
+
+function testCloseDMWithAccelKey(aWin)
+{
+ function dmWindowClosedListener() {
+ aWin.removeEventListener("unload", dmWindowClosedListener, false);
+ ok(!dmui.visible, "DMUI closes with accel + w");
+ SimpleTest.finish();
+ }
+ aWin.addEventListener("unload", dmWindowClosedListener, false);
+
+ synthesizeKey("w", { accelKey: true }, aWin);
+}
+
+function runTest()
+{
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var testPhase = 0;
+ // Specify an observer that will be notified when the dm has been rendered on screen
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ SimpleTest.waitForFocus(function () { closeDM(aSubject) }, aSubject);
+ }
+ };
+
+ function closeDM(win) {
+ // if we add more ways to close DM with keys, add more cases here
+ switch(testPhase++) {
+ case 0:
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ testCloseDMWithAccelKey(win);
+ }
+ }
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_delete_key_cancels.xul b/comm/suite/components/downloads/tests/chrome/test_delete_key_cancels.xul
new file mode 100644
index 0000000000..f02459dd6f
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_delete_key_cancels.xul
@@ -0,0 +1,200 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jens Hatlak <jh@junetz.de> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This tests that the delete key will cancel a download in the UI.
+ * This test was added in bug 474622.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+var invokeCount = 0;
+var cancelDownload = null;
+
+function dlObs(aWin)
+{
+ this.mWin = aWin;
+ this.wasPaused = false;
+ this.wasCanceled = false;
+}
+dlObs.prototype = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if ("timer-callback" == aTopic) {
+ // We're done!
+ this.mWin.close();
+ SimpleTest.finish();
+ }
+ },
+
+ onDownloadStateChange: function(aState, aDownload)
+ {
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING &&
+ !this.wasPaused) {
+ // Make a copy of the cancelDownload function and replace it with a test
+ var counter = () => invokeCount++;
+ [cancelDownload, this.mWin["cancelDownload"]] = [this.mWin["cancelDownload"], counter];
+
+ synthesizeKey("VK_DELETE", {}, this.mWin);
+ is(invokeCount, 1, "Delete canceled the active download");
+
+ this.wasPaused = true;
+ this.mWin.pauseDownload(aDownload.id);
+ }
+
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_PAUSED &&
+ !this.wasCanceled) {
+ synthesizeKey("VK_DELETE", {}, this.mWin);
+ is(invokeCount, 2, "Delete canceled the paused download");
+
+ // After all tests, restore original function
+ this.mWin["cancelDownload"] = cancelDownload;
+
+ this.wasCanceled = true;
+ this.mWin.cancelDownload(aDownload);
+
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ dm.removeListener(this);
+
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ },
+ onStateChange: function(a, b, c, d, e) { },
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onSecurityChange: function(a, b, c, d) { }
+};
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ function addDownload() {
+ function createURI(aObj) {
+ return (aObj instanceof Ci.nsIFile) ? Services.io.newFileURI(aObj) :
+ Services.io.newURI(aObj);
+ }
+
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ var destFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ destFile.append("download.result");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var dl = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
+ createURI("http://example.com/httpd.js"),
+ createURI(destFile), null, null,
+ Math.round(Date.now() * 1000), null, persist, false);
+
+ persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);
+ persist.saveURI(dl.source, null, null, 0, null, null, dl.targetFile, null);
+
+ return dl;
+ }
+
+ // First, we clear out the database
+ dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { cancelDL(aSubject) }, aSubject);
+ }
+ };
+
+ function cancelDL(win) {
+ var doc = win.document;
+ dm.addListener(new dlObs(win));
+
+ addDownload();
+ // we need to focus the download as well
+ doc.getElementById("downloadTree").view.selection.select(0);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_delete_key_removes.xul b/comm/suite/components/downloads/tests/chrome/test_delete_key_removes.xul
new file mode 100644
index 0000000000..8462c3323b
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_delete_key_removes.xul
@@ -0,0 +1,198 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This test ensures that the delete key removes a download. This was added by
+ * bug 411172.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+var dmFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+dmFile.append("dm-ui-test.file");
+dmFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var gTestPath = Services.io.newFileURI(dmFile).spec;
+
+// Downloads are sorted by endTime, so make sure the end times are distinct
+const DownloadData = [
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859236,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FAILED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859234,
+ state: Ci.nsIDownloadManager.DOWNLOAD_CANCELED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859232,
+ state: Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859230,
+ state: Ci.nsIDownloadManager.DOWNLOAD_DIRTY,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 },
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859229,
+ endTime: 1180493839859229,
+ state: Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // First, we populate the database with some fake data
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " +
+ "state, currBytes, maxBytes, preferredAction, autoResume) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, " +
+ ":currBytes, :maxBytes, :preferredAction, :autoResume)");
+ for (let dl of DownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ stmt.finalize();
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { deleteDL(aSubject) }, aSubject);
+ }
+ };
+
+ function deleteDL(win) {
+ var doc = win.document;
+
+ var stmt = db.createStatement("SELECT COUNT(*) FROM moz_downloads");
+ try {
+ stmt.executeStep();
+ let dlTree = doc.getElementById("downloadTree");
+ is(stmt.getInt32(0), dlTree.view.rowCount,
+ "The database and the number of downloads display matches");
+ stmt.reset();
+
+ let len = DownloadData.length;
+ for (let i = 0; i < len; i++) {
+ synthesizeKey("VK_DELETE", {}, win);
+
+ stmt.executeStep();
+ is(stmt.getInt32(0), len - (i + 1),
+ "The download was properly removed");
+ stmt.reset();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ win.close();
+ dmFile.remove(false);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_drag.xul b/comm/suite/components/downloads/tests/chrome/test_drag.xul
new file mode 100644
index 0000000000..133e633c39
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_drag.xul
@@ -0,0 +1,201 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Phil Lacy <philbaseless-firefox@yahoo.com> (Original Author)
+ * Jens Hatlak <jh@junetz.de>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Assure download manager can load valid list item as
+ * "application/moz-x-file", "text/uri-list" and "text/plain"
+ */
+
+based on toolkit/mozapps/downloads/tests/chrome/test_bug_462172.xul
+https://bugzilla.mozilla.org/show_bug.cgi?id=462172
+
+create a file with unique name
+create another file with unique name and delete it
+load into downloads database
+open download manager
+synthesize drag on both files
+missing file should not init drag
+real file should return transferdata with application/x-moz-file,
+ text/uri-list (CRLF-terminated) and text/plain (LF-terminated)
+close window
+-->
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js"></script>
+
+ <script>
+ <![CDATA[
+var missingFileElid;
+var realFileElid;
+const kFiller = "notApplicable";
+const kFillerURL = "https://bugzilla.mozilla.org/show_bug.cgi?id=462172"
+var realFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+var missingFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+realFile.append(kFiller);
+realFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var realFilePath = Services.io.newFileURI(realFile).spec;
+
+missingFile.append(kFiller);
+missingFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var missingFilePath = Services.io.newFileURI(missingFile).spec;
+missingFile.remove(false);
+
+// Dummy data for our files.
+// 'source' field must be in form of a URL.
+const DownloadData = [
+ { name: kFiller,
+ source: kFillerURL,
+ target: realFilePath,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED },
+ { name: kFiller,
+ source: kFillerURL,
+ target: missingFilePath,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED }
+];
+
+function mouseDragStartOnCell(aTree, aRow, aColumn, aWin, aFile)
+{
+ // get cell coordinates
+ if (typeof aTree.columns != "undefined")
+ aColumn = aTree.columns[aColumn];
+ var rect = aTree.treeBoxObject.getCoordsForCellItem(aRow, aColumn, "text");
+ return synthesizeDragStart(aTree.body, aFile, aWin, rect.x, rect.y);
+}
+
+function compareFunc(actualData, expectedData)
+{
+ return expectedData.equals(actualData);
+}
+
+var dragRealFile = [[
+ { type: "application/x-moz-file",
+ data: realFile,
+ eqTest: compareFunc },
+ { type: "text/uri-list",
+ data: realFilePath + "\r\n" },
+ { type: "text/plain",
+ data: realFilePath + "\n" }
+]];
+var dragMissingFile = [[
+ { type: "application/x-moz-file",
+ data: missingFile,
+ eqTest: compareFunc },
+ { type: "text/uri-list",
+ data: missingFilePath + "\r\n" },
+ { type: "text/plain",
+ data: missingFilePath + "\n" }
+]];
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ // load files into db
+ var db = dm.DBConnection;
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads ( name, source, target, state)" +
+ "VALUES (:name, :source, :target, :state)");
+ for (let dl of DownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+ stmt.execute();
+ }
+ stmt.finalize();
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+ win.focus();
+
+ var downloadTree = win.document.getElementById("downloadTree");
+
+ // Now we can run our tests
+ // Unordered sorting -> DownloadData/insert order: realFile, missingFile
+ // Column 4 is "Progress" (column 1, "Status", is hidden by default)
+ var result = mouseDragStartOnCell(downloadTree, 0, 4, win, dragRealFile);
+ is(result, null, "Checking for Real file match");
+ result = mouseDragStartOnCell(downloadTree, 1, 4, win, dragMissingFile);
+ isnot(result, null, "Drag start did not return item for missing file");
+
+ // Done.
+ win.close();
+ realFile.remove(false);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+ };
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_enter_dblclick_opens.xul b/comm/suite/components/downloads/tests/chrome/test_enter_dblclick_opens.xul
new file mode 100644
index 0000000000..59c105ceaa
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_enter_dblclick_opens.xul
@@ -0,0 +1,243 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jens Hatlak <jh@junetz.de> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test bug 495545 (implemented by bug 474622) to make sure the enter key
+ * or a double click actually calls opening the downloaded file.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+// similar, but not equal to the one in tree_shared.js
+function mouseDblClickOnCell(aTree, aRow, aColumn, aWin)
+{
+ // get cell coordinates
+ if (typeof aTree.columns != "undefined")
+ aColumn = aTree.columns[aColumn];
+ var rect = aTree.treeBoxObject.getCoordsForCellItem(aRow, aColumn, "text");
+ synthesizeMouse(aTree.body, rect.x, rect.y, { clickCount: 2 }, aWin);
+}
+
+function dlObs(aWin)
+{
+ this.mWin = aWin;
+ this.currDownload = null;
+}
+dlObs.prototype = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if ("timer-callback" == aTopic) {
+ var downloadTree = this.mWin.document.getElementById("downloadTree");
+ var downloadView = downloadTree.view;
+
+ // Default test/check for invocations
+ var invokeCount = 0;
+ var counter = () => invokeCount++;
+
+ // Run tests
+
+ // Make a copy of the openDownload function and replace it with a test
+ let copy;
+ [copy, this.mWin["openDownload"]] = [this.mWin["openDownload"], counter];
+
+ // Select the first (paused) download for not calling openDownload
+ downloadView.selection.select(0);
+
+ synthesizeKey("VK_RETURN", {}, this.mWin);
+ is(invokeCount, 0, "Enter didn't do anything");
+
+ mouseDblClickOnCell(downloadTree, 0, 3, this.mWin);
+ is(invokeCount, 0, "Double click didn't do anything");
+
+ // Select the second (finished) download for calling openDownload
+ downloadView.selection.select(1);
+
+ synthesizeKey("VK_RETURN", {}, this.mWin);
+ is(invokeCount, 1, "Enter opened download");
+
+ mouseDblClickOnCell(downloadTree, 1, 3, this.mWin);
+ is(invokeCount, 2, "Double click opened download");
+
+ // After all tests, restore original function
+ this.mWin["openDownload"] = copy;
+
+ // We're done!
+ this.mWin.close();
+ this.currDownload.targetFile.remove(false);
+ SimpleTest.finish();
+ }
+ },
+
+ onDownloadStateChange: function(aState, aDownload)
+ {
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
+ this.currDownload = aDownload;
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ dm.removeListener(this);
+ }
+ },
+ onStateChange: function(a, b, c, d, e) { },
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onSecurityChange: function(a, b, c, d) { }
+};
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ function addDownload() {
+ function createURI(aObj) {
+ return (aObj instanceof Ci.nsIFile) ? Services.io.newFileURI(aObj) :
+ Services.io.newURI(aObj);
+ }
+
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ var destFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ destFile.append("download.result");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var dl = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
+ createURI("http://example.com/httpd.js"),
+ createURI(destFile), null, null,
+ Math.round(Date.now() * 1000), null, persist, false);
+
+ persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);
+ persist.saveURI(dl.source, null, null, 0, null, null, dl.targetFile, null);
+
+ return dl;
+ }
+
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (source, state, target, referrer) " +
+ "VALUES (?1, ?2, ?3, ?4)");
+
+ // add first download: PAUSED state
+ try {
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("dltest-paused");
+ var fileSpec = Services.io.newFileURI(file).spec;
+ stmt.bindByIndex(0, "http://example.com/file");
+ stmt.bindByIndex(1, dm.DOWNLOAD_PAUSED);
+ stmt.bindByIndex(2, fileSpec);
+ stmt.bindByIndex(3, "http://referrer/");
+
+ // Add it!
+ stmt.execute();
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ dm.addListener(new dlObs(win));
+
+ // add second download: FINISHED state, actually created
+ // (checked by cmd_open)
+ addDownload();
+
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ }
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_multi_select.xul b/comm/suite/components/downloads/tests/chrome/test_multi_select.xul
new file mode 100644
index 0000000000..0c0a05cf4b
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_multi_select.xul
@@ -0,0 +1,204 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!--
+ * Test bug 228842 to make sure multiple selections work in the download
+ * manager by making sure commands work as expected for both single and doubly
+ * selected items.
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (source, state, target, referrer) " +
+ "VALUES (?1, ?2, ?3, ?4)");
+
+ try {
+ for (let site of ["ed.agadak.net", "mozilla.org", "mozilla.com", "mozilla.net"]) {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(site);
+ let fileSpec = Services.io.newFileURI(file).spec;
+
+ stmt.bindByIndex(0, "http://" + site + "/file");
+ stmt.bindByIndex(1, dm.DOWNLOAD_FINISHED);
+ stmt.bindByIndex(2, fileSpec);
+ stmt.bindByIndex(3, "http://referrer/");
+
+ // Add it!
+ stmt.execute();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ var downloadView = win.document.getElementById("downloadTree").view;
+
+ // Default test/check for invocations
+ var invokeCount = 0;
+ var counter = () => invokeCount++;
+
+ // Accessors for returning a value for various properties
+ var getItems = () => downloadView.rowCount;
+ var getSelected = () => downloadView.selection.count;
+ var getClipboard = function() {
+ var clip = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(Ci.nsIClipboard);
+ var trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+
+ trans.init(null);
+ trans.addDataFlavor("text/unicode");
+ clip.getData(trans, clip.kGlobalClipboard);
+ var str = {};
+ trans.getTransferData("text/unicode", str, {});
+ return str.value.QueryInterface(Ci.nsISupportsString)
+ .data;
+ };
+
+ // Array of tests that consist of the command name, download manager
+ // function to temporarily replace, method to use in its place, value to
+ // use when checking correctness
+ var commandTests = [
+ ["pause", "pauseDownload", counter, counter],
+ ["resume", "resumeDownload", counter, counter],
+ ["cancel", "cancelDownload", counter, counter],
+ ["open", "openDownload", counter, counter],
+ ["show", "showDownload", counter, counter],
+ ["properties", "showProperties", counter, counter],
+ ["retry", "retryDownload", counter, counter],
+ ["openReferrer", "openUILink", counter, counter],
+ ["copyLocation", null, null, getClipboard],
+ ["remove", null, null, getItems],
+ ["selectAll", null, null, getSelected],
+ ];
+
+ // All the expected results for both single and double selections
+ var allExpected = {
+ single: {
+ pause: [0, "Paused no downloads"],
+ resume: [0, "Resumed no downloads"],
+ cancel: [0, "Canceled no downloads"],
+ open: [0, "Opened no downloads"],
+ show: [0, "Showed no downloads"],
+ properties: [1, "Called properties for one download"],
+ retry: [0, "Retried no downloads"],
+ openReferrer: [1, "Opened one referrer"],
+ copyLocation: ["http://ed.agadak.net/file", "Copied one location"],
+ remove: [3, "Removed one download, remaining 3"],
+ selectAll: [3, "Selected all 3 remaining downloads"],
+ },
+ double: {
+ pause: [0, "Paused neither download"],
+ resume: [0, "Resumed neither download"],
+ cancel: [0, "Canceled neither download"],
+ open: [0, "Opened neither download"],
+ show: [0, "Showed neither download"],
+ properties: [0, "Called properties for neither download"],
+ retry: [0, "Retried neither download"],
+ openReferrer: [0, "Opened neither referrer"],
+ copyLocation: ["http://mozilla.org/file\nhttp://mozilla.com/file", "Copied both locations"],
+ remove: [1, "Removed both downloads, remaining 1"],
+ selectAll: [1, "Selected the 1 remaining download"],
+ },
+ };
+
+ var cmdName;
+
+ // Run two tests: single selected, double selected
+ for (let whichTest of ["single", "double"]) {
+ let expected = allExpected[whichTest];
+
+ if (whichTest == "double")
+ // Select the first 2 downloads for double
+ downloadView.selection.rangedSelect(0, 1, false);
+ else
+ // Select the first download for single
+ downloadView.selection.select(0);
+
+ for (let [command, func, test, value] of commandTests) {
+ // Make a copy of the original function and replace it with a test
+ let copy;
+ [copy, win[func]] = [win[func], test];
+
+ // Run the command from the menu
+ if (command == "selectAll")
+ cmdName = "menu_" + command;
+ else
+ cmdName = "dlMenu_" + command;
+
+ win.document.getElementById(cmdName).doCommand();
+
+ // Make sure the value is as expected
+ let [correct, message] = expected[command];
+ is(value(), correct, message);
+
+ // Restore original values
+ invokeCount = 0;
+ win[func] = copy;
+ }
+ }
+
+ // We're done!
+ win.close();
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_multiword_search.xul b/comm/suite/components/downloads/tests/chrome/test_multiword_search.xul
new file mode 100644
index 0000000000..ae2817c3fa
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_multiword_search.xul
@@ -0,0 +1,173 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test for bug 419403 that lets the download manager support multiple word
+ * search against the download name, source/referrer, date/time, file size,
+ * etc.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // Make a file name for the downloads
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("multiWord");
+ var filePath = Services.io.newFileURI(file).spec;
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, target, source, state, endTime, maxBytes) " +
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6)");
+
+ try {
+ for (let site of ["ed.agadak.net", "mozilla.org"]) {
+ stmt.bindByIndex(0, "Super Pimped Download");
+ stmt.bindByIndex(1, filePath);
+ stmt.bindByIndex(2, "http://" + site + "/file");
+ stmt.bindByIndex(3, dm.DOWNLOAD_FINISHED);
+ stmt.bindByIndex(4, new Date(1985, 7, 2) * 1000);
+ stmt.bindByIndex(5, 111222333444);
+
+ // Add it!
+ stmt.execute();
+ }
+ } finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testPhase = -1;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+ var downloadView = win.document.getElementById("downloadTree").view;
+ var searchbox = win.document.getElementById("search-box");
+
+ var search = function(aTerms) {
+ searchbox.value = aTerms;
+ searchbox.doCommand();
+ };
+
+ let testResults = function(aExpected) {
+ is(downloadView.rowCount, aExpected,
+ "Phase " + testPhase + ": search matched " + aExpected + " download(s)");
+ };
+
+ // The list must have built, so figure out what test to do
+ switch (++testPhase) {
+ case 0:
+ // Search for multiple words in any order in all places
+ search("download super pimped multiWord");
+
+ break;
+ case 1:
+ // Done populating the two items
+ testResults(2);
+
+ // Do partial word matches including the site
+ search("Agadak.net downl pimp multi");
+
+ break;
+ case 2:
+ // Done populating the one result
+ testResults(1);
+
+ // The search term shouldn't be treated like a regular expression,
+ // e.g. "D.wnload" shouldn't match "Download".
+ search("D.wnload");
+
+ break;
+ case 3:
+ testResults(0);
+
+ // We're done!
+ win.close();
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+ };
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_open_properties.xul b/comm/suite/components/downloads/tests/chrome/test_open_properties.xul
new file mode 100644
index 0000000000..2f319d6766
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_open_properties.xul
@@ -0,0 +1,197 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008-2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Anoop Saldanha <poonaatsoc@gmail.com> (Original Author)
+ * Robert Kaiser <kairo@kairo.at>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This tests bug 474620 - opening a progress dialog with the "properties"
+ * item in the download manager.
+ */
+-->
+
+<window title="Download Manager Test"
+ onload="runTest();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+const nsIDownloadManager = Ci.nsIDownloadManager;
+var dmFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+dmFile.append("dm-ui-test.file");
+dmFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var gTestPath = Services.io.newFileURI(dmFile).spec;
+
+const DoneDownloadData = [
+ { name: "Dead",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: nsIDownloadManager.DOWNLOAD_CANCELED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+const ActiveDownloadData = [
+ { name: "Patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: nsIDownloadManager.DOWNLOAD_DOWNLOADING,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+function runTest()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " +
+ "state, currBytes, maxBytes, preferredAction, autoResume) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, " +
+ ":currBytes, :maxBytes, :preferredAction, :autoResume)");
+ for (let dl of DoneDownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ //stmt.finalize();
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var dmview;
+ var testStartTime = Date.now();
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ switch (testPhase++) {
+ case 0:
+ ok(!!aSubject.document.getElementById("dlWinCommands"),
+ "The download manager window is active");
+ // the download manager is started, select the first download
+ dmview = aSubject.document.getElementById("downloadTree").view;
+ dmview.selection.select(0);
+ // call "properties"
+ aSubject.document.getElementById("dlMenu_properties").doCommand();
+
+ break;
+ case 1:
+ ok(!!aSubject.document.getElementById("dlProgressCommands"),
+ "The progress dialog was called for a canceled download");
+ var endTimeSeconds = Math.round(DoneDownloadData[0].endTime / 1000);
+ is(aSubject.gEndTime, endTimeSeconds, "End time matches data");
+
+ // Close the progress window
+ aSubject.close();
+
+ // Populate the download manager with an active download now, and
+ // rebuild the list
+ stmt.reset();
+ for (let dl of ActiveDownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ stmt.finalize();
+ dm.cleanUp();
+
+ break;
+ case 2:
+ ok(!!aSubject.document.getElementById("dlWinCommands"),
+ "The download manager window got an event");
+ // the download manager UI is rebuilt, select the first download
+ dmview.selection.select(0);
+ // call "properties"
+ aSubject.document.getElementById("dlMenu_properties").doCommand();
+
+ break;
+ case 3:
+ ok(!!aSubject.document.getElementById("dlProgressCommands"),
+ "The progress dialog was called for an active download");
+ // active downloads updated to current time,
+ // just check if it's set to later than start of the test
+ ok(aSubject.gEndTime >= testStartTime, "End time within test run");
+
+ // Close the progress window
+ aSubject.close();
+
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+ };
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_removeDownload_updates_ui.xul b/comm/suite/components/downloads/tests/chrome/test_removeDownload_updates_ui.xul
new file mode 100644
index 0000000000..d5b5d6c657
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_removeDownload_updates_ui.xul
@@ -0,0 +1,150 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test bug 394039 to make sure calling removeDownload of the
+ * nsIDownloadManager service correctly updates the download window.
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+var dmFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+dmFile.append("dm-ui-test.file");
+dmFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+var gTestPath = Services.io.newFileURI(dmFile).spec;
+
+const DownloadData = [
+ { name: "381603.patch",
+ source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520",
+ target: gTestPath,
+ startTime: 1180493839859230,
+ endTime: 1180493839859239,
+ state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
+ currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 }
+];
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // First, we populate the database with some fake data
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " +
+ "state, currBytes, maxBytes, preferredAction, autoResume) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, " +
+ ":currBytes, :maxBytes, :preferredAction, :autoResume)");
+ for (let dl of DownloadData) {
+ for (let [prop, value] of Object.entries(dl))
+ stmt.params[prop] = value;
+
+ stmt.execute();
+ }
+ stmt.finalize();
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+ var doc = win.document;
+
+ // Note: This also tests the ordering of the display
+ var stmt = db.createStatement("SELECT id FROM moz_downloads");
+ var id = -1;
+ try {
+ stmt.executeStep();
+ id = stmt.getInt64(0);
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ dm.removeDownload(id);
+ var dlTreeView = doc.getElementById("downloadTree").view;
+ is(dlTreeView.rowCount, 0,
+ "The download was properly removed");
+
+ win.close();
+ dmFile.remove(false);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_search_clearlist.xul b/comm/suite/components/downloads/tests/chrome/test_search_clearlist.xul
new file mode 100644
index 0000000000..862291cd5d
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_search_clearlist.xul
@@ -0,0 +1,168 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test bug 431188 to make sure the Clear list button is enabled after doing a
+ * search and finding results.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // Make a file name for the downloads
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("cleanUp");
+ var filePath = Services.io.newFileURI(file).spec;
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (target, source, state, endTime) " +
+ "VALUES (?1, ?2, ?3, ?4)");
+
+ // Add a bunch of downloads that don't match the search
+ var sites = [];
+ for (let i = 0; i < 50; i++)
+ sites.push("i-hate.clear-list-" + i);
+
+ // Add one download that matches the search
+ var searchTerm = "one-download.match-search";
+ sites.push(searchTerm);
+
+ try {
+ for (let site of sites) {
+ stmt.bindByIndex(0, filePath);
+ stmt.bindByIndex(1, "http://" + site + "/file");
+ stmt.bindByIndex(2, dm.DOWNLOAD_FINISHED);
+ // Make the one that matches slightly older so it appears last
+ stmt.bindByIndex(3, 1112223334445556 - (site == searchTerm));
+
+ // Add it!
+ stmt.execute();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testPhase = 0;
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+ var downloadView = win.document.getElementById("downloadTree").view;
+ var searchbox = win.document.getElementById("search-box");
+ var clearList = win.document.getElementById("clearListButton");
+
+ // The list must have built, so figure out what test to do
+ switch (testPhase++) {
+ case 0:
+ case 2:
+ // Search for the one download
+ searchbox.value = searchTerm;
+ searchbox.doCommand();
+
+ break;
+ case 1:
+ // Search came back with 1 item
+ is(downloadView.rowCount, 1, "Search found the item to delete");
+ is(clearList.disabled, false, "Clear list is enabled for search matching 1 item");
+
+ // Clear the list that has the single matched item
+ clearList.doCommand();
+
+ break;
+ case 3:
+ // There's nothing that matches the search
+ is(downloadView.rowCount, 0, "Clear list killed the one matching download");
+ is(clearList.disabled, true, "Clear list is disabled for no items");
+
+ // We're done!
+ win.close();
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+
+ break;
+ }
+ }
+ };
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_search_keys.xul b/comm/suite/components/downloads/tests/chrome/test_search_keys.xul
new file mode 100644
index 0000000000..e651e341b5
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_search_keys.xul
@@ -0,0 +1,128 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Make sure the download manager can display downloads in the right order and
+ * contains the expected data. The list has one of each download state ordered
+ * by the start/end times.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+function test_meta_f(aWin)
+{
+ var doc = aWin.document;
+ var searchbox = doc.getElementById("search-box");
+ var dlTree = doc.getElementById("downloadTree");
+
+ // Enusre the searchbox is not focused
+ dlTree.focus();
+
+ // Dispatch the right key combination
+ synthesizeKey("f", {accelKey: true}, aWin);
+
+ ok(searchbox.hasAttribute("focused"), "Searchbox is focused");
+}
+
+var testFuncs = [
+ test_meta_f,
+]
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ // Now we can run our tests
+ for (let t of testFuncs)
+ t(win);
+
+ win.close();
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_select_all.xul b/comm/suite/components/downloads/tests/chrome/test_select_all.xul
new file mode 100644
index 0000000000..85dadcb168
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_select_all.xul
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test bug 429614 to make sure ctrl/cmd-a work to select all downloads and
+ * hitting delete removes them all.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ var db = dm.DBConnection;
+
+ // Empty any old downloads
+ db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // Make a file name for the downloads
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("selectAll");
+ var filePath = Services.io.newFileURI(file).spec;
+
+ var stmt = db.createStatement(
+ "INSERT INTO moz_downloads (target, source, state) " +
+ "VALUES (?1, ?2, ?3)");
+
+ var sites = ["mozilla.org", "mozilla.com", "select.all"];
+ try {
+ for (let site of sites) {
+ stmt.bindByIndex(0, filePath);
+ stmt.bindByIndex(1, "http://" + site + "/file");
+ stmt.bindByIndex(2, dm.DOWNLOAD_FINISHED);
+
+ // Add it!
+ stmt.execute();
+ }
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ var downloadView = win.document.getElementById("downloadTree").view;
+
+ is(downloadView.rowCount, sites.length, "All downloads displayed");
+
+ // Select all downloads
+ var isMac = AppConstants.platform == "macosx";
+ synthesizeKey("a", { metaKey: isMac, ctrlKey: !isMac }, win);
+ is(downloadView.selection.count, sites.length, "All downloads selected");
+
+ // Delete all downloads
+ synthesizeKey("VK_DELETE", {}, win);
+ is(downloadView.rowCount, 0, "All downloads removed");
+
+ // We're done!
+ win.close();
+ obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+
+ obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_space_key_pauses_resumes.xul b/comm/suite/components/downloads/tests/chrome/test_space_key_pauses_resumes.xul
new file mode 100644
index 0000000000..d22624be62
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_space_key_pauses_resumes.xul
@@ -0,0 +1,221 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This tests that the space key will pause and resume a download in the UI.
+ * This test was added in bug 413985.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+function bug413985obs(aWin)
+{
+ this.mWin = aWin;
+ this.wasPaused = false;
+ this.wasResumed = false;
+ this.wasFinished = false;
+}
+bug413985obs.prototype = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if ("timer-callback" == aTopic) {
+ if (this.wasFinished) {
+ // We're done!
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ dm.removeListener(this);
+ this.mWin.close();
+ SimpleTest.finish();
+ } else {
+ if (!this.wasPaused)
+ this.wasPaused = true;
+ else if (!this.wasResumed)
+ this.wasResumed = true;
+
+ // dispatch a space keypress to pause/resume the download
+ synthesizeKey(" ", {}, this.mWin);
+ }
+ }
+ },
+
+ onDownloadStateChange: function(aState, aDownload)
+ {
+ ok(true, "State value = " + aDownload.state);
+ switch (aDownload.state) {
+ case Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+ if (!this.wasPaused) {
+ ok(true, "The download was started successfully");
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ } else {
+ ok(this.wasResumed, "The download was resumed successfully");
+ }
+ break;
+
+ case Ci.nsIDownloadManager.DOWNLOAD_PAUSED:
+ ok(this.wasPaused && !this.wasResumed,
+ "The download was paused successfully");
+ if (!this.wasResumed) {
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ break;
+
+ case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
+ ok(this.wasPaused && this.wasResumed,
+ "The download was paused, and then resumed to completion");
+ this.wasFinished = true;
+ aDownload.targetFile.remove(false);
+
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ break;
+
+ default:
+ break;
+ }
+ },
+ onStateChange: function(a, b, c, d, e) { },
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onSecurityChange: function(a, b, c, d) { }
+};
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ function addDownload() {
+ function createURI(aObj) {
+ return (aObj instanceof Ci.nsIFile) ? Services.io.newFileURI(aObj) :
+ Services.io.newURI(aObj);
+ }
+
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ var destFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ destFile.append("download.result");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ // SeaMonkey: Use a bigger file than "http://example.com/httpd.js". (Bug 595685)
+ var dl = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
+ createURI("http://example.com/tests/fonts/mplus/mplus-1p-regular.ttf"),
+ createURI(destFile), null, null,
+ Math.round(Date.now() * 1000), null, persist, false);
+
+ persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);
+ persist.saveURI(dl.source, null, null, 0, null, null, dl.targetFile, null);
+
+ return dl;
+ }
+
+ // First, we clear out the database
+ dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ var doc = win.document;
+ dm.addListener(new bug413985obs(win));
+
+ addDownload();
+ // we need to focus the download as well
+ doc.getElementById("downloadTree").view.selection.select(0);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_space_key_retries.xul b/comm/suite/components/downloads/tests/chrome/test_space_key_retries.xul
new file mode 100644
index 0000000000..5255f9caea
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_space_key_retries.xul
@@ -0,0 +1,198 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jens Hatlak <jh@junetz.de> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This tests that the space key will retry a download in the UI.
+ * This test was added in bug 474622.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script>
+ <![CDATA[
+
+function dlObs(aWin)
+{
+ this.mWin = aWin;
+ this.wasCanceled = false;
+ this.wasFinished = false;
+}
+dlObs.prototype = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if ("timer-callback" == aTopic) {
+ if (this.wasFinished) {
+ // We're done!
+ this.mWin.close();
+ SimpleTest.finish();
+ } else {
+ // dispatch a space keypress to retry the download
+ synthesizeKey(" ", {}, this.mWin);
+ }
+ }
+ },
+
+ onDownloadStateChange: function(aState, aDownload)
+ {
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING &&
+ !this.wasCanceled) {
+ this.wasCanceled = true;
+ this.mWin.cancelDownload(aDownload);
+ }
+
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_CANCELED) {
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
+ ok(this.wasCanceled,
+ "The download was canceled, retried and then ran to completion");
+ this.wasFinished = true;
+ aDownload.targetFile.remove(false);
+
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ dm.removeListener(this);
+
+ // We have to do this on a timer so other JS stuff that handles the UI
+ // can actually catch up to us...
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ },
+ onStateChange: function(a, b, c, d, e) { },
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onSecurityChange: function(a, b, c, d) { }
+};
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ function addDownload() {
+ function createURI(aObj) {
+ return (aObj instanceof Ci.nsIFile) ? Services.io.newFileURI(aObj) :
+ Services.io.newURI(aObj);
+ }
+
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ var destFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ destFile.append("download.result");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var dl = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
+ createURI("http://example.com/httpd.js"),
+ createURI(destFile), null, null,
+ Math.round(Date.now() * 1000), null, persist, false);
+
+ persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);
+ persist.saveURI(dl.source, null, null, 0, null, null, dl.targetFile, null);
+
+ return dl;
+ }
+
+ // First, we clear out the database
+ dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ // See if the DM is already open, and if it is, close it!
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win)
+ win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ SimpleTest.waitForFocus(function () { continueTest(aSubject) }, aSubject);
+ }
+ };
+
+ function continueTest(win) {
+ var doc = win.document;
+ dm.addListener(new dlObs(win));
+
+ addDownload();
+ // we need to focus the download as well
+ doc.getElementById("downloadTree").view.selection.select(0);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ }
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Show the Download Manager UI
+ Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsISuiteDownloadManagerUI)
+ .showManager();
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/downloads/tests/chrome/test_ui_stays_open_on_alert_clickback.xul b/comm/suite/components/downloads/tests/chrome/test_ui_stays_open_on_alert_clickback.xul
new file mode 100644
index 0000000000..f54fb2dc21
--- /dev/null
+++ b/comm/suite/components/downloads/tests/chrome/test_ui_stays_open_on_alert_clickback.xul
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<!--
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test bug 397935 to make sure the download manager ui window stays open when
+ * it's opened by the user clicking the alert and has the close-when-done pref
+ * set.
+ */
+-->
+
+<window title="Download Manager Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ <![CDATA[
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function test()
+{
+ var dm = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+
+ // Empty any old downloads
+ dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
+
+ var setClose = aVal =>
+ Services.prefs.setBoolPref("browser.download.manager.closeWhenDone", aVal);
+
+ // Close the UI if necessary
+ var win = Services.wm.getMostRecentWindow("Download:Manager");
+ if (win) win.close();
+
+ const DLMGR_UI_DONE = "download-manager-ui-done";
+
+ var testObs = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic != DLMGR_UI_DONE)
+ return;
+
+ var win = aSubject;
+
+ // Note: This test will not be valid if the download list is built
+ // synchronously in Startup in downloads.js
+ // Make sure the window stays open
+ var dmui = Cc["@mozilla.org/download-manager-ui;1"]
+ .getService(Ci.nsIDownloadManagerUI);
+ ok(dmui.visible, "Download Manager stays open on alert click");
+
+ win.close();
+ setClose(false);
+ Services.obs.removeObserver(testObs, DLMGR_UI_DONE);
+ SimpleTest.finish();
+ }
+ };
+
+ // Register with the observer service
+ Services.obs.addObserver(testObs, DLMGR_UI_DONE);
+
+ // Simulate an alert click with pref set to true
+ setClose(true);
+ dm.QueryInterface(Ci.nsIObserver)
+ .observe(null, "alertclickcallback", null);
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/comm/suite/components/feeds/FeedConverter.js b/comm/suite/components/feeds/FeedConverter.js
new file mode 100644
index 0000000000..153f2a803d
--- /dev/null
+++ b/comm/suite/components/feeds/FeedConverter.js
@@ -0,0 +1,461 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
+const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
+const TYPE_ANY = "*/*";
+
+const FEEDHANDLER_URI = "about:feeds";
+
+const PREF_SELECTED_APP = "browser.feeds.handlers.application";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+
+const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
+const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
+const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
+const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
+
+const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
+const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
+const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
+const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
+
+function getPrefAppForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_APP;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_APP;
+
+ default:
+ return PREF_SELECTED_APP;
+ }
+}
+
+function getPrefWebForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_WEB;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_WEB;
+
+ default:
+ return PREF_SELECTED_WEB;
+ }
+}
+
+function getPrefActionForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_ACTION;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_ACTION;
+
+ default:
+ return PREF_SELECTED_ACTION;
+ }
+}
+
+function getPrefReaderForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_READER;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_READER;
+
+ default:
+ return PREF_SELECTED_READER;
+ }
+}
+
+function LOG(str) {
+ if (Services.prefs.getBoolPref("feeds.log", false))
+ dump("*** Feeds: " + str + "\n");
+}
+
+function FeedConverter() {
+}
+
+FeedConverter.prototype = {
+ /**
+ * This is the downloaded text data for the feed.
+ */
+ _data: null,
+
+ /**
+ * This is the object listening to the conversion, which is ultimately the
+ * docshell for the load.
+ */
+ _listener: null,
+
+ /**
+ * Records if the feed was sniffed
+ */
+ _sniffed: false,
+
+ /**
+ * See nsISupports.idl
+ */
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFeedResultListener,
+ Ci.nsIStreamConverter,
+ Ci.nsIStreamListener,
+ Ci.nsIRequestObserver]),
+ classID: Components.ID("{88592f45-3866-4c8e-9d8a-ab58b290fcf7}"),
+
+ /**
+ * See nsIStreamConverter.idl
+ */
+ convert: function convert(sourceStream, sourceType, destinationType,
+ context) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * See nsIStreamConverter.idl
+ */
+ asyncConvertData: function asyncConvertData(sourceType, destinationType,
+ listener, context) {
+ this._listener = listener;
+ },
+
+ /**
+ * Whether or not the preview page is being forced.
+ */
+ _forcePreviewPage: false,
+
+ /**
+ * Release our references to various things once we're done using them.
+ */
+ _releaseHandles: function _releaseHandles() {
+ this._listener = null;
+ this._request = null;
+ this._processor = null;
+ },
+
+ /**
+ * See nsIFeedResultListener.idl
+ */
+ handleResult: function handleResult(result) {
+ // Feeds come in various content types, which our feed sniffer coerces to
+ // the maybe.feed type. However, feeds are used as a transport for
+ // different data types, e.g. news/blogs (traditional feed), video/audio
+ // (podcasts) and photos (photocasts, photostreams). Each of these is
+ // different in that there's a different class of application suitable for
+ // handling feeds of that type, but without a content-type differentiation
+ // it is difficult for us to disambiguate.
+ //
+ // The other problem is that if the user specifies an auto-action handler
+ // for one feed application, the fact that the content type is shared means
+ // that all other applications will auto-load with that handler too,
+ // regardless of the content-type.
+ //
+ // This means that content-type alone is not enough to determine whether
+ // or not a feed should be auto-handled. Therefore for feeds we need
+ // to always use this stream converter, even when an auto-action is
+ // specified, not the basic one provided by WebContentConverter. This
+ // converter needs to consume all of the data and parse it, and based on
+ // that determination make a judgement about type.
+ //
+ // Since there are no content types for this content, and I'm not going to
+ // invent any, the upshot is that while a user can set an auto-handler for
+ // generic feed content, the system will prevent them from setting an auto-
+ // handler for other stream types. In those cases, the user will always see
+ // the preview page and have to select a handler. We can guess and show
+ // a client handler, but will not be able to show web handlers for those
+ // types.
+ //
+ // If this is just a feed, not some kind of specialized application, then
+ // auto-handlers can be set and we should obey them.
+ try {
+ var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]
+ .getService(Ci.nsIFeedResultService);
+ if (!this._forcePreviewPage && result.doc) {
+ var feed = result.doc.QueryInterface(Ci.nsIFeed);
+ var handler = Services.prefs.getCharPref(getPrefActionForType(feed.type), "ask");
+
+ if (handler != "ask") {
+ if (handler == "reader")
+ handler = Services.prefs.getCharPref(getPrefReaderForType(feed.type), "messenger");
+ switch (handler) {
+ case "web":
+ var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]
+ .getService(Ci.nsIWebContentConverterService);
+ if ((feed.type == Ci.nsIFeed.TYPE_FEED &&
+ wccr.getAutoHandler(TYPE_MAYBE_FEED)) ||
+ (feed.type == Ci.nsIFeed.TYPE_VIDEO &&
+ wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) ||
+ (feed.type == Ci.nsIFeed.TYPE_AUDIO &&
+ wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) {
+ wccr.loadPreferredHandler(this._request);
+ return;
+ }
+ break;
+
+ default:
+ LOG("unexpected handler: " + handler);
+ // fall through -- let feed service handle error
+ case "bookmarks":
+ case "client":
+ case "messenger":
+ try {
+ var title = feed.title ? feed.title.plainText() : "";
+ var desc = feed.subtitle ? feed.subtitle.plainText() : "";
+ feedService.addToClientReader(result.uri.spec, title, desc, feed.type);
+ return;
+ } catch(ex) {
+ /* fallback to preview mode */
+ }
+ }
+ }
+ }
+
+ var chromeChannel;
+ var oldChannel = this._request.QueryInterface(Ci.nsIChannel);
+ var loadInfo = oldChannel.loadInfo;
+
+ // If there was no automatic handler, or this was a podcast,
+ // photostream or some other kind of application, show the
+ // preview page if the parser returned a document.
+ if (result.doc) {
+
+ // Store the result in the result service so that the display
+ // page can access it.
+ feedService.addFeedResult(result);
+
+ // Now load the actual XUL document.
+ var chromeURI = Services.io.newURI(FEEDHANDLER_URI);
+ chromeChannel = Services.io.newChannelFromURIWithLoadInfo(chromeURI, loadInfo);
+ // carry the origin attributes from the channel that loaded the feed.
+ chromeChannel.owner = Services.scriptSecurityManager
+ .createCodebasePrincipal(chromeURI,
+ loadInfo.originAttributes);
+ chromeChannel.originalURI = result.uri;
+ }
+ else
+ chromeChannel = Services.io.newChannelFromURIWithLoadInfo(result.uri, loadInfo);
+
+ chromeChannel.loadGroup = this._request.loadGroup;
+ chromeChannel.asyncOpen2(this._listener);
+ }
+ finally {
+ this._releaseHandles();
+ }
+ },
+
+ /**
+ * See nsIStreamListener.idl
+ */
+ onDataAvailable: function onDataAvailable(request, context, inputStream,
+ sourceOffset, count) {
+ if (this._processor)
+ this._processor.onDataAvailable(request, context, inputStream,
+ sourceOffset, count);
+ },
+
+ /**
+ * See nsIRequestObserver.idl
+ */
+ onStartRequest: function onStartRequest(request, context) {
+ var channel = request.QueryInterface(Ci.nsIChannel);
+
+ // Check for a header that tells us there was no sniffing
+ // The value doesn't matter.
+ try {
+ var httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ // Make sure to check requestSucceeded before the potentially-throwing
+ // getResponseHeader.
+ if (!httpChannel.requestSucceeded) {
+ // Just give up, but don't forget to cancel the channel first!
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+ // Note: this throws if the header is not set.
+ httpChannel.getResponseHeader("X-Moz-Is-Feed");
+ }
+ catch (ex) {
+ this._sniffed = true;
+ }
+
+ this._request = request;
+
+ // Save and reset the forced state bit early, in case there's some kind of
+ // error.
+ var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]
+ .getService(Ci.nsIFeedResultService);
+ this._forcePreviewPage = feedService.forcePreviewPage;
+ feedService.forcePreviewPage = false;
+
+ // Parse feed data as it comes in
+ this._processor = Cc["@mozilla.org/feed-processor;1"]
+ .createInstance(Ci.nsIFeedProcessor);
+ this._processor.listener = this;
+ this._processor.parseAsync(null, channel.URI);
+
+ this._processor.onStartRequest(request, context);
+ },
+
+ /**
+ * See nsIRequestObserver.idl
+ */
+ onStopRequest: function onStopRequest(request, context, status) {
+ if (this._processor)
+ this._processor.onStopRequest(request, context, status);
+ }
+
+};
+
+/**
+ * Keeps parsed FeedResults around for use elsewhere in the UI after the stream
+ * converter completes.
+ */
+function FeedResultService() {
+}
+
+FeedResultService.prototype = {
+ /**
+ * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the
+ * value in case the same URI is requested concurrently.
+ */
+ _results: { },
+
+ /**
+ * See nsIFeedResultService.idl
+ */
+ forcePreviewPage: false,
+
+ /**
+ * See nsIFeedResultService.idl
+ */
+ addToClientReader: function addToClientReader(spec, title, subtitle, feedType) {
+ var handler = Services.prefs.getCharPref(getPrefActionForType(feedType), "reader");
+ if (handler == "ask" || handler == "reader")
+ handler = Services.prefs.getCharPref(getPrefReaderForType(feedType), "messenger");
+
+ switch (handler) {
+ case "client":
+ var clientApp = Services.prefs.getComplexValue(getPrefAppForType(feedType),
+ Ci.nsIFile);
+
+ // For the benefit of applications that might know how to deal with more
+ // URLs than just feeds, send feed: URLs in the following format:
+ //
+ // http urls: replace scheme with feed, e.g.
+ // http://foo.com/index.rdf -> feed://foo.com/index.rdf
+ // other urls: prepend feed: scheme, e.g.
+ // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf
+ var feedURI = Services.io.newURI(spec);
+ if (feedURI.schemeIs("http")) {
+ feedURI.scheme = "feed";
+ spec = feedURI.spec;
+ }
+ else
+ spec = "feed:" + spec;
+
+ // Retrieving the shell service might fail on some systems, most
+ // notably systems where GNOME is not installed.
+ try {
+ var ss = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ ss.openApplicationWithURI(clientApp, spec);
+ } catch(e) {
+ // If we couldn't use the shell service, fallback to using a
+ // nsIProcess instance
+ var p = Cc["@mozilla.org/process/util;1"]
+ .createInstance(Ci.nsIProcess);
+ p.init(clientApp);
+ p.run(false, [spec], 1);
+ }
+ break;
+
+ default:
+ // "web" should have been handled elsewhere
+ LOG("unexpected handler: " + handler);
+ // fall through
+ case "bookmarks":
+ var topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle)
+ .catch(Cu.reportError);
+ break;
+ case "messenger":
+ Cc["@mozilla.org/newsblog-feed-downloader;1"]
+ .getService(Ci.nsINewsBlogFeedDownloader)
+ .subscribeToFeed("feed:" + spec, null, null);
+ break;
+
+ }
+ },
+
+ /**
+ * See nsIFeedResultService.idl
+ */
+ addFeedResult: function addFeedResult(feedResult) {
+ if (feedResult == null)
+ throw new Error("null feedResult!");
+ if (feedResult.uri == null)
+ throw new Error("null URI!");
+ var spec = feedResult.uri.spec;
+ if (!this._results[spec])
+ this._results[spec] = [];
+ this._results[spec].push(feedResult);
+ },
+
+ /**
+ * See nsIFeedResultService.idl
+ */
+ getFeedResult: function getFeedResult(uri) {
+ if (uri == null)
+ throw new Error("null URI!");
+ var resultList = this._results[uri.spec];
+ for (let i = 0; i < resultList.length; ++i) {
+ if (resultList[i].uri == uri)
+ return resultList[i];
+ }
+ return null;
+ },
+
+ /**
+ * See nsIFeedResultService.idl
+ */
+ removeFeedResult: function removeFeedResult(uri) {
+ if (uri == null)
+ throw new Error("null URI!");
+ var resultList = this._results[uri.spec];
+ if (!resultList)
+ return;
+ var deletions = 0;
+ for (let i = 0; i < resultList.length; ++i) {
+ if (resultList[i].uri == uri) {
+ delete resultList[i];
+ ++deletions;
+ }
+ }
+
+ // send the holes to the end
+ resultList.sort();
+ // and trim the list
+ resultList.splice(resultList.length - deletions, deletions);
+ if (resultList.length == 0)
+ delete this._results[uri.spec];
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResultService]),
+ classID: Components.ID("{e5b05e9d-f037-48e4-b9a4-b99476582927}")
+};
+
+var components = [FeedConverter,
+ FeedResultService];
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/comm/suite/components/feeds/FeedWriter.js b/comm/suite/components/feeds/FeedWriter.js
new file mode 100644
index 0000000000..c02e914caf
--- /dev/null
+++ b/comm/suite/components/feeds/FeedWriter.js
@@ -0,0 +1,1211 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+const FEEDWRITER_CID = Components.ID("{49bb6593-3aff-4eb3-a068-2712c28bd58e}");
+const FEEDWRITER_CONTRACTID = "@mozilla.org/browser/feeds/result-writer;1";
+
+const XML_NS = "http://www.w3.org/XML/1998/namespace";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
+const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
+const STRING_BUNDLE_URI = "chrome://communicator/locale/feeds/subscribe.properties";
+
+const PREF_SELECTED_APP = "browser.feeds.handlers.application";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+
+const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
+const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
+const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
+const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
+
+const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
+const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
+const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
+const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
+
+const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI";
+
+const TITLE_ID = "feedTitleText";
+const SUBTITLE_ID = "feedSubtitleText";
+
+function getPrefAppForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_APP;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_APP;
+
+ default:
+ return PREF_SELECTED_APP;
+ }
+}
+
+function getPrefWebForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_WEB;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_WEB;
+
+ default:
+ return PREF_SELECTED_WEB;
+ }
+}
+
+function getPrefActionForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_ACTION;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_ACTION;
+
+ default:
+ return PREF_SELECTED_ACTION;
+ }
+}
+
+function getPrefReaderForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_READER;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_READER;
+
+ default:
+ return PREF_SELECTED_READER;
+ }
+}
+
+function LOG(str) {
+ if (Services.prefs.getBoolPref("feeds.log", false))
+ dump("*** Feeds: " + str + "\n");
+}
+
+/**
+ * Wrapper function for nsIIOService::newURI.
+ * @param aURLSpec
+ * The URL string from which to create an nsIURI.
+ * @returns an nsIURI object, or null if the creation of the URI failed.
+ */
+function makeURI(aURLSpec, aCharset) {
+ try {
+ return Services.io.newURI(aURLSpec, aCharset);
+ } catch (ex) {
+ }
+
+ return null;
+}
+
+/**
+ * Converts a number of bytes to the appropriate unit that results in a
+ * number that needs fewer than 4 digits
+ *
+ * @return a pair: [new value with 3 sig. figs., its unit]
+ */
+function convertByteUnits(aBytes) {
+ var units = ["bytes", "kilobytes", "megabytes", "gigabytes"];
+ var unitIndex = 0;
+
+ // convert to next unit if it needs 4 digits (after rounding), but only if
+ // we know the name of the next unit
+ while ((aBytes >= 999.5) && (unitIndex < units.length - 1)) {
+ aBytes /= 1024;
+ unitIndex++;
+ }
+
+ // Get rid of insignificant bits by truncating to 1 or 0 decimal points
+ // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
+ aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
+
+ return [aBytes, units[unitIndex]];
+}
+
+function FeedWriter() {
+ this._mimeSvc = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService);
+}
+
+FeedWriter.prototype = {
+ _getPropertyAsBag: function getPropertyAsBag(container, property) {
+ return container.fields.getProperty(property)
+ .QueryInterface(Ci.nsIPropertyBag2);
+ },
+
+ _getPropertyAsString: function getPropertyAsString(container, property) {
+ try {
+ return container.fields.getPropertyAsAString(property);
+ }
+ catch (e) {
+ }
+ return "";
+ },
+
+ /**
+ * @param element
+ * The element to add the text content to.
+ * @param text
+ * An nsIFeedTextConstruct
+ */
+ _setContentText: function setContentText(element, text) {
+ if (typeof element == "string")
+ element = this._document.getElementById(element);
+
+ // Takes the content of the nsIFeedTextConstruct and creates a
+ // sanitized documentFragment.
+ var docFragment = text.createDocumentFragment(element);
+ element.innerHTML = "";
+ element.appendChild(docFragment);
+ if (text.base)
+ element.setAttributeNS(XML_NS, "base", text.base.spec);
+ },
+
+ /**
+ * Safely sets the href attribute on an anchor tag, providing the URI
+ * specified can be loaded according to rules.
+ * @param element
+ * The element to set a URI attribute on
+ * @param attribute
+ * The attribute of the element to set the URI to, e.g. href or src
+ * @param uri
+ * The URI spec to set as the href
+ */
+ _safeSetURIAttribute: function safeSetURIAttribute(element, attribute, uri) {
+ const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL;
+ try {
+ Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags);
+ // checkLoadURIStrWithPrincipal will throw if the link URI should not be
+ // loaded, either because our feedURI isn't allowed to load it or per
+ // the rules specified in |flags|, so we'll never "linkify" the link...
+ element.setAttribute(attribute, uri);
+ }
+ catch (e) {
+ // Not allowed to load this link because checkLoadURIStrWithPrincipal threw
+ }
+ },
+
+ __bundle: null,
+ get _bundle() {
+ if (!this.__bundle) {
+ this.__bundle = Services.strings.createBundle(STRING_BUNDLE_URI);
+ }
+
+ return this.__bundle;
+ },
+
+ _getFormattedString: function getFormattedString(key, params) {
+ return this._bundle.formatStringFromName(key, params, params.length);
+ },
+
+ _getString: function getString(key) {
+ try {
+ return this._bundle.GetStringFromName(key);
+ } catch(e) {
+ LOG("Couldn't retrieve key from bundle");
+ }
+
+ return null;
+ },
+
+ /* Magic helper methods to be used instead of xbl properties */
+ _getSelectedItemFromMenulist: function getSelectedItemFromList(aList) {
+ return aList.getElementsByAttribute("selected", "true").item(0);
+ },
+
+ _setCheckboxCheckedState: function setCheckboxCheckedState(aCheckbox, aValue) {
+ // see checkbox.xml, xbl bindings are not visible through xrays!
+ var change = (aValue != (aCheckbox.getAttribute('checked') == 'true'));
+ if (aValue)
+ aCheckbox.setAttribute("checked", "true");
+ else
+ aCheckbox.removeAttribute("checked");
+
+ if (change) {
+ aCheckbox.dispatchEvent(new this._document.defaultView.Event(
+ "CheckboxStateChange", { bubbles: true, cancelable: true }));
+ }
+ },
+
+ /**
+ * Returns a date suitable for displaying in the feed preview.
+ * If the date cannot be parsed, the return value is "null".
+ * @param dateString
+ * A date as extracted from a feed entry. (entry.updated)
+ */
+ _parseDate: function parseDate(dateString) {
+ // Convert the date into the user's local time zone.
+ var dateObj = new Date(dateString);
+ // Make sure the date we're given is valid.
+ if (!dateObj.getTime())
+ return false;
+
+ return this._dateFormatter.format(dateObj);
+ },
+
+ __dateFormatter: null,
+ get _dateFormatter() {
+ if (!this.__dateFormatter) {
+ const dtOptions = { timeStyle: "short", dateStyle: "long" };
+ this.__dateFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
+ }
+ return this.__dateFormatter;
+ },
+
+ /**
+ * Returns the feed type.
+ */
+ __feedType: null,
+ _getFeedType: function getFeedType() {
+ if (this.__feedType != null)
+ return this.__feedType;
+
+ try {
+ // grab the feed because it's got the feed.type in it.
+ var container = this._getContainer();
+ var feed = container.QueryInterface(Ci.nsIFeed);
+ this.__feedType = feed.type;
+ return feed.type;
+ } catch (ex) {
+ }
+
+ return Ci.nsIFeed.TYPE_FEED;
+ },
+
+ /**
+ * Maps a feed type to a maybe-feed mimetype.
+ */
+ _getMimeTypeForFeedType: function getMimeTypeForFeedType() {
+ switch (this._getFeedType()) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return TYPE_MAYBE_VIDEO_FEED;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return TYPE_MAYBE_AUDIO_FEED;
+
+ default:
+ return TYPE_MAYBE_FEED;
+ }
+ },
+
+ /**
+ * Writes the feed title into the preview document.
+ * @param container
+ * The feed container, an nsIFeedContainer
+ */
+ _setTitleText: function setTitleText(container) {
+ if (container.title) {
+ this._setContentText(TITLE_ID, container.title);
+ this._document.title = container.title.plainText();
+ }
+
+ var feed = container.QueryInterface(Ci.nsIFeed);
+ if (feed && feed.subtitle)
+ this._setContentText(SUBTITLE_ID, feed.subtitle);
+ },
+
+ /**
+ * Writes the title image into the preview document if one is present.
+ * @param container
+ * The feed container
+ */
+ _setTitleImage: function setTitleImage(container) {
+ try {
+ var parts = container.image;
+
+ // Set up the title image (supplied by the feed)
+ var feedTitleImage = this._document.getElementById("feedTitleImage");
+ this._safeSetURIAttribute(feedTitleImage, "src",
+ parts.getPropertyAsAString("url"));
+
+ // Set up the title image link
+ var feedTitleLink = this._document.getElementById("feedTitleLink");
+
+ var titleText = this._getFormattedString("linkTitleTextFormat",
+ [parts.getPropertyAsAString("title")]);
+ feedTitleLink.setAttribute("title", titleText);
+ var titleImageWidth = parseInt(parts.getPropertyAsAString("width")) + 15;
+ feedTitleLink.style.MozMarginEnd = titleImageWidth + "px";
+
+ this._safeSetURIAttribute(feedTitleLink, "href",
+ parts.getPropertyAsAString("link"));
+ }
+ catch (e) {
+ LOG("Failed to set Title Image (this is benign): " + e);
+ }
+ },
+
+ /**
+ * Writes all entries contained in the feed.
+ * @param container
+ * The container of entries in the feed
+ */
+ _writeFeedContent: function writeFeedContent(container) {
+ // Build the actual feed content
+ var feed = container.QueryInterface(Ci.nsIFeed);
+ if (feed.items.length == 0)
+ return;
+
+ var feedContent = this._document.getElementById("feedContent");
+
+ for (let i = 0; i < feed.items.length; ++i) {
+ let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
+ entry.QueryInterface(Ci.nsIFeedContainer);
+
+ let entryContainer = this._document.createElementNS(HTML_NS, "div");
+ entryContainer.className = "entry";
+
+ // If the entry has a title, make it a link
+ if (entry.title) {
+ let a = this._document.createElementNS(HTML_NS, "a");
+ let span = this._document.createElementNS(HTML_NS, "span");
+ a.appendChild(span);
+ this._setContentText(span, entry.title);
+
+ // Entries are not required to have links, so entry.link can be null.
+ if (entry.link)
+ this._safeSetURIAttribute(a, "href", entry.link.spec);
+
+ let title = this._document.createElementNS(HTML_NS, "h3");
+ title.appendChild(a);
+
+ let lastUpdated = this._parseDate(entry.updated);
+ if (lastUpdated) {
+ let dateDiv = this._document.createElementNS(HTML_NS, "div");
+ dateDiv.className = "lastUpdated";
+ dateDiv.textContent = lastUpdated;
+ title.appendChild(dateDiv);
+ }
+
+ entryContainer.appendChild(title);
+ }
+
+ var body = this._document.createElementNS(HTML_NS, "div");
+ var summary = entry.summary || entry.content;
+ var docFragment = null;
+ if (summary) {
+ if (summary.base)
+ body.setAttributeNS(XML_NS, "base", summary.base.spec);
+ else
+ LOG("no base?");
+ docFragment = summary.createDocumentFragment(body);
+ if (docFragment)
+ body.appendChild(docFragment);
+
+ // If the entry doesn't have a title, append a # permalink
+ // See http://scripting.com/rss.xml for an example
+ if (!entry.title && entry.link) {
+ var a = this._document.createElementNS(HTML_NS, "a");
+ a.appendChild(this._document.createTextNode("#"));
+ this._safeSetURIAttribute(a, "href", entry.link.spec);
+ body.appendChild(this._document.createTextNode(" "));
+ body.appendChild(a);
+ }
+
+ }
+ body.className = "feedEntryContent";
+ entryContainer.appendChild(body);
+
+ if (entry.enclosures && entry.enclosures.length > 0) {
+ var enclosuresDiv = this._buildEnclosureDiv(entry);
+ entryContainer.appendChild(enclosuresDiv);
+ }
+
+ feedContent.appendChild(entryContainer);
+
+ var clearDiv = this._document.createElementNS(HTML_NS, "div");
+ clearDiv.style.clear = "both";
+ feedContent.appendChild(clearDiv);
+ }
+ },
+
+ /**
+ * Takes a url to a media item and returns the best name it can come up with.
+ * Frequently this is the filename portion (e.g. passing in
+ * http://example.com/foo.mpeg would return "foo.mpeg"), but in more complex
+ * cases, this will return the entire url (e.g. passing in
+ * http://example.com/somedirectory/ would return
+ * http://example.com/somedirectory/).
+ * @param aURL
+ * The URL string from which to create a display name
+ * @returns a string
+ */
+ _getURLDisplayName: function getURLDisplayName(aURL) {
+ var url = makeURI(aURL);
+
+ if ((url instanceof Ci.nsIURL) && url.fileName)
+ return decodeURIComponent(url.fileName);
+ return aURL;
+ },
+
+ /**
+ * Takes a FeedEntry with enclosures, generates the HTML code to represent
+ * them, and returns that.
+ * @param entry
+ * FeedEntry with enclosures
+ * @returns element
+ */
+ _buildEnclosureDiv: function buildEnclosureDiv(entry) {
+ var enclosuresDiv = this._document.createElementNS(HTML_NS, "div");
+ enclosuresDiv.className = "enclosures";
+
+ enclosuresDiv.appendChild(this._document.createTextNode(this._getString("mediaLabel")));
+
+ for (let i_enc = 0; i_enc < entry.enclosures.length; ++i_enc) {
+ let enc = entry.enclosures.queryElementAt(i_enc, Ci.nsIWritablePropertyBag2);
+
+ if (!(enc.hasKey("url")))
+ continue;
+
+ let enclosureDiv = this._document.createElementNS(HTML_NS, "div");
+ enclosureDiv.setAttribute("class", "enclosure");
+
+ let mozicon = "moz-icon://.txt?size=16";
+ let type_text = null;
+ let size_text = null;
+
+ if (enc.hasKey("type")) {
+ type_text = enc.get("type");
+ try {
+ let handlerInfoWrapper = this._mimeSvc.getFromTypeAndExtension(enc.get("type"), null);
+
+ if (handlerInfoWrapper)
+ type_text = handlerInfoWrapper.description;
+
+ if (type_text && type_text.length > 0)
+ mozicon = "moz-icon://goat?size=16&contentType=" + enc.get("type");
+
+ } catch (ex) {
+ }
+
+ }
+
+ if (enc.hasKey("length") && /^[0-9]+$/.test(enc.get("length"))) {
+ let enc_size = convertByteUnits(parseInt(enc.get("length")));
+
+ size_text = this._getFormattedString("enclosureSizeText",
+ [enc_size[0], this._getString(enc_size[1])]);
+ }
+
+ let iconimg = this._document.createElementNS(HTML_NS, "img");
+ iconimg.setAttribute("src", mozicon);
+ iconimg.setAttribute("class", "type-icon");
+ enclosureDiv.appendChild(iconimg);
+
+ enclosureDiv.appendChild(this._document.createTextNode( " " ));
+
+ let enc_href = this._document.createElementNS(HTML_NS, "a");
+ enc_href.appendChild(this._document.createTextNode(this._getURLDisplayName(enc.get("url"))));
+ this._safeSetURIAttribute(enc_href, "href", enc.get("url"));
+ enclosureDiv.appendChild(enc_href);
+
+ if (type_text && size_text)
+ enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ", " + size_text + ")"));
+
+ else if (type_text)
+ enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ")"))
+
+ else if (size_text)
+ enclosureDiv.appendChild(this._document.createTextNode( " (" + size_text + ")"))
+
+ enclosuresDiv.appendChild(enclosureDiv);
+ }
+
+ return enclosuresDiv;
+ },
+
+ /**
+ * Gets a valid nsIFeedContainer object from the parsed nsIFeedResult.
+ * Displays error information if there was one.
+ * @param result
+ * The parsed feed result
+ * @returns A valid nsIFeedContainer object containing the contents of
+ * the feed.
+ */
+ _getContainer: function getContainer(result) {
+ var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]
+ .getService(Ci.nsIFeedResultService);
+
+ try {
+ var result = feedService.getFeedResult(this._getOriginalURI(this._window));
+
+ if (result.bozo) {
+ LOG("Subscribe Preview: feed result is bozo?!");
+ }
+ }
+ catch (e) {
+ LOG("Subscribe Preview: feed not available?!");
+ }
+
+ try {
+ var container = result.doc;
+ }
+ catch (e) {
+ LOG("Subscribe Preview: no result.doc? Why didn't the original reload?");
+ return null;
+ }
+ return container;
+ },
+
+ /**
+ * Get the human-readable display name of a file. This could be the
+ * application name.
+ * @param file
+ * A nsIFile to look up the name of
+ * @returns The display name of the application represented by the file.
+ */
+ _getFileDisplayName: function getFileDisplayName(file) {
+ if (AppConstants.platform == "win" &&
+ file instanceof Ci.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ } else if (AppConstants.platform == "macosx" &&
+ file instanceof Ci.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ return file.leafName;
+ },
+
+ /**
+ * Get moz-icon url for a file
+ * @param file
+ * A nsIFile object for which the moz-icon:// is returned
+ * @returns moz-icon url of the given file as a string
+ */
+ _getFileIconURL: function getFileIconURL(file) {
+ var fph = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ var urlSpec = fph.getURLSpecFromFile(file);
+ return "moz-icon://" + urlSpec + "?size=16";
+ },
+
+ /**
+ * Helper method to set the selected application and system default
+ * reader menuitems details from a file object
+ * @param aMenuItem
+ * The menuitem on which the attributes should be set
+ * @param aFile
+ * The menuitem's associated file
+ */
+ _initMenuItemWithFile: function(aMenuItem, aFile) {
+ aMenuItem.setAttribute("label", this._getFileDisplayName(aFile));
+ aMenuItem.setAttribute("image", this._getFileIconURL(aFile));
+ },
+
+ /**
+ * Helper method to get an element in the XBL binding where the handler
+ * selection UI lives
+ */
+ _getUIElement: function getUIElement(id) {
+ return this._document.getAnonymousElementByAttribute(
+ this._document.getElementById("feedSubscribeLine"), "anonid", id);
+ },
+
+ /**
+ * Displays a prompt from which the user may choose a (client) feed reader.
+ * @param aCallback the callback method, passes in true if a feed reader was
+ * selected, false otherwise.
+ */
+ _chooseClientApp: function chooseClientApp(aCallback) {
+ try {
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == Ci.nsIFilePicker.returnOK) {
+ this._selectedApp = fp.file;
+ if (this._selectedApp) {
+ let file = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ if (fp.file.leafName != file.leafName) {
+ this._initMenuItemWithFile(this._selectedAppMenuItem,
+ this._selectedApp);
+
+ // Show and select the selected application menuitem
+ this._selectedAppMenuItem.hidden = false;
+ this._selectedAppMenuItem.doCommand();
+ if (aCallback) {
+ aCallback(true);
+ return;
+ }
+ }
+ }
+ }
+ if (aCallback) {
+ aCallback(false);
+ }
+ }.bind(this);
+
+ fp.init(this._window,
+ this._getString("chooseApplicationDialogTitle"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+ fp.open(fpCallback);
+ } catch(ex) {}
+ },
+
+ _setAlwaysUseCheckedState: function setAlwaysUseCheckedState(feedType) {
+ var checkbox = this._getUIElement("alwaysUse");
+ if (checkbox) {
+ var alwaysUse = (Services.prefs.getCharPref(getPrefActionForType(feedType), "ask") != "ask");
+ this._setCheckboxCheckedState(checkbox, alwaysUse);
+ }
+ },
+
+ _setSubscribeUsingLabel: function setSubscribeUsingLabel() {
+ var stringLabel = "subscribeFeedUsing";
+ switch (this._getFeedType()) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ stringLabel = "subscribeVideoPodcastUsing";
+ break;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ stringLabel = "subscribeAudioPodcastUsing";
+ break;
+ }
+
+ var subscribeUsing = this._getUIElement("subscribeUsingDescription");
+ subscribeUsing.setAttribute("value", this._getString(stringLabel));
+ },
+
+ _setAlwaysUseLabel: function setAlwaysUseLabel() {
+ var checkbox = this._getUIElement("alwaysUse");
+ if (checkbox) {
+ if (this._handlersMenuList) {
+ var handlerName = this._getSelectedItemFromMenulist(this._handlersMenuList)
+ .getAttribute("label");
+ var stringLabel = "alwaysUseForFeeds";
+ switch (this._getFeedType()) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ stringLabel = "alwaysUseForVideoPodcasts";
+ break;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ stringLabel = "alwaysUseForAudioPodcasts";
+ break;
+ }
+
+ checkbox.setAttribute("label", this._getFormattedString(stringLabel, [handlerName]));
+ }
+ }
+ },
+
+ // nsIDOMEventListener
+ handleEvent: function(event) {
+ if (event.target != this._document &&
+ event.target.ownerDocument != this._document) {
+ LOG("FeedWriter.handleEvent: Someone passed the feed writer as a listener to the events of another document!");
+ return;
+ }
+
+ if (event.type == "load")
+ this._writeContent();
+ else if (event.type == "unload")
+ this._close();
+ else if (event.type == "command") {
+ switch (event.target.getAttribute("anonid")) {
+ case "subscribeButton":
+ this._subscribe();
+ break;
+ case "chooseApplicationMenuItem":
+ /* Bug 351263: Make sure to not steal focus if the "Choose
+ * Application" item is being selected with the keyboard. We do this
+ * by ignoring command events while the dropdown is closed (user
+ * arrowing through the combobox), but handling them while the
+ * combobox dropdown is open (user pressed enter when an item was
+ * selected). If we don't show the filepicker here, it will be shown
+ * when clicking "Subscribe Now".
+ */
+ var popupbox = this._handlersMenuList.firstChild.boxObject;
+ if (popupbox.popupState == "hiding") {
+ this._chooseClientApp(function(aResult) {
+ if (!aResult) {
+ // Select the (per-prefs) selected handler if no application
+ // was selected
+ this._setSelectedHandler(this._getFeedType());
+ }
+ }.bind(this));
+ }
+ break;
+ default:
+ this._setAlwaysUseLabel();
+ }
+ }
+ },
+
+ _setSelectedHandler: function setSelectedHandler(feedType) {
+ var handler = Services.prefs.getCharPref(getPrefReaderForType(feedType), "messenger");
+
+ switch (handler) {
+ case "web":
+ if (this._handlersMenuList) {
+ var url = Services.prefs.getStringPref(getPrefWebForType(feedType));
+ var handlers = this._handlersMenuList.getElementsByAttribute("webhandlerurl", url);
+ if (handlers.length == 0) {
+ LOG("FeedWriter._setSelectedHandler: selected web handler isn't in the menulist");
+ return;
+ }
+
+ handlers[0].doCommand();
+ }
+ break;
+ case "client":
+ try {
+ this._selectedApp =
+ Services.prefs.getComplexValue(getPrefAppForType(feedType),
+ Ci.nsIFile);
+ }
+ catch(ex) {
+ this._selectedApp = null;
+ }
+
+ if (this._selectedApp) {
+ this._initMenuItemWithFile(this._selectedAppMenuItem,
+ this._selectedApp);
+ this._selectedAppMenuItem.hidden = false;
+ this._selectedAppMenuItem.doCommand();
+
+ // Only show the default reader menuitem if the default reader
+ // isn't the selected application
+ if (this._defaultSystemReader) {
+ var shouldHide = this._defaultSystemReader.path == this._selectedApp.path;
+ this._defaultHandlerMenuItem.hidden = shouldHide;
+ }
+ break;
+ }
+ case "bookmarks":
+ var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem");
+ if (liveBookmarksMenuItem)
+ liveBookmarksMenuItem.doCommand();
+ break;
+ // fall through if this._selectedApp is null
+ default:
+ var messengerFeedsMenuItem = this._getUIElement("messengerFeedsMenuItem");
+ if (messengerFeedsMenuItem)
+ messengerFeedsMenuItem.doCommand();
+ break;
+ }
+ },
+
+ _initSubscriptionUI: function initSubscriptionUI() {
+ var handlersMenuPopup = this._getUIElement("handlersMenuPopup");
+ if (!handlersMenuPopup)
+ return;
+
+ var feedType = this._getFeedType();
+
+ // change the background
+ var header = this._document.getElementById("feedHeader");
+ switch (feedType) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ header.className = "videoPodcastBackground";
+ break;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ header.className = "audioPodcastBackground";
+ break;
+
+ default:
+ header.className = "feedBackground";
+ }
+
+ var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem");
+
+ // Last-selected application
+ var menuItem = liveBookmarksMenuItem.cloneNode(false);
+ menuItem.removeAttribute("selected");
+ menuItem.setAttribute("anonid", "selectedAppMenuItem");
+ menuItem.className = "menuitem-iconic selectedAppMenuItem";
+ menuItem.setAttribute("handlerType", "client");
+ try {
+ this._selectedApp = Services.prefs.getComplexValue(getPrefAppForType(feedType),
+ Ci.nsIFile);
+
+ if (this._selectedApp.exists())
+ this._initMenuItemWithFile(menuItem, this._selectedApp);
+ else {
+ // Hide the menuitem if the last selected application doesn't exist
+ menuItem.hidden = true;
+ }
+ }
+ catch(ex) {
+ // Hide the menuitem until an application is selected
+ menuItem.hidden = true;
+ }
+ this._selectedAppMenuItem = menuItem;
+ handlersMenuPopup.appendChild(menuItem);
+
+ // List the default feed reader
+ try {
+ this._defaultSystemReader = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .defaultFeedReader;
+ menuItem = liveBookmarksMenuItem.cloneNode(false);
+ menuItem.removeAttribute("selected");
+ menuItem.setAttribute("anonid", "defaultHandlerMenuItem");
+ menuItem.className = "menuitem-iconic defaultHandlerMenuItem";
+ menuItem.setAttribute("handlerType", "client");
+
+ this._initMenuItemWithFile(menuItem, this._defaultSystemReader);
+
+ // Hide the default reader item if it points to the same application
+ // as the last-selected application
+ if (this._selectedApp &&
+ this._selectedApp.path == this._defaultSystemReader.path)
+ menuItem.hidden = true;
+ }
+ catch(ex) {
+ }
+
+ if (menuItem) {
+ this._defaultHandlerMenuItem = menuItem;
+ handlersMenuPopup.appendChild(menuItem);
+ }
+
+ // "Choose Application..." menuitem
+ menuItem = liveBookmarksMenuItem.cloneNode(false);
+ menuItem.removeAttribute("selected");
+ menuItem.setAttribute("anonid", "chooseApplicationMenuItem");
+ menuItem.className = "menuitem-iconic chooseApplicationMenuItem";
+ menuItem.setAttribute("label", this._getString("chooseApplicationMenuItem"));
+ handlersMenuPopup.appendChild(menuItem);
+
+ // separator
+ menuItem = liveBookmarksMenuItem.nextSibling.cloneNode(false);
+ handlersMenuPopup.appendChild(menuItem);
+
+ // List of web handlers
+ var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]
+ .getService(Ci.nsIWebContentConverterService);
+ var handlers = wccr.getContentHandlers(this._getMimeTypeForFeedType(feedType));
+ if (handlers.length != 0) {
+ for (let i = 0; i < handlers.length; ++i) {
+ menuItem = liveBookmarksMenuItem.cloneNode(false);
+ menuItem.removeAttribute("selected");
+ menuItem.className = "menuitem-iconic";
+ menuItem.setAttribute("label", handlers[i].name);
+ menuItem.setAttribute("handlerType", "web");
+ menuItem.setAttribute("webhandlerurl", handlers[i].uri);
+ handlersMenuPopup.appendChild(menuItem);
+ }
+ }
+
+ this._setSelectedHandler(feedType);
+
+ // "Subscribe using..."
+ this._setSubscribeUsingLabel();
+
+ // "Always use..." checkbox initial state
+ this._setAlwaysUseCheckedState(feedType);
+ this._setAlwaysUseLabel();
+
+ // We update the "Always use.." checkbox label whenever the selected item
+ // in the list is changed
+ handlersMenuPopup.addEventListener("command", this);
+
+ // Set up the "Subscribe Now" button
+ this._getUIElement("subscribeButton")
+ .addEventListener("command", this);
+
+ // first-run ui
+ var showFirstRunUI = true;
+ try {
+ showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI);
+ }
+ catch (ex) {
+ }
+ if (showFirstRunUI) {
+ var textfeedinfo1, textfeedinfo2;
+ switch (feedType) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ textfeedinfo1 = "feedSubscriptionVideoPodcast1";
+ textfeedinfo2 = "feedSubscriptionVideoPodcast2";
+ break;
+ case Ci.nsIFeed.TYPE_AUDIO:
+ textfeedinfo1 = "feedSubscriptionAudioPodcast1";
+ textfeedinfo2 = "feedSubscriptionAudioPodcast2";
+ break;
+ default:
+ textfeedinfo1 = "feedSubscriptionFeed1";
+ textfeedinfo2 = "feedSubscriptionFeed2";
+ }
+
+ this._document.getElementById("feedSubscriptionInfo1").textContent =
+ this._getString(textfeedinfo1);
+ this._document.getElementById("feedSubscriptionInfo2").textContent =
+ this._getString(textfeedinfo2);
+ header.setAttribute("firstrun", "true");
+ Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false);
+ }
+ },
+
+ /**
+ * Returns the original URI object of the feed and ensures that this
+ * component is only ever invoked from the preview document.
+ * @param aWindow
+ * The window of the document invoking the BrowserFeedWriter
+ */
+ _getOriginalURI: function getOriginalURI(aWindow) {
+ let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let chan = docShell.currentDocumentChannel;
+
+ // We probably need to call Inherit() for this, but right now we can't call
+ // it from JS.
+ let attrs = docShell.getOriginAttributes();
+ let nullPrincipal = Services.scriptSecurityManager
+ .createNullPrincipal(attrs);
+
+ // This channel is not going to be opened, use a nullPrincipal
+ // and the most restrictive securityFlag.
+ let resolvedURI = NetUtil.newChannel({
+ uri: "about:feeds",
+ loadingPrincipal: nullPrincipal,
+ securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
+ }).URI;
+
+ if (resolvedURI.equals(chan.URI))
+ return chan.originalURI;
+
+ return null;
+ },
+
+ _window: null,
+ _document: null,
+ _feedURI: null,
+ _feedPrincipal: null,
+ _handlersMenuList: null,
+ _selectedAppMenuItem: null,
+ _defaultHandlerMenuItem: null,
+
+ // nsIDOMGlobalPropertyInitializer
+ init: function init(aWindow) {
+ this._feedURI = this._getOriginalURI(aWindow);
+ if (!this._feedURI)
+ return;
+
+ this._window = aWindow;
+ this._document = aWindow.document;
+ this._handlersMenuList = this._getUIElement("handlersMenuList");
+
+ this._feedPrincipal = Services.scriptSecurityManager
+ .createCodebasePrincipal(this._feedURI, {});
+
+ LOG("Subscribe Preview: feed uri = " + this._window.location.href);
+
+ // Set up the subscription UI
+ this._initSubscriptionUI();
+ Services.prefs.addObserver(PREF_SELECTED_ACTION, this);
+ Services.prefs.addObserver(PREF_SELECTED_READER, this);
+ Services.prefs.addObserver(PREF_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_SELECTED_APP, this);
+ Services.prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this);
+ Services.prefs.addObserver(PREF_VIDEO_SELECTED_READER, this);
+ Services.prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_VIDEO_SELECTED_APP, this);
+
+ Services.prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this);
+ Services.prefs.addObserver(PREF_AUDIO_SELECTED_READER, this);
+ Services.prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_AUDIO_SELECTED_APP, this);
+
+ this._window.addEventListener("load", this);
+ this._window.addEventListener("unload", this);
+ },
+
+ _writeContent: function writeContent() {
+ if (!this._window)
+ return;
+
+ try {
+ // Set up the feed content
+ var container = this._getContainer();
+ if (!container)
+ return;
+
+ this._setTitleText(container);
+ this._setTitleImage(container);
+ this._writeFeedContent(container);
+ }
+ finally {
+ this._removeFeedFromCache();
+ }
+ },
+
+ _close: function close() {
+ this._window.removeEventListener("load", this);
+ this._window.removeEventListener("unload", this);
+ this._getUIElement("handlersMenuPopup")
+ .removeEventListener("command", this);
+ this._getUIElement("subscribeButton")
+ .removeEventListener("command", this);
+ this._document = null;
+ this._window = null;
+ Services.prefs.removeObserver(PREF_SELECTED_ACTION, this);
+ Services.prefs.removeObserver(PREF_SELECTED_READER, this);
+ Services.prefs.removeObserver(PREF_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_SELECTED_APP, this);
+ Services.prefs.removeObserver(PREF_VIDEO_SELECTED_ACTION, this);
+ Services.prefs.removeObserver(PREF_VIDEO_SELECTED_READER, this);
+ Services.prefs.removeObserver(PREF_VIDEO_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_VIDEO_SELECTED_APP, this);
+
+ Services.prefs.removeObserver(PREF_AUDIO_SELECTED_ACTION, this);
+ Services.prefs.removeObserver(PREF_AUDIO_SELECTED_READER, this);
+ Services.prefs.removeObserver(PREF_AUDIO_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_AUDIO_SELECTED_APP, this);
+
+ this._removeFeedFromCache();
+ this.__bundle = null;
+ this._feedURI = null;
+ this._selectedAppMenuItem = null;
+ this._defaultHandlerMenuItem = null;
+ },
+
+ _removeFeedFromCache: function removeFeedFromCache() {
+ if (this._feedURI) {
+ var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]
+ .getService(Ci.nsIFeedResultService);
+ feedService.removeFeedResult(this._feedURI);
+ this._feedURI = null;
+ }
+ },
+
+ _subscribe: function subscribe() {
+ var feedType = this._getFeedType();
+
+ // Subscribe to the feed using the selected handler and save prefs
+ var defaultHandler = "reader";
+ var useAsDefault = this._getUIElement("alwaysUse").getAttribute("checked");
+
+ var selectedItem = this._getSelectedItemFromMenulist(this._handlersMenuList);
+ let subscribeCallback = function() {
+ if (selectedItem.hasAttribute("webhandlerurl")) {
+ var webURI = selectedItem.getAttribute("webhandlerurl");
+ Services.prefs.setCharPref(getPrefReaderForType(feedType), "web");
+
+ Services.prefs.setStringPref(getPrefWebForType(feedType), webURI);
+
+ var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]
+ .getService(Ci.nsIWebContentConverterService);
+ var handler = wccr.getWebContentHandlerByURI(this._getMimeTypeForFeedType(feedType), webURI);
+ if (handler) {
+ if (useAsDefault)
+ wccr.setAutoHandler(this._getMimeTypeForFeedType(feedType), handler);
+
+ this._window.location.href = handler.getHandlerURI(this._window.location.href);
+ }
+ }
+ else {
+ switch (selectedItem.getAttribute("anonid")) {
+ case "selectedAppMenuItem":
+ Services.prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsIFile,
+ this._selectedApp);
+ Services.prefs.setCharPref(getPrefReaderForType(feedType), "client");
+ break;
+ case "defaultHandlerMenuItem":
+ Services.prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsIFile,
+ this._defaultSystemReader);
+ Services.prefs.setCharPref(getPrefReaderForType(feedType), "client");
+ break;
+ case "liveBookmarksMenuItem":
+ defaultHandler = "bookmarks";
+ Services.prefs.setCharPref(getPrefReaderForType(feedType), "bookmarks");
+ break;
+ case "messengerFeedsMenuItem":
+ defaultHandler = "messenger";
+ Services.prefs.setCharPref(getPrefReaderForType(feedType), "messenger");
+ break;
+ }
+ var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]
+ .getService(Ci.nsIFeedResultService);
+
+ // Pull the title and subtitle out of the document
+ var feedTitle = this._document.getElementById(TITLE_ID).textContent;
+ var feedSubtitle = this._document.getElementById(SUBTITLE_ID).textContent;
+ feedService.addToClientReader(this._window.location.href, feedTitle, feedSubtitle, feedType);
+ }
+
+ // If "Always use..." is checked, we should set PREF_*SELECTED_ACTION
+ // to either "reader" (If a web reader or if an application is selected),
+ // or to "messenger" (if the messenger feeds option is selected).
+ // Otherwise, we should set it to "ask"
+ if (useAsDefault) {
+ Services.prefs.setCharPref(getPrefActionForType(feedType), defaultHandler);
+ } else {
+ Services.prefs.setCharPref(getPrefActionForType(feedType), "ask");
+ }
+ }.bind(this);
+
+ // Show the file picker before subscribing if the
+ // choose application menuitem was chosen using the keyboard
+ if (selectedItem.getAttribute("anonid") == "chooseApplicationMenuItem") {
+ this._chooseClientApp(function(aResult) {
+ if (aResult) {
+ selectedItem =
+ this._getSelectedItemFromMenulist(this._handlersMenuList);
+ subscribeCallback();
+ }
+ }.bind(this));
+ } else {
+ subscribeCallback();
+ }
+ },
+
+ // nsIObserver
+ observe: function observe(subject, topic, data) {
+ if (!this._window) {
+ // this._window is null unless this.init was called with a trusted
+ // window object.
+ return;
+ }
+
+ var feedType = this._getFeedType();
+
+ if (topic == "nsPref:changed") {
+ switch (data) {
+ case PREF_SELECTED_READER:
+ case PREF_SELECTED_WEB:
+ case PREF_SELECTED_APP:
+ case PREF_VIDEO_SELECTED_READER:
+ case PREF_VIDEO_SELECTED_WEB:
+ case PREF_VIDEO_SELECTED_APP:
+ case PREF_AUDIO_SELECTED_READER:
+ case PREF_AUDIO_SELECTED_WEB:
+ case PREF_AUDIO_SELECTED_APP:
+ this._setSelectedHandler(feedType);
+ break;
+ case PREF_SELECTED_ACTION:
+ case PREF_VIDEO_SELECTED_ACTION:
+ case PREF_AUDIO_SELECTED_ACTION:
+ this._setAlwaysUseCheckedState(feedType);
+ }
+ }
+ },
+
+ classID: FEEDWRITER_CID,
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver])
+
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([FeedWriter]);
diff --git a/comm/suite/components/feeds/SuiteFeeds.manifest b/comm/suite/components/feeds/SuiteFeeds.manifest
new file mode 100644
index 0000000000..e44e0c4c31
--- /dev/null
+++ b/comm/suite/components/feeds/SuiteFeeds.manifest
@@ -0,0 +1,11 @@
+component {88592f45-3866-4c8e-9d8a-ab58b290fcf7} FeedConverter.js
+contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7}
+contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.video.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7}
+contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.audio.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7}
+component {e5b05e9d-f037-48e4-b9a4-b99476582927} FeedConverter.js
+contract @mozilla.org/browser/feeds/result-service;1 {e5b05e9d-f037-48e4-b9a4-b99476582927}
+component {49bb6593-3aff-4eb3-a068-2712c28bd58e} FeedWriter.js
+contract @mozilla.org/browser/feeds/result-writer;1 {49bb6593-3aff-4eb3-a068-2712c28bd58e}
+component {792a7e82-06a0-437c-af63-b2d12e808acc} WebContentConverter.js
+contract @mozilla.org/embeddor.implemented/web-content-handler-registrar;1 {792a7e82-06a0-437c-af63-b2d12e808acc}
+category app-startup WebContentConverter service,@mozilla.org/embeddor.implemented/web-content-handler-registrar;1
diff --git a/comm/suite/components/feeds/WebContentConverter.js b/comm/suite/components/feeds/WebContentConverter.js
new file mode 100644
index 0000000000..ef519c7df2
--- /dev/null
+++ b/comm/suite/components/feeds/WebContentConverter.js
@@ -0,0 +1,818 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1";
+const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}");
+
+const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}");
+const WCC_CLASSNAME = "Web Service Handler";
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_ANY = "*/*";
+
+const PREF_CONTENTHANDLERS_AUTO = "browser.contentHandlers.auto.";
+const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types.";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+const PREF_HANDLER_EXTERNAL_PREFIX = "network.protocol-handler.external";
+const PREF_ALLOW_DIFFERENT_HOST = "gecko.handlerService.allowRegisterFromDifferentHost";
+
+const STRING_BUNDLE_URI = "chrome://communicator/locale/feeds/subscribe.properties";
+
+const NS_ERROR_MODULE_DOM = 0x80530000;
+const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12;
+
+function LOG(str) {
+ try {
+ if (Services.prefs.getBoolPref("feeds.log"))
+ dump("*** Feeds: " + str + "\n");
+ }
+ catch (ex) {
+ }
+}
+
+function getNotificationBox(aWindow)
+{
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.parentNode.wrappedJSObject;
+}
+
+function WebContentConverter() {
+}
+
+WebContentConverter.prototype = {
+ convert: function convert() { },
+ asyncConvertData: function asyncConvertData() { },
+ onDataAvailable: function onDataAvailable() { },
+ onStopRequest: function onStopRequest() { },
+
+ onStartRequest: function onStartRequest(request, context) {
+ var wccr = Cc[WCCR_CONTRACTID]
+ .getService(Ci.nsIWebContentConverterService);
+ wccr.loadPreferredHandler(request);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIStreamConverter,
+ Ci.nsIStreamListener])
+};
+
+var WebContentConverterFactory = {
+ createInstance: function createInstance(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return new WebContentConverter().QueryInterface(iid);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFactory])
+};
+
+function ServiceInfo(contentType, uri, name) {
+ this._contentType = contentType;
+ this._uri = uri;
+ this._name = name;
+}
+
+ServiceInfo.prototype = {
+ /**
+ * See nsIHandlerApp
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * See nsIHandlerApp
+ */
+ equals: function equals(aHandlerApp) {
+ if (!aHandlerApp)
+ throw Cr.NS_ERROR_NULL_POINTER;
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo &&
+ aHandlerApp.contentType == this.contentType &&
+ aHandlerApp.uri == this.uri)
+ return true;
+
+ return false;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ get contentType() {
+ return this._contentType;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ get uri() {
+ return this._uri;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ getHandlerURI: function getHandlerURI(uri) {
+ return this._uri.replace(/%s/gi, encodeURIComponent(uri));
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIWebContentHandlerInfo])
+};
+
+function WebContentConverterRegistrar() {
+ this._contentTypes = { };
+ this._autoHandleContentTypes = { };
+}
+
+WebContentConverterRegistrar.prototype = {
+ __bundle: null,
+ get _bundle() {
+ if (!this.__bundle) {
+ this.__bundle = Services.strings.createBundle(STRING_BUNDLE_URI);
+ }
+ return this.__bundle;
+ },
+
+ _getFormattedString: function getFormattedString(key, params) {
+ return this._bundle.formatStringFromName(key, params, params.length);
+ },
+
+ _getString: function getString(key) {
+ try {
+ return this._bundle.GetStringFromName(key);
+ } catch(e) {
+ LOG("Couldn't retrieve key from bundle");
+ }
+
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getAutoHandler: function getAutoHandler(contentType) {
+ contentType = this._resolveContentType(contentType);
+ if (contentType in this._autoHandleContentTypes)
+ return this._autoHandleContentTypes[contentType];
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ setAutoHandler: function setAutoHandler(contentType, handler) {
+ if (handler && !this._typeIsRegistered(contentType, handler.uri))
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ contentType = this._resolveContentType(contentType);
+ this._setAutoHandler(contentType, handler);
+
+ var autoBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_AUTO);
+ if (handler)
+ autoBranch.setCharPref(contentType, handler.uri);
+ else if (autoBranch.prefHasUserValue(contentType))
+ autoBranch.clearUserPref(contentType);
+
+ Services.prefs.savePrefFile(null);
+ },
+
+ /**
+ * Update the internal data structure (not persistent)
+ */
+ _setAutoHandler: function setAutoHandler(contentType, handler) {
+ if (handler)
+ this._autoHandleContentTypes[contentType] = handler;
+ else if (contentType in this._autoHandleContentTypes)
+ delete this._autoHandleContentTypes[contentType];
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getWebContentHandlerByURI: function getWebContentHandlerByURI(contentType, uri) {
+ var handlers = this.getContentHandlers(contentType, { });
+ for (let i = 0; i < handlers.length; ++i) {
+ if (handlers[i].uri == uri)
+ return handlers[i];
+ }
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ loadPreferredHandler: function loadPreferredHandler(request) {
+ var channel = request.QueryInterface(Ci.nsIChannel);
+ var contentType = this._resolveContentType(channel.contentType);
+ var handler = this.getAutoHandler(contentType);
+ if (handler) {
+ request.cancel(Cr.NS_ERROR_FAILURE);
+
+ let triggeringPrincipal = channel.loadInfo
+ ? channel.loadInfo.triggeringPrincipal
+ : Services.scriptSecurityManager.getSystemPrincipal();
+
+ let webNavigation = channel.notificationCallbacks
+ .getInterface(Ci.nsIWebNavigation);
+ webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null, triggeringPrincipal);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ removeProtocolHandler: function removeProtocolHandler(aProtocol, aURITemplate) {
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ var handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = 0; i < handlers.length; i++) {
+ try { // We only want to test web handlers
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ if (handler.uriTemplate == aURITemplate) {
+ handlers.removeElementAt(i);
+ let hs = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ return;
+ }
+ } catch (e) {
+ /* it wasn't a web handler */
+ }
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ removeContentHandler: function removeContentHandler(contentType, uri) {
+ function notURI(serviceInfo) {
+ return serviceInfo.uri != uri;
+ }
+
+ if (contentType in this._contentTypes) {
+ this._contentTypes[contentType] = this._contentTypes[contentType]
+ .filter(notURI);
+ }
+ },
+
+ /**
+ *
+ */
+ _mappings: {
+ "application/rss+xml": TYPE_MAYBE_FEED,
+ "application/atom+xml": TYPE_MAYBE_FEED,
+ },
+
+ /**
+ * These are types for which there is a separate content converter aside
+ * from our built in generic one. We should not automatically register
+ * a factory for creating a converter for these types.
+ */
+ _blockedTypes: {
+ "application/vnd.mozilla.maybe.feed": true,
+ },
+
+ /**
+ * Determines the "internal" content type based on the _mappings.
+ * @param contentType
+ * @returns The resolved contentType value.
+ */
+ _resolveContentType: function resolveContentType(contentType) {
+ if (contentType in this._mappings)
+ return this._mappings[contentType];
+ return contentType;
+ },
+
+ _makeURI: function(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ _checkAndGetURI: function checkAndGetURI(aURIString, aContentWindow) {
+ try {
+ var uri = this._makeURI(aURIString);
+ } catch (ex) {
+ // not supposed to throw according to spec
+ return;
+ }
+
+ // For security reasons we reject non-http(s) urls (see bug 354316),
+ // we may need to revise this once we support more content types
+ // XXX this should be a "security exception" according to spec, but that
+ // isn't defined yet.
+ if (uri.scheme != "http" && uri.scheme != "https")
+ throw("Permission denied to add " + uri.spec + " as a content or protocol handler");
+
+ // We also reject handlers registered from a different host (see bug 402287)
+ // The pref allows us to test the feature
+ if ((!Services.prefs.prefHasUserValue(PREF_ALLOW_DIFFERENT_HOST) ||
+ !Services.prefs.getBoolPref(PREF_ALLOW_DIFFERENT_HOST)) &&
+ aContentWindow.location.hostname != uri.host)
+ throw("Permission denied to add " + uri.spec + " as a content or protocol handler");
+
+ // If the uri doesn't contain '%s', it won't be a good handler
+ if (!uri.spec.includes("%s"))
+ throw NS_ERROR_DOM_SYNTAX_ERR;
+
+ return uri;
+ },
+
+ /**
+ * Determines if a web handler is already registered.
+ *
+ * @param aProtocol
+ * The scheme of the web handler we are checking for.
+ * @param aURITemplate
+ * The URI template that the handler uses to handle the protocol.
+ * @return true if it is already registered, false otherwise.
+ */
+ _protocolHandlerRegistered: function protocolHandlerRegistered(aProtocol, aURITemplate) {
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ var handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = 0; i < handlers.length; i++) {
+ try { // We only want to test web handlers
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ if (handler.uriTemplate == aURITemplate)
+ return true;
+ } catch (e) { /* it wasn't a web handler */ }
+ }
+ return false;
+ },
+
+ /**
+ * See nsIWebContentHandlerRegistrar
+ */
+ registerProtocolHandler: function registerProtocolHandler(aProtocol, aURIString,
+ aTitle, aContentWindow) {
+ LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")");
+
+ var win = aContentWindow;
+ var isPB = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing;
+ if (isPB) {
+ // Inside the private browsing mode, we don't want to alert the user to save
+ // a protocol handler. We log it to the error console so that web developers
+ // would have some way to tell what's going wrong.
+ Services.console.logStringMessage("Web page denied access to register a protocol handler inside private browsing mode");
+ return;
+ }
+
+ // First, check to make sure this isn't already handled internally (we don't
+ // want to let them take over, say "chrome").
+ var handler = Services.io.getProtocolHandler(aProtocol);
+ if (!(handler instanceof Ci.nsIExternalProtocolHandler)) {
+ // This is handled internally, so we don't want them to register
+ Services.console.logStringMessage("Permission denied to add " + aURIString + " as a protocol handler");
+ return;
+ }
+
+ // check if it is in the black list
+ var allowed;
+ try {
+ allowed = Services.prefs.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol);
+ }
+ catch (e) {
+ allowed = Services.prefs.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default");
+ }
+ if (!allowed) {
+ Services.console.logStringMessage("Not allowed to register a protocol handler for " + aProtocol);
+ return;
+ }
+
+ var uri = this._checkAndGetURI(aURIString, aContentWindow);
+
+ var buttons, message;
+ if (this._protocolHandlerRegistered(aProtocol, uri.spec))
+ message = this._getFormattedString("protocolHandlerRegistered",
+ [aTitle, aProtocol]);
+ else {
+ // Now Ask the user and provide the proper callback
+ message = this._getFormattedString("addProtocolHandler",
+ [aTitle, uri.host, aProtocol]);
+ var notificationIcon = uri.resolve("/favicon.ico");
+ var notificationValue = "Protocol Registration: " + aProtocol;
+ var addButton = {
+ label: this._getString("addProtocolHandlerAddButton"),
+ accessKey: this._getString("addHandlerAddButtonAccesskey"),
+ protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle },
+
+ callback: function addProtocolHandlerButtonCallback(aNotification, aButtonInfo) {
+ var protocol = aButtonInfo.protocolInfo.protocol;
+ var uri = aButtonInfo.protocolInfo.uri;
+ var name = aButtonInfo.protocolInfo.name;
+
+ var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]
+ .createInstance(Ci.nsIWebHandlerApp);
+ handler.name = name;
+ handler.uriTemplate = uri;
+
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(protocol);
+ handlerInfo.possibleApplicationHandlers.appendElement(handler);
+
+ // Since the user has agreed to add a new handler, chances are good
+ // that the next time they see a handler of this type, they're going
+ // to want to use it. Reset the handlerInfo to ask before the next
+ // use.
+ handlerInfo.alwaysAskBeforeHandling = true;
+
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ }
+ };
+ buttons = [addButton];
+ }
+
+ var notificationBox = getNotificationBox(aContentWindow);
+ notificationBox.appendNotification(message,
+ notificationValue,
+ notificationIcon,
+ notificationBox.PRIORITY_INFO_LOW,
+ buttons);
+ },
+
+ /**
+ * See nsIWebContentHandlerRegistrar
+ * If a DOM window is provided, then the request came from content, so we
+ * prompt the user to confirm the registration.
+ */
+ registerContentHandler: function registerContentHandler(aContentType, aURIString,
+ aTitle, aContentWindow) {
+ LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")");
+
+ // We only support feed types at present.
+ // XXX this should be a "security exception" according to spec, but that
+ // isn't defined yet.
+ var contentType = this._resolveContentType(aContentType);
+ if (contentType != TYPE_MAYBE_FEED)
+ return;
+
+ if (aContentWindow) {
+ var uri = this._checkAndGetURI(aURIString, aContentWindow);
+
+ this._appendFeedReaderNotification(uri, aTitle, getNotificationBox(aContentWindow));
+ }
+ else
+ this._registerContentHandler(contentType, aURIString, aTitle);
+ },
+
+ /**
+ * Appends a notifcation for the given feed reader details.
+ *
+ * The notification could be either a pseudo-dialog which lets
+ * the user to add the feed reader:
+ * [ [icon] Add %feed-reader-name% (%feed-reader-host%) as a Feed Reader? (Add) [x] ]
+ *
+ * or a simple message for the case where the feed reader is already registered:
+ * [ [icon] %feed-reader-name% is already registered as a Feed Reader [x] ]
+ *
+ * A new notification isn't appended if the given notificationbox has a
+ * notification for the same feed reader.
+ *
+ * @param aURI
+ * The url of the feed reader as a nsIURI object
+ * @param aName
+ * The feed reader name as it was passed to registerContentHandler
+ * @param aNotificationBox
+ * The notification box to which a notification might be appended
+ * @return true if a notification has been appended, false otherwise.
+ */
+ _appendFeedReaderNotification: function appendFeedReaderNotification(aURI, aName, aNotificationBox) {
+ var uriSpec = aURI.spec;
+ var notificationValue = "feed reader notification: " + uriSpec;
+ var notificationIcon = aURI.resolve("/favicon.ico");
+
+ // Don't append a new notification if the notificationbox
+ // has a notification for the given feed reader already
+ if (aNotificationBox.getNotificationWithValue(notificationValue))
+ return false;
+
+ var buttons, message;
+ if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec))
+ message = this._getFormattedString("handlerRegistered", [aName]);
+ else {
+ message = this._getFormattedString("addHandler", [aName, aURI.host]);
+ var self = this;
+ var addButton = {
+ _outer: self,
+ label: self._getString("addHandlerAddButton"),
+ accessKey: self._getString("addHandlerAddButtonAccesskey"),
+ feedReaderInfo: { uri: uriSpec, name: aName },
+
+ /* static */
+ callback: function addFeedReaderButtonCallback(aNotification, aButtonInfo) {
+ var uri = aButtonInfo.feedReaderInfo.uri;
+ var name = aButtonInfo.feedReaderInfo.name;
+ var outer = aButtonInfo._outer;
+
+ // The reader could have been added from another window mean while
+ if (!outer.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uri))
+ outer._registerContentHandler(TYPE_MAYBE_FEED, uri, name);
+
+ // avoid reference cycles
+ aButtonInfo._outer = null;
+
+ return false;
+ }
+ };
+ buttons = [addButton];
+ }
+
+ aNotificationBox.appendNotification(message,
+ notificationValue,
+ notificationIcon,
+ aNotificationBox.PRIORITY_INFO_LOW,
+ buttons);
+ return true;
+ },
+
+ /**
+ * Save Web Content Handler metadata to persistent preferences.
+ * @param contentType
+ * The content Type being handled
+ * @param uri
+ * The uri of the web service
+ * @param title
+ * The human readable name of the web service
+ *
+ * This data is stored under:
+ *
+ * browser.contentHandlers.type0 = content/type
+ * browser.contentHandlers.uri0 = http://www.foo.com/q=%s
+ * browser.contentHandlers.title0 = Foo 2.0alphr
+ */
+ _saveContentHandlerToPrefs: function saveContentHandlerToPrefs(contentType, uri, title) {
+ var i = 0;
+ var typeBranch = null;
+ while (true) {
+ typeBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + ".");
+ try {
+ typeBranch.getCharPref("type");
+ ++i;
+ }
+ catch (e) {
+ // No more handlers
+ break;
+ }
+ }
+ if (typeBranch) {
+ typeBranch.setCharPref("type", contentType);
+ var pls = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = uri;
+ typeBranch.setComplexValue("uri",
+ Ci.nsIPrefLocalizedString, pls);
+ pls.data = title;
+ typeBranch.setComplexValue("title",
+ Ci.nsIPrefLocalizedString, pls);
+
+ Services.prefs.savePrefFile(null);
+ }
+ },
+
+ /**
+ * Determines if there is a type with a particular uri registered for the
+ * specified content type already.
+ * @param contentType
+ * The content type that the uri handles
+ * @param uri
+ * The uri of the
+ */
+ _typeIsRegistered: function typeIsRegistered(contentType, uri) {
+ if (!(contentType in this._contentTypes))
+ return false;
+
+ var services = this._contentTypes[contentType];
+ for (let i = 0; i < services.length; ++i) {
+ // This uri has already been registered
+ if (services[i].uri == uri)
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Gets a stream converter contract id for the specified content type.
+ * @param contentType
+ * The source content type for the conversion.
+ * @returns A contract id to construct a converter to convert between the
+ * contentType and *\/*.
+ */
+ _getConverterContractID: function getConverterContractID(contentType) {
+ const template = "@mozilla.org/streamconv;1?from=%s&to=*/*";
+ return template.replace(/%s/, contentType);
+ },
+
+ /**
+ * Register a web service handler for a content type.
+ *
+ * @param contentType
+ * the content type being handled
+ * @param uri
+ * the URI of the web service
+ * @param title
+ * the human readable name of the web service
+ */
+ _registerContentHandler: function registerContentHandler(contentType, uri, title) {
+ this._updateContentTypeHandlerMap(contentType, uri, title);
+ this._saveContentHandlerToPrefs(contentType, uri, title);
+
+ if (contentType == TYPE_MAYBE_FEED) {
+ // Make the new handler the last-selected reader in the preview page
+ // and make sure the preview page is shown the next time a feed is visited
+ Services.prefs.setCharPref(PREF_SELECTED_READER, "web");
+
+ Services.prefs.setStringPref(PREF_SELECTED_WEB, uri);
+ Services.prefs.setCharPref(PREF_SELECTED_ACTION, "ask");
+ this._setAutoHandler(TYPE_MAYBE_FEED, null);
+ }
+ },
+
+ /**
+ * Update the content type -> handler map. This mapping is not persisted, use
+ * registerContentHandler or _saveContentHandlerToPrefs for that purpose.
+ * @param contentType
+ * The content Type being handled
+ * @param uri
+ * The uri of the web service
+ * @param title
+ * The human readable name of the web service
+ */
+ _updateContentTypeHandlerMap: function updateContentTypeHandlerMap(contentType, uri, title) {
+ if (!(contentType in this._contentTypes))
+ this._contentTypes[contentType] = [];
+
+ // Avoid adding duplicates
+ if (this._typeIsRegistered(contentType, uri))
+ return;
+
+ this._contentTypes[contentType].push(new ServiceInfo(contentType, uri, title));
+
+ if (!(contentType in this._blockedTypes)) {
+ var converterContractID = this._getConverterContractID(contentType);
+ var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID,
+ WebContentConverterFactory);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getContentHandlers: function getContentHandlers(contentType, countRef) {
+ countRef.value = 0;
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ var handlers = this._contentTypes[contentType];
+ countRef.value = handlers.length;
+ return handlers;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ resetHandlersForType: function resetHandlersForType(contentType) {
+ // currently unused within the tree, so only useful for extensions; previous
+ // impl. was buggy (and even infinite-looped!), so I argue that this is a
+ // definite improvement
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Registers a handler from the settings on a preferences branch.
+ *
+ * @param branch
+ * an nsIPrefBranch containing "type", "uri", and "title" preferences
+ * corresponding to the content handler to be registered
+ */
+ _registerContentHandlerWithBranch: function(branch) {
+ /**
+ * Since we support up to six predefined readers, we need to handle gaps
+ * better, since the first branch with user-added values will be .6
+ *
+ * How we deal with that is to check to see if there's no prefs in the
+ * branch and stop cycling once that's true. This doesn't fix the case
+ * where a user manually removes a reader, but that's not supported yet!
+ */
+ var vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ try {
+ var type = branch.getCharPref("type");
+ var uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ var title = branch.getComplexValue("title",
+ Ci.nsIPrefLocalizedString).data;
+ this._updateContentTypeHandlerMap(type, uri, title);
+ }
+ catch(ex) {
+ // do nothing, the next branch might have values
+ }
+ },
+
+ /**
+ * Load the auto handler, content handler and protocol tables from
+ * preferences.
+ */
+ _init: function init() {
+ var kids = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH)
+ .getChildList("");
+ // first get the numbers of the providers by getting all ###.uri prefs
+ var nums = [];
+ for (let i = 0; i < kids.length; i++) {
+ let match = /^(\d+)\.uri$/.exec(kids[i]);
+ if (match)
+ nums.push(match[1]);
+ }
+ // sort them, to get them back in order
+ nums.sort(function(a, b) {return a - b;});
+ // now register them
+ for (let i = 0; i < nums.length; i++) {
+ let branch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH + nums[i] + ".");
+ this._registerContentHandlerWithBranch(branch);
+ }
+
+ // We need to do this _after_ registering all of the available handlers,
+ // so that getWebContentHandlerByURI can return successfully.
+ try {
+ var autoBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_AUTO);
+ var childPrefs = autoBranch.getChildList("");
+ for (let i = 0; i < childPrefs.length; ++i) {
+ let type = childPrefs[i];
+ let uri = autoBranch.getCharPref(type);
+ if (uri) {
+ let handler = this.getWebContentHandlerByURI(type, uri);
+ this._setAutoHandler(type, handler);
+ }
+ }
+ }
+ catch (e) {
+ }
+ },
+
+ /**
+ * See nsIObserver
+ */
+ observe: function observe(subject, topic, data) {
+ switch (topic) {
+ case "app-startup":
+ Services.obs.addObserver(this, "final-ui-startup");
+ break;
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, "final-ui-startup");
+ this._init();
+ break;
+ }
+ },
+
+ /**
+ * See nsIFactory
+ */
+ createInstance: function createInstance(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ },
+
+ classID: WCCR_CLASSID,
+ classInfo: XPCOMUtils.generateCI({
+ classID: WCCR_CLASSID,
+ contractID: WCCR_CONTRACTID,
+ interfaces: [Ci.nsIWebContentConverterService,
+ Ci.nsIWebContentHandlerRegistrar,
+ Ci.nsIObserver,
+ Ci.nsIFactory],
+ flags: Ci.nsIClassInfo.DOM_OBJECT}),
+
+ /**
+ * See nsISupports
+ */
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIWebContentConverterService,
+ Ci.nsIWebContentHandlerRegistrar,
+ Ci.nsIObserver,
+ Ci.nsIFactory])
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]);
diff --git a/comm/suite/components/feeds/content/subscribe.css b/comm/suite/components/feeds/content/subscribe.css
new file mode 100644
index 0000000000..1ba7611b80
--- /dev/null
+++ b/comm/suite/components/feeds/content/subscribe.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#feedSubscribeLine {
+ -moz-binding: url(chrome://communicator/content/feeds/subscribe.xml#feedreaderUI);
+}
diff --git a/comm/suite/components/feeds/content/subscribe.xhtml b/comm/suite/components/feeds/content/subscribe.xhtml
new file mode 100644
index 0000000000..c1c4b90765
--- /dev/null
+++ b/comm/suite/components/feeds/content/subscribe.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % feedDTD
+ SYSTEM "chrome://communicator/locale/feeds/subscribe.dtd">
+ %feedDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<html id="feedHandler"
+ xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&feedPage.title;</title>
+ <link rel="stylesheet"
+ href="chrome://communicator/content/feeds/subscribe.css"
+ type="text/css"
+ media="all"/>
+ <link rel="stylesheet"
+ href="chrome://communicator/skin/feed-subscribe.css"
+ type="text/css"
+ media="all"/>
+ </head>
+ <body>
+ <div id="feedHeaderContainer">
+ <div id="feedHeader" dir="&locale.dir;">
+ <div id="feedIntroText">
+ <p id="feedSubscriptionInfo1" />
+ <p id="feedSubscriptionInfo2" />
+ </div>
+ <div id="feedSubscribeLine" />
+ </div>
+ </div>
+
+ <div id="feedBody">
+ <div id="feedTitle">
+ <a id="feedTitleLink">
+ <img id="feedTitleImage"/>
+ </a>
+ <div id="feedTitleContainer">
+ <h1 id="feedTitleText"/>
+ <h2 id="feedSubtitleText"/>
+ </div>
+ </div>
+ <div id="feedContent"/>
+ </div>
+ </body>
+</html>
diff --git a/comm/suite/components/feeds/content/subscribe.xml b/comm/suite/components/feeds/content/subscribe.xml
new file mode 100644
index 0000000000..33a9e539df
--- /dev/null
+++ b/comm/suite/components/feeds/content/subscribe.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings SYSTEM "chrome://communicator/locale/feeds/subscribe.dtd">
+
+<bindings id="feedBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <binding id="feedreaderUI" bindToUntrustedContent="true">
+ <content>
+ <xul:vbox>
+ <xul:hbox align="center">
+ <xul:description anonid="subscribeUsingDescription" class="subscribeUsingDescription"/>
+ <xul:menulist anonid="handlersMenuList" class="handlersMenuList" aria-labelledby="subscribeUsingDescription">
+ <xul:menupopup anonid="handlersMenuPopup" class="handlersMenuPopup">
+ <xul:menuitem anonid="messengerFeedsMenuItem" label="&feedMessenger;" class="menuitem-iconic messengerFeedsMenuItem" image="chrome://communicator/skin/icons/feedIcon16.png" selected="true"/>
+ <xul:menuitem anonid="liveBookmarksMenuItem" label="&feedLiveBookmarks;" class="menuitem-iconic liveBookmarksMenuItem" image="chrome://communicator/skin/icons/feedIcon16.png"/>
+ <xul:menuseparator/>
+ </xul:menupopup>
+ </xul:menulist>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:checkbox anonid="alwaysUse" class="alwaysUse" checked="false"/>
+ </xul:hbox>
+ <xul:hbox align="center">
+ <xul:spacer flex="1"/>
+ <xul:button label="&feedSubscribeNow;" anonid="subscribeButton" class="subscribeButton"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <constructor>
+ new BrowserFeedWriter();
+ </constructor>
+ </implementation>
+ <resources>
+ <stylesheet src="chrome://communicator/skin/feed-subscribe-ui.css"/>
+ </resources>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/feeds/jar.mn b/comm/suite/components/feeds/jar.mn
new file mode 100644
index 0000000000..b1df80fa40
--- /dev/null
+++ b/comm/suite/components/feeds/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/feeds/subscribe.css (content/subscribe.css)
+ content/communicator/feeds/subscribe.xhtml (content/subscribe.xhtml)
+ content/communicator/feeds/subscribe.xml (content/subscribe.xml)
diff --git a/comm/suite/components/feeds/moz.build b/comm/suite/components/feeds/moz.build
new file mode 100644
index 0000000000..07571081e3
--- /dev/null
+++ b/comm/suite/components/feeds/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# XPIDL_SOURCES += [
+# 'nsIFeedResultService.idl',
+# 'nsIWebContentConverterRegistrar.idl',
+# ]
+
+# XPIDL_MODULE = 'suite-feeds'
+
+SOURCES += [
+ "nsFeedSniffer.cpp",
+]
+
+EXTRA_COMPONENTS += [
+ "FeedConverter.js",
+ "FeedWriter.js",
+ "SuiteFeeds.manifest",
+ "WebContentConverter.js",
+]
+
+FINAL_LIBRARY = "suite"
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/feeds/nsFeedSniffer.cpp b/comm/suite/components/feeds/nsFeedSniffer.cpp
new file mode 100644
index 0000000000..eba3a012ff
--- /dev/null
+++ b/comm/suite/components/feeds/nsFeedSniffer.cpp
@@ -0,0 +1,356 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsFeedSniffer.h"
+
+#include "mozilla/Unused.h"
+
+#include "nsNetCID.h"
+#include "nsXPCOM.h"
+#include "nsCOMPtr.h"
+#include "nsStringStream.h"
+
+#include "nsICategoryManager.h"
+#include "nsIServiceManager.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+#include "nsIStreamConverterService.h"
+#include "nsIStreamConverter.h"
+
+#include "nsIStreamListener.h"
+
+#include "nsIHttpChannel.h"
+#include "nsIMIMEHeaderParam.h"
+
+#include "nsMimeTypes.h"
+#include "nsIURI.h"
+#include <algorithm>
+
+#define TYPE_ATOM "application/atom+xml"
+#define TYPE_RSS "application/rss+xml"
+#define TYPE_MAYBE_FEED "application/vnd.mozilla.maybe.feed"
+
+#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+#define NS_RSS "http://purl.org/rss/1.0/"
+
+#define MAX_BYTES 512u
+
+NS_IMPL_ISUPPORTS(nsFeedSniffer,
+ nsIContentSniffer,
+ nsIStreamListener,
+ nsIRequestObserver)
+
+nsresult
+nsFeedSniffer::ConvertEncodedData(nsIRequest* request,
+ const uint8_t* data,
+ uint32_t length)
+{
+ nsresult rv = NS_OK;
+
+ mDecodedData = "";
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request));
+ if (!httpChannel)
+ return NS_ERROR_NO_INTERFACE;
+
+ nsAutoCString contentEncoding;
+
+ mozilla::Unused << httpChannel->GetResponseHeader("Content-Encoding"_ns,
+ contentEncoding);
+ if (!contentEncoding.IsEmpty()) {
+ nsCOMPtr<nsIStreamConverterService> converterService(do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID));
+ if (converterService) {
+ ToLowerCase(contentEncoding);
+
+ nsCOMPtr<nsIStreamListener> converter;
+ rv = converterService->AsyncConvertData(contentEncoding.get(),
+ "uncompressed", this, nullptr,
+ getter_AddRefs(converter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ converter->OnStartRequest(request);
+
+ nsCOMPtr<nsIStringInputStream> rawStream =
+ do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID);
+ if (!rawStream)
+ return NS_ERROR_FAILURE;
+
+ rv = rawStream->SetData((const char*)data, length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = converter->OnDataAvailable(request, rawStream, 0, length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ converter->OnStopRequest(request, NS_OK);
+ }
+ }
+ return rv;
+}
+
+template<int N>
+static bool
+StringBeginsWithLowercaseLiteral(nsAString& aString,
+ const char (&aSubstring)[N])
+{
+ return StringHead(aString, N).LowerCaseEqualsLiteral(aSubstring);
+}
+
+bool
+HasAttachmentDisposition(nsIHttpChannel* httpChannel)
+{
+ if (!httpChannel)
+ return false;
+
+ uint32_t disp;
+ nsresult rv = httpChannel->GetContentDisposition(&disp);
+
+ if (NS_SUCCEEDED(rv) && disp == nsIChannel::DISPOSITION_ATTACHMENT)
+ return true;
+
+ return false;
+}
+
+/**
+ * @return the first occurrence of a character within a string buffer,
+ * or nullptr if not found
+ */
+inline const char*
+FindChar(char c, const char *begin, const char *end)
+{
+ return static_cast<const char *>(memchr(begin, c, end - begin));
+}
+
+/**
+ *
+ * Determine if a substring is the "documentElement" in the document.
+ *
+ * All of our sniffed substrings: <rss, <feed, <rdf:RDF must be the "document"
+ * element within the XML DOM, i.e. the root container element. Otherwise,
+ * it's possible that someone embedded one of these tags inside a document of
+ * another type, e.g. a HTML document, and we don't want to show the preview
+ * page if the document isn't actually a feed.
+ *
+ * @param start
+ * The beginning of the data being sniffed
+ * @param end
+ * The end of the data being sniffed, right before the substring that
+ * was found.
+ * @returns true if the found substring is the documentElement, false
+ * otherwise.
+ */
+static bool
+IsDocumentElement(const char *start, const char* end)
+{
+ // For every tag in the buffer, check to see if it's a PI, Doctype or
+ // comment, our desired substring or something invalid.
+ while ( (start = FindChar('<', start, end)) ) {
+ ++start;
+ if (start >= end)
+ return false;
+
+ // Check to see if the character following the '<' is either '?' or '!'
+ // (processing instruction or doctype or comment)... these are valid nodes
+ // to have in the prologue.
+ if (*start != '?' && *start != '!')
+ return false;
+
+ // Now advance the iterator until the '>' (We do this because we don't want
+ // to sniff indicator substrings that are embedded within other nodes, e.g.
+ // comments: <!-- <rdf:RDF .. > -->
+ start = FindChar('>', start, end);
+ if (!start)
+ return false;
+
+ ++start;
+ }
+ return true;
+}
+
+/**
+ * Determines whether or not a string exists as the root element in an XML data
+ * string buffer.
+ * @param dataString
+ * The data being sniffed
+ * @param substring
+ * The substring being tested for existence and root-ness.
+ * @returns true if the substring exists and is the documentElement, false
+ * otherwise.
+ */
+static bool
+ContainsTopLevelSubstring(nsACString& dataString, const char *substring)
+{
+ nsACString::const_iterator start, end;
+ dataString.BeginReading(start);
+ dataString.EndReading(end);
+
+ if (!FindInReadable(nsCString(substring), start, end)){
+ return false;
+ }
+
+ auto offset = start.get() - dataString.Data();
+
+ const char *begin = dataString.BeginReading();
+
+ // Only do the validation when we find the substring.
+ return IsDocumentElement(begin, begin + offset);
+}
+
+NS_IMETHODIMP
+nsFeedSniffer::GetMIMETypeFromContent(nsIRequest* request,
+ const uint8_t* data,
+ uint32_t length,
+ nsACString& sniffedType)
+{
+ nsCOMPtr<nsIHttpChannel> channel(do_QueryInterface(request));
+ if (!channel)
+ return NS_ERROR_NO_INTERFACE;
+
+ // Check that this is a GET request, since you can't subscribe to a POST...
+ nsAutoCString method;
+ nsresult rv;
+ mozilla::Unused << channel->GetRequestMethod(method);
+ if (!method.EqualsLiteral("GET")) {
+ sniffedType.Truncate();
+ return NS_OK;
+ }
+
+ // We need to find out if this is a load of a view-source document. In this
+ // case we do not want to override the content type, since the source display
+ // does not need to be converted from feed format to XUL. More importantly,
+ // we don't want to change the content type from something
+ // nsContentDLF::CreateInstance knows about (e.g. application/xml, text/html
+ // etc) to something that only the application fe knows about (maybe.feed)
+ // thus deactivating syntax highlighting.
+ nsCOMPtr<nsIURI> originalURI;
+ channel->GetOriginalURI(getter_AddRefs(originalURI));
+
+ nsAutoCString scheme;
+ originalURI->GetScheme(scheme);
+ if (scheme.EqualsLiteral("view-source")) {
+ sniffedType.Truncate();
+ return NS_OK;
+ }
+
+ // Check the Content-Type to see if it is set correctly. If it is set to
+ // something specific that we think is a reliable indication of a feed, don't
+ // bother sniffing since we assume the site maintainer knows what they're
+ // doing.
+ nsAutoCString contentType;
+ channel->GetContentType(contentType);
+ bool noSniff = contentType.EqualsLiteral(TYPE_RSS) ||
+ contentType.EqualsLiteral(TYPE_ATOM);
+
+ if (noSniff) {
+ // check for an attachment after we have a likely feed.
+ if(HasAttachmentDisposition(channel)) {
+ sniffedType.Truncate();
+ return NS_OK;
+ }
+
+ // set the feed header as a response header, since we have good metadata
+ // telling us that the feed is supposed to be RSS or Atom
+ mozilla::DebugOnly<nsresult> rv =
+ channel->SetResponseHeader("X-Moz-Is-Feed"_ns,
+ "1"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ sniffedType.AssignLiteral(TYPE_MAYBE_FEED);
+ return NS_OK;
+ }
+
+ // Don't sniff arbitrary types. Limit sniffing to situations that
+ // we think can reasonably arise.
+ if (!contentType.EqualsLiteral(TEXT_HTML) &&
+ !contentType.EqualsLiteral(APPLICATION_OCTET_STREAM) &&
+ // Same criterion as XMLHttpRequest. Should we be checking for "+xml"
+ // and check for text/xml and application/xml by hand instead?
+ contentType.Find("xml") == -1) {
+ sniffedType.Truncate();
+ return NS_OK;
+ }
+
+ // Now we need to potentially decompress data served with
+ // Content-Encoding: gzip
+ rv = ConvertEncodedData(request, data, length);
+ if (NS_FAILED(rv))
+ return rv;
+
+ // We cap the number of bytes to scan at MAX_BYTES to prevent picking up
+ // false positives by accidentally reading document content, e.g. a "how to
+ // make a feed" page.
+ const char* testData;
+ if (mDecodedData.IsEmpty()) {
+ testData = (const char*)data;
+ length = std::min<uint32_t>(length, MAX_BYTES);
+ } else {
+ testData = mDecodedData.get();
+ length = std::min<uint32_t>(mDecodedData.Length(), MAX_BYTES);
+ }
+
+ // The strategy here is based on that described in:
+ // http://blogs.msdn.com/rssteam/articles/PublishersGuide.aspx
+ // for interoperarbility purposes.
+
+ // Thus begins the actual sniffing.
+ nsDependentCSubstring dataString((const char*)testData, length);
+
+ bool isFeed = false;
+
+ // RSS 0.91/0.92/2.0
+ isFeed = ContainsTopLevelSubstring(dataString, "<rss");
+
+ // Atom 1.0
+ if (!isFeed)
+ isFeed = ContainsTopLevelSubstring(dataString, "<feed");
+
+ // RSS 1.0
+ if (!isFeed) {
+ bool foundNS_RDF = FindInReadable(nsLiteralCString(NS_RDF), dataString);
+ bool foundNS_RSS = FindInReadable(nsLiteralCString(NS_RSS), dataString);
+ isFeed = ContainsTopLevelSubstring(dataString, "<rdf:RDF") &&
+ foundNS_RDF && foundNS_RSS;
+ }
+
+ // If we sniffed a feed, coerce our internal type
+ if (isFeed && !HasAttachmentDisposition(channel))
+ sniffedType.AssignLiteral(TYPE_MAYBE_FEED);
+ else
+ sniffedType.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFeedSniffer::OnStartRequest(nsIRequest* request)
+{
+ return NS_OK;
+}
+
+nsresult
+nsFeedSniffer::AppendSegmentToString(nsIInputStream* inputStream,
+ void* closure,
+ const char* rawSegment,
+ uint32_t toOffset,
+ uint32_t count,
+ uint32_t* writeCount)
+{
+ nsCString* decodedData = static_cast<nsCString*>(closure);
+ decodedData->Append(rawSegment, count);
+ *writeCount = count;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFeedSniffer::OnDataAvailable(nsIRequest* request, nsIInputStream* stream,
+ uint64_t offset, uint32_t count)
+{
+ uint32_t read;
+ return stream->ReadSegments(AppendSegmentToString, &mDecodedData, count,
+ &read);
+}
+
+NS_IMETHODIMP
+nsFeedSniffer::OnStopRequest(nsIRequest* request, nsresult status)
+{
+ return NS_OK;
+}
diff --git a/comm/suite/components/feeds/nsFeedSniffer.h b/comm/suite/components/feeds/nsFeedSniffer.h
new file mode 100644
index 0000000000..0128de2483
--- /dev/null
+++ b/comm/suite/components/feeds/nsFeedSniffer.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIContentSniffer.h"
+#include "nsIStreamListener.h"
+#include "nsString.h"
+#include "nsSuiteCID.h"
+#include "mozilla/Attributes.h"
+
+class nsFeedSniffer final : public nsIContentSniffer, nsIStreamListener
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTSNIFFER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ static nsresult AppendSegmentToString(nsIInputStream* inputStream,
+ void* closure,
+ const char* rawSegment,
+ uint32_t toOffset,
+ uint32_t count,
+ uint32_t* writeCount);
+
+protected:
+ ~nsFeedSniffer() {}
+
+ nsresult ConvertEncodedData(nsIRequest* request, const uint8_t* data,
+ uint32_t length);
+
+private:
+ nsCString mDecodedData;
+};
diff --git a/comm/suite/components/feeds/nsIFeedResultService.idl b/comm/suite/components/feeds/nsIFeedResultService.idl
new file mode 100644
index 0000000000..a72292abd6
--- /dev/null
+++ b/comm/suite/components/feeds/nsIFeedResultService.idl
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface nsIURI;
+interface nsIRequest;
+interface nsIFeedResult;
+
+/**
+ * nsIFeedResultService provides a globally-accessible object for retrieving
+ * the results of feed processing.
+ */
+[scriptable, uuid(950a829e-c20e-4dc3-b447-f8b753ae54da)]
+interface nsIFeedResultService : nsISupports
+{
+ /**
+ * When set to true, forces the preview page to be displayed, regardless
+ * of the user's preferences.
+ */
+ attribute boolean forcePreviewPage;
+
+ /**
+ * Adds a URI to the user's specified external feed handler, or live
+ * bookmarks.
+ * @param uri
+ * The uri of the feed to add.
+ * @param title
+ * The title of the feed to add.
+ * @param subtitle
+ * The subtitle of the feed to add.
+ * @param feedType
+ * The nsIFeed type of the feed. See nsIFeed.idl
+ */
+ void addToClientReader(in AUTF8String uri,
+ in AString title,
+ in AString subtitle,
+ in unsigned long feedType);
+
+ /**
+ * Registers a Feed Result object with a globally accessible service
+ * so that it can be accessed by a singleton method outside the usual
+ * flow of control in document loading.
+ *
+ * @param feedResult
+ * An object implementing nsIFeedResult representing the feed.
+ */
+ void addFeedResult(in nsIFeedResult feedResult);
+
+ /**
+ * Gets a Feed Handler object registered using addFeedResult.
+ *
+ * @param uri
+ * The URI of the feed a handler is being requested for
+ */
+ nsIFeedResult getFeedResult(in nsIURI uri);
+
+ /**
+ * Unregisters a Feed Handler object registered using addFeedResult.
+ * @param uri
+ * The feed URI the handler was registered under. This must be
+ * the same *instance* the feed was registered under.
+ */
+ void removeFeedResult(in nsIURI uri);
+};
diff --git a/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl b/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl
new file mode 100644
index 0000000000..68ec48a936
--- /dev/null
+++ b/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIMIMEInfo.idl"
+#include "nsIWebContentHandlerRegistrar.idl"
+
+interface nsIRequest;
+
+[scriptable, uuid(eb361098-5158-4b21-8f98-50b445f1f0b2)]
+interface nsIWebContentHandlerInfo : nsIHandlerApp
+{
+ /**
+ * The content type handled by the handler
+ */
+ readonly attribute AString contentType;
+
+ /**
+ * The uri of the handler, with an embedded %s where the URI of the loaded
+ * document will be encoded.
+ */
+ readonly attribute AString uri;
+
+ /**
+ * Gets the service URL Spec, with the loading document URI encoded in it.
+ * @param uri
+ * The URI of the document being loaded
+ * @returns The URI of the service with the loading document URI encoded in
+ * it.
+ */
+ AString getHandlerURI(in AString uri);
+};
+
+[scriptable, uuid(de7cc06e-e778-45cb-b7db-7a114e1e75b1)]
+interface nsIWebContentConverterService : nsIWebContentHandlerRegistrar
+{
+ /**
+ * Specifies the handler to be used to automatically handle all links of a
+ * certain content type from now on.
+ * @param contentType
+ * The content type to automatically load with the specified handler
+ * @param handler
+ * A web service handler. If this is null, no automatic action is
+ * performed and the user must choose.
+ * @throws NS_ERROR_NOT_AVAILABLE if the service refered to by |handler| is
+ * not already registered.
+ */
+ void setAutoHandler(in AString contentType, in nsIWebContentHandlerInfo handler);
+
+ /**
+ * Gets the auto handler specified for a particular content type
+ * @param contentType
+ * The content type to look up an auto handler for.
+ * @returns The web service handler that will automatically handle all
+ * documents of the specified type. null if there is no automatic
+ * handler. (Handlers may be registered, just none of them specified
+ * as "automatic").
+ */
+ nsIWebContentHandlerInfo getAutoHandler(in AString contentType);
+
+ /**
+ * Gets a web handler for the specified service URI
+ * @param contentType
+ * The content type of the service being located
+ * @param uri
+ * The service URI of the handler to locate.
+ * @returns A web service handler that uses the specified uri.
+ */
+ nsIWebContentHandlerInfo getWebContentHandlerByURI(in AString contentType,
+ in AString uri);
+
+ /**
+ * Loads the preferred handler when content of a registered type is about
+ * to be loaded.
+ * @param request
+ * The nsIRequest for the load of the content
+ */
+ void loadPreferredHandler(in nsIRequest request);
+
+ /**
+ * Removes a registered protocol handler
+ * @param protocol
+ * The protocol scheme to remove a service handler for
+ * @param uri
+ * The uri of the service handler to remove
+ */
+ void removeProtocolHandler(in AString protocol, in AString uri);
+
+ /**
+ * Removes a registered content handler
+ * @param contentType
+ * The content type to remove a service handler for
+ * @param uri
+ * The uri of the service handler to remove
+ */
+ void removeContentHandler(in AString contentType, in AString uri);
+
+ /**
+ * Gets the list of content handlers for a particular type.
+ * @param contentType
+ * The content type to get handlers for
+ * @returns An array of nsIWebContentHandlerInfo objects
+ */
+ void getContentHandlers(in AString contentType,
+ [optional] out unsigned long count,
+ [retval,array,size_is(count)] out nsIWebContentHandlerInfo handlers);
+
+ /**
+ * Resets the list of available content handlers to the default set from
+ * the distribution.
+ * @param contentType
+ * The content type to reset handlers for
+ */
+ void resetHandlersForType(in AString contentType);
+};
diff --git a/comm/suite/components/helpviewer/content/contextHelp.js b/comm/suite/components/helpviewer/content/contextHelp.js
new file mode 100644
index 0000000000..151cd777a9
--- /dev/null
+++ b/comm/suite/components/helpviewer/content/contextHelp.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+// Set the default content pack to the Mozilla content pack. Use the
+// setHelpFileURI function to set this value.
+var helpFileURI;
+
+// openHelp - Opens up the Mozilla Help Viewer with the specified
+// topic and content pack.
+// see http://www.mozilla.org/projects/help-viewer/content_packs.html
+function openHelp(topic, contentPack)
+{
+ // helpFileURI is the content pack to use in this function. If contentPack is defined,
+ // use that and set the helpFileURI to that value so that it will be the default.
+ helpFileURI = contentPack || helpFileURI;
+
+ // Try to find previously opened help.
+ var topWindow = locateHelpWindow(helpFileURI);
+
+ if ( topWindow ) {
+ // Open topic in existing window.
+ topWindow.focus();
+ topWindow.displayTopic(topic);
+ } else {
+ // Open topic in new window.
+ const params = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+ params.SetNumberStrings(2);
+ params.SetString(0, helpFileURI);
+ params.SetString(1, topic);
+
+ let openFeatures = "chrome,all,dialog=no";
+
+ if (AppConstants.platform == "win") {
+ openFeatures += ",alwaysRaised";
+ }
+ Services.ww.openWindow(null, "chrome://help/content/help.xul", "_blank",
+ openFeatures, params);
+ }
+}
+
+// setHelpFileURI - Sets the default content pack to use in the Help Viewer
+function setHelpFileURI(rdfURI)
+{
+ helpFileURI = rdfURI;
+}
+
+// Locate existing help window for this content pack.
+function locateHelpWindow(contentPack) {
+ const iterator = Services.wm.getEnumerator("suite:help");
+ var topWindow = null;
+ var aWindow;
+
+ // Loop through help windows looking for one with selected content
+ // pack.
+ while (iterator.hasMoreElements()) {
+ aWindow = iterator.getNext();
+ if (!aWindow.closed && aWindow.getHelpFileURI() == contentPack) {
+ topWindow = aWindow;
+ }
+ }
+ return topWindow;
+}
diff --git a/comm/suite/components/helpviewer/content/help.js b/comm/suite/components/helpviewer/content/help.js
new file mode 100644
index 0000000000..4a7d0b1cbb
--- /dev/null
+++ b/comm/suite/components/helpviewer/content/help.js
@@ -0,0 +1,856 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+// Global Variables
+var helpExternal;
+var helpBrowser;
+var helpSearchPanel;
+var emptySearch;
+var emptySearchText;
+var emptySearchLink = "about:blank";
+var helpTocPanel;
+var helpIndexPanel;
+var helpGlossaryPanel;
+var strBundle;
+var gTocDSList = "";
+
+// Namespaces
+const NC = "http://home.netscape.com/NC-rdf#";
+const MAX_LEVEL = 40; // maximum depth of recursion in search datasources.
+const MAX_HISTORY_MENU_ITEMS = 6;
+
+const platform = getCurrentPlatform();
+
+// Resources
+const RDF = Cc["@mozilla.org/rdf/rdf-service;1"]
+ .getService(Ci.nsIRDFService);
+const RDF_ROOT = RDF.GetResource("urn:root");
+const NC_PANELLIST = RDF.GetResource(NC + "panellist");
+const NC_PANELID = RDF.GetResource(NC + "panelid");
+const NC_EMPTY_SEARCH_TEXT = RDF.GetResource(NC + "emptysearchtext");
+const NC_EMPTY_SEARCH_LINK = RDF.GetResource(NC + "emptysearchlink");
+const NC_DATASOURCES = RDF.GetResource(NC + "datasources");
+const NC_PLATFORM = RDF.GetResource(NC + "platform");
+const NC_SUBHEADINGS = RDF.GetResource(NC + "subheadings");
+const NC_NAME = RDF.GetResource(NC + "name");
+const NC_CHILD = RDF.GetResource(NC + "child");
+const NC_LINK = RDF.GetResource(NC + "link");
+const NC_TITLE = RDF.GetResource(NC + "title");
+const NC_BASE = RDF.GetResource(NC + "base");
+const NC_DEFAULTTOPIC = RDF.GetResource(NC + "defaulttopic");
+
+var RDFContainer = Cc["@mozilla.org/rdf/container;1"]
+ .createInstance(Ci.nsIRDFContainer);
+
+var RE;
+
+var helpFileURI;
+var helpFileDS;
+// Set from nc:base attribute on help rdf file. It may be used for prefix
+// reduction on all links within the current help set.
+var helpBaseURI;
+
+/* defaultTopic is either set
+ 1. in the openHelp() call, passed as an argument to the Help window and
+ evaluated in init(), or
+ 2. in nc:defaulttopic in the content pack (e.g. firebirdhelp.rdf),
+ evaluated in loadHelpRDF(), or
+ 3. "welcome" as a fallback, specified in loadHelpRDF() as well;
+ displayTopic() then uses defaultTopic because topic is null. */
+var defaultTopic;
+
+const NSRESULT_RDF_SYNTAX_ERROR = 0x804e03f7;
+
+// Translate the current application platform to one the
+// help viewer understands.
+function getCurrentPlatform() {
+
+ // The supported platforms are defined in
+ // suite/locales/en-US/chrome/common/help/suitehelp.rdf.
+ // We can't just return the current platform 1:1 because
+ // this would need l10n changes for all languages.
+ if (AppConstants.platform == "win") {
+ return "win";
+ }
+
+ if (AppConstants.platform == "macosx") {
+ return "mac";
+ }
+
+ if (AppConstants.platform == "linux") {
+ return "unix";
+ }
+
+ // We never end up here in official builds.
+ return "---";
+}
+
+// This function is called by dialogs/windows that want to display
+// context-sensitive help
+// These dialogs/windows should include the script
+// chrome://help/content/contextHelp.js
+function displayTopic(topic) {
+ // Get the page to open.
+ var uri = getLink(topic);
+ // Use default topic if specified topic is not found.
+ if (!uri) {
+ uri = getLink(defaultTopic);
+ }
+ // Load the page.
+ if (uri)
+ loadURI(uri);
+}
+
+// Initialize the Help window
+function init() {
+ // Cache panel references.
+ helpSearchPanel = document.getElementById("help-search-panel");
+ helpTocPanel = document.getElementById("help-toc-panel");
+ helpIndexPanel = document.getElementById("help-index-panel");
+ helpGlossaryPanel = document.getElementById("help-glossary-panel");
+ helpBrowser = document.getElementById("help-content");
+
+ // Turn off unnecessary features for security
+ helpBrowser.docShell.allowJavascript = false;
+ helpBrowser.docShell.allowPlugins = false;
+ helpBrowser.docShell.allowSubframes = false;
+ helpBrowser.docShell.allowMetaRedirects = false;
+
+ strBundle = document.getElementById("bundle_help");
+ emptySearchText = strBundle.getString("emptySearchText");
+
+ // Get the content pack, base URL, and help topic
+ var helpTopic = defaultTopic;
+ if ("arguments" in window &&
+ window.arguments[0] instanceof Ci.nsIDialogParamBlock) {
+ helpFileURI = window.arguments[0].GetString(0);
+ // trailing "/" included.
+ helpBaseURI = helpFileURI.substring(0, helpFileURI.lastIndexOf("/")+1);
+ helpTopic = window.arguments[0].GetString(1);
+ }
+
+ loadHelpRDF();
+ displayTopic(helpTopic);
+
+ // Move to Center of Screen
+ const width = document.documentElement.getAttribute("width");
+ const height = document.documentElement.getAttribute("height");
+ window.moveTo((screen.availWidth - width) / 2, (screen.availHeight - height) / 2);
+
+ // Initialize history.
+ getWebNavigation().sessionHistory =
+ Cc["@mozilla.org/browser/shistory;1"].createInstance(Ci.nsISHistory);
+ window.XULBrowserWindow = new nsHelpStatusHandler();
+
+ //Start the status handler.
+ window.XULBrowserWindow.init();
+
+ // Hook up UI through Progress Listener
+ const interfaceRequestor = helpBrowser.docShell.QueryInterface(Ci.nsIInterfaceRequestor);
+ const webProgress = interfaceRequestor.getInterface(Ci.nsIWebProgress);
+
+ webProgress.addProgressListener(window.XULBrowserWindow, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ var searchBox = document.getElementById("findText");
+ searchBox.clickSelectsAll = Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll", true);
+
+ setTimeout(focusSearch, 0);
+
+ helpExternal = document.getElementById("help-external");
+ helpExternal.docShell.useErrorPages = false;
+ helpExternal
+ .docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIURIContentListener)
+ .parentContentListener = helpContentListener;
+ helpExternal.addProgressListener(window.XULBrowserWindow, Ci.nsIWebProgress.NOTIFY_ALL);
+
+}
+function contentClick(event) {
+ // is this a left click on a link?
+ if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey || event.button != 0)
+ return true;
+
+ // is this a link?
+ var target = event.target;
+ while (!(target instanceof HTMLAnchorElement))
+ if (!(target = target.parentNode))
+ return true;
+
+ // is this an internal link?
+ if (target.href.lastIndexOf("chrome:", 0) == 0)
+ return true;
+
+ var uri = target.href;
+ if (/^x-moz-url-link:/.test(uri))
+ uri = Services.urlFormatter.formatURLPref(RegExp.rightContext);
+
+ try {
+ helpExternal.webNavigation
+ .loadURI(uri,
+ Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
+ null, null, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (e) {}
+ return false;
+}
+
+function showSidebar() {
+ document.getElementById("help-sidebar-splitter").setAttribute("state", "open");
+}
+
+// needed by findUtils.js
+var gFindInstData;
+function getFindInstData()
+{
+ if (!gFindInstData) {
+ gFindInstData = new nsFindInstData();
+ gFindInstData.browser = getBrowser();
+ // defaults for rootSearchWindow and currentSearchWindow are fine here
+ }
+ return gFindInstData;
+}
+
+function showSearchSidebar() {
+ // if you tab too quickly, you end up with stuck focus, revert focus to the searchbar
+ var searchTree = document.getElementById("help-toc-panel");
+ if (searchTree.treeBoxObject.focused) {
+ focusSearch();
+ }
+
+ var tableOfContents = document.getElementById("help-toc-sidebar");
+ tableOfContents.setAttribute("hidden", "true");
+
+ var sidebar = document.getElementById("help-search-sidebar");
+ sidebar.removeAttribute("hidden");
+}
+
+function hideSearchSidebar(aEvent) {
+ // if we're focused in the search results, focus content
+ var searchTree = document.getElementById("help-search-tree");
+ if (searchTree.treeBoxObject.focused) {
+ content.focus();
+ }
+
+ var sidebar = document.getElementById("help-search-sidebar");
+ sidebar.setAttribute("hidden", "true");
+
+ var tableOfContents = document.getElementById("help-toc-sidebar");
+ tableOfContents.removeAttribute("hidden");
+}
+
+// loadHelpRDF
+// Parse the provided help content pack RDF file, and use it to
+// populate the datasources attached to the trees in the viewer.
+// Filter out any information not applicable to the user's platform.
+function loadHelpRDF() {
+ if (!helpFileDS) {
+ try {
+ helpFileDS = RDF.GetDataSourceBlocking(helpFileURI);
+ } catch (e) {
+ if (e.result == NSRESULT_RDF_SYNTAX_ERROR) {
+ log("Help file: " + helpFileURI + " contains a syntax error.");
+ } else {
+ log("Help file: " + helpFileURI + " was not found.");
+ }
+ }
+
+ try {
+ document.title = getAttribute(helpFileDS, RDF_ROOT, NC_TITLE, "");
+ helpBaseURI = getAttribute(helpFileDS, RDF_ROOT, NC_BASE, helpBaseURI);
+ // if there's no nc:defaulttopic in the content pack, set "welcome"
+ // as the default topic
+ defaultTopic = getAttribute(helpFileDS,
+ RDF_ROOT, NC_DEFAULTTOPIC, "welcome");
+
+ var panelDefs = helpFileDS.GetTarget(RDF_ROOT, NC_PANELLIST, true);
+ RDFContainer.Init(helpFileDS, panelDefs);
+ var iterator = RDFContainer.GetElements();
+ while (iterator.hasMoreElements()) {
+ var panelDef = iterator.getNext();
+
+ var panelID = getAttribute(helpFileDS, panelDef, NC_PANELID, null);
+ var datasources = getAttribute(helpFileDS, panelDef, NC_DATASOURCES, "");
+ var panelPlatforms = getAttribute(helpFileDS, panelDef, NC_PLATFORM, null);
+
+ if (panelPlatforms && !panelPlatforms.split(/\s+/).includes(platform))
+ continue; // ignore datasources for other platforms
+
+ // empty datasources are valid on search panel definitions
+ // convert them to "rdf:null" which can be filtered and ignored
+ if (!datasources)
+ datasources = "rdf:null";
+
+ datasources = normalizeLinks(helpBaseURI, datasources);
+
+ var datasourceArray = datasources.split(/\s+/)
+ .filter(function(x) { return x != "rdf:null"; })
+ .map(RDF.GetDataSourceBlocking);
+
+ // Cache Additional Datasources to Augment Search Datasources.
+ if (panelID == "search") {
+ emptySearchText = getAttribute(helpFileDS, panelDef, NC_EMPTY_SEARCH_TEXT, emptySearchText);
+ emptySearchLink = getAttribute(helpFileDS, panelDef, NC_EMPTY_SEARCH_LINK, emptySearchLink);
+
+ datasourceArray.forEach(helpSearchPanel.database.AddDataSource,
+ helpSearchPanel.database);
+ if (!panelPlatforms)
+ filterDatasourceByPlatform(helpSearchPanel.database);
+
+ continue; // to next panel definition
+ }
+
+ // cache toc datasources list for use in getLink()
+ if (panelID == "toc")
+ gTocDSList += " " + datasources;
+
+ var tree = document.getElementById("help-" + panelID + "-panel");
+
+ // add each datasource to the current tree
+ datasourceArray.forEach(tree.database.AddDataSource,
+ tree.database);
+
+ // filter and display the current tree
+ if (!panelPlatforms)
+ filterDatasourceByPlatform(tree.database);
+ tree.builder.rebuild();
+ }
+ } catch (e) {
+ log(e + "");
+ }
+ }
+}
+
+// filterDatasourceByPlatform
+// Remove statements for other platforms from a datasource.
+function filterDatasourceByPlatform(aDatasource) {
+ filterNodeByPlatform(aDatasource, RDF_ROOT, 0);
+}
+
+// filterNodeByPlatform
+// Remove statements for other platforms from the provided datasource.
+function filterNodeByPlatform(aDatasource, aCurrentResource, aCurrentLevel) {
+ if (aCurrentLevel > MAX_LEVEL) {
+ log("Datasources over " + MAX_LEVEL + " levels deep are unsupported.");
+ return;
+ }
+
+ // get the subheadings under aCurrentResource and filter them
+ var nodes = aDatasource.GetTargets(aCurrentResource, NC_SUBHEADINGS, true);
+ while (nodes.hasMoreElements()) {
+ var node = nodes.getNext();
+ node = node.QueryInterface(Ci.nsIRDFResource);
+ // should we test for rdf:Seq here? see also doFindOnDatasource
+ filterSeqByPlatform(aDatasource, node, aCurrentLevel+1);
+ }
+}
+
+// filterSeqByPlatform
+// Go through the children of aNode, if any, removing statements applicable
+// only on other platforms.
+function filterSeqByPlatform(aDatasource, aNode, aCurrentLevel) {
+ // get nc:subheading children into an enumerator
+ var RDFC = Cc["@mozilla.org/rdf/container;1"]
+ .createInstance(Ci.nsIRDFContainer);
+ RDFC.Init(aDatasource, aNode);
+ var targets = RDFC.GetElements();
+
+ // process items in the rdf:Seq
+ while (targets.hasMoreElements()) {
+ var currentTarget = targets.getNext();
+
+ // find out on which platforms this node is meaningful
+ var nodePlatforms = getAttribute(aDatasource,
+ currentTarget.QueryInterface(Ci.nsIRDFResource),
+ NC_PLATFORM,
+ platform);
+
+ if (!nodePlatforms.split(/\s+/).includes(platform)) { // node is for another platform
+ var currentNode = currentTarget.QueryInterface(Ci.nsIRDFNode);
+ // "false" because we don't want to renumber elements in the container
+ RDFC.RemoveElement(currentNode, false);
+
+ // move to next node - ignore the children, because 1) they might be
+ // needed elsewhere and 2) nodes not connected to RDF_ROOT are ignored
+ continue;
+ }
+
+ // filter any children
+ filterNodeByPlatform(aDatasource, currentTarget, aCurrentLevel+1);
+ }
+}
+
+// Prepend helpBaseURI to list of space separated links if they don't start with
+// "chrome:"
+function normalizeLinks(helpBaseURI, links) {
+ if (!helpBaseURI) {
+ return links;
+ }
+ var ls = links.split(/\s+/);
+ if (ls.length == 0) {
+ return links;
+ }
+ for (var i=0; i < ls.length; ++i) {
+ if (ls[i] == "")
+ continue;
+
+ if (ls[i].substr(0,7) != "chrome:" && ls[i].substr(0,4) != "rdf:")
+ ls[i] = helpBaseURI + ls[i];
+ }
+ return ls.join(" ");
+}
+
+function getLink(ID) {
+ if (!ID)
+ return null;
+
+ var tocDS = document.getElementById("help-toc-panel").database;
+ if (!tocDS)
+ return null;
+
+ // URIs include both the ID part and the base file name,
+ // so we need to check for a matching ID in each datasource
+ var tocDSArray = gTocDSList.split(/\s+/)
+ .filter(function(x) { return x != "rdf:null"; });
+
+ for (var i = 0; i < tocDSArray.length; i++) {
+ var resource = RDF.GetResource(tocDSArray[i] + "#" + ID);
+ var link = tocDS.GetTarget(resource, NC_LINK, true);
+ if (!link) // no such rdf:ID found
+ continue;
+ return link.QueryInterface(Ci.nsIRDFLiteral).Value;
+ }
+ return null;
+}
+
+// Called by contextHelp.js to determine if this window is displaying the
+// requested help file.
+function getHelpFileURI() {
+ return helpFileURI;
+}
+
+function getBrowser() {
+ return helpBrowser;
+}
+
+function getWebNavigation() {
+ try {
+ return helpBrowser.webNavigation;
+ } catch (e)
+ {
+ return null;
+ }
+}
+
+function loadURI(uri) {
+ if (uri.substr(0,7) != "chrome:") {
+ uri = helpBaseURI + uri;
+ }
+ getWebNavigation().loadURI(uri,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+}
+
+function goBack() {
+ try
+ {
+ getWebNavigation().goBack();
+ } catch (e)
+ {
+ }
+}
+
+function goForward() {
+ try
+ {
+ getWebNavigation().goForward();
+ } catch(e)
+ {
+ }
+}
+
+function goHome() {
+ // Load "Welcome" page
+ displayTopic(defaultTopic);
+}
+
+function print() {
+ try {
+ _content.print();
+ } catch (e) {
+ }
+}
+
+function FillHistoryMenu(aParent, aMenu)
+ {
+ // Remove old entries if any
+ deleteHistoryItems(aParent);
+
+ var sessionHistory = getWebNavigation().sessionHistory;
+
+ var count = sessionHistory.count;
+ var index = sessionHistory.index;
+ var end;
+ var j;
+ var entry;
+
+ switch (aMenu)
+ {
+ case "back":
+ end = (index > MAX_HISTORY_MENU_ITEMS) ? index - MAX_HISTORY_MENU_ITEMS : 0;
+ if ((index - 1) < end) return false;
+ for (j = index - 1; j >= end; j--)
+ {
+ entry = sessionHistory.getEntryAtIndex(j);
+ if (entry)
+ createMenuItem(aParent, j, entry.title);
+ }
+ break;
+ case "forward":
+ end = ((count-index) > MAX_HISTORY_MENU_ITEMS) ? index + MAX_HISTORY_MENU_ITEMS : count - 1;
+ if ((index + 1) > end) return false;
+ for (j = index + 1; j <= end; j++)
+ {
+ entry = sessionHistory.getEntryAtIndex(j);
+ if (entry)
+ createMenuItem(aParent, j, entry.title);
+ }
+ break;
+ }
+ return true;
+ }
+
+function createMenuItem( aParent, aIndex, aLabel)
+ {
+ var menuitem = document.createElement( "menuitem" );
+ menuitem.setAttribute( "label", aLabel );
+ menuitem.setAttribute( "index", aIndex );
+ aParent.appendChild( menuitem );
+ }
+
+function deleteHistoryItems(aParent)
+{
+ var children = aParent.childNodes;
+ for (var i = children.length - 1; i >= 0; --i)
+ {
+ var index = children[i].getAttribute("index");
+ if (index)
+ aParent.removeChild(children[i]);
+ }
+}
+
+function createBackMenu(event) {
+ return FillHistoryMenu(event.target, "back");
+}
+
+function createForwardMenu(event) {
+ return FillHistoryMenu(event.target, "forward");
+}
+
+function gotoHistoryIndex(aEvent) {
+ var index = aEvent.target.getAttribute("index");
+ if (!index) {
+ return false;
+ }
+ try {
+ getWebNavigation().gotoIndex(index);
+ } catch(ex) {
+ return false;
+ }
+ return true;
+}
+
+function nsHelpStatusHandler() {
+ this.init();
+}
+
+nsHelpStatusHandler.prototype = {
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {},
+ onProgressChange : function(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {},
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange : function(aWebProgress, aRequest, state) {},
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) {
+ UpdateBackForwardButtons();
+ },
+ QueryInterface : function(aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsIXULBrowserWindow) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ init : function() {},
+
+ destroy : function() {},
+
+ setJSStatus : function(status) {},
+ setOverLink : function(link, context) {},
+ onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {}
+}
+
+function UpdateBackForwardButtons() {
+ var backBroadcaster = document.getElementById("canGoBack");
+ var forwardBroadcaster = document.getElementById("canGoForward");
+ var webNavigation = getWebNavigation();
+
+ // Avoid setting attributes on broadcasters if the value hasn't changed!
+ // Remember, guys, setting attributes on elements is expensive! They
+ // get inherited into anonymous content, broadcast to other widgets, etc.!
+ // Don't do it if the value hasn't changed! - dwh
+
+ var backDisabled = (backBroadcaster.getAttribute("disabled") == "true");
+ var forwardDisabled = (forwardBroadcaster.getAttribute("disabled") == "true");
+
+ if (backDisabled == webNavigation.canGoBack) {
+ if (backDisabled)
+ backBroadcaster.removeAttribute("disabled");
+ else
+ backBroadcaster.setAttribute("disabled", true);
+ }
+
+ if (forwardDisabled == webNavigation.canGoForward) {
+ if (forwardDisabled)
+ forwardBroadcaster.removeAttribute("disabled");
+ else
+ forwardBroadcaster.setAttribute("disabled", true);
+ }
+}
+
+function onselect_loadURI(tree) {
+ try {
+ var resource = tree.view.getResourceAtIndex(tree.currentIndex);
+ var link = tree.database.GetTarget(resource, NC_LINK, true);
+ if (link) {
+ link = link.QueryInterface(Ci.nsIRDFLiteral);
+ loadURI(link.Value);
+ }
+ } catch (e) {
+ }// when switching between tabs a spurious row number is returned.
+}
+
+function focusSearch() {
+ var searchBox = document.getElementById("findText");
+ searchBox.focus();
+}
+
+// doFind - Searches the help files for what is located in findText and outputs into
+// the find search tree.
+function doFind() {
+ if (document.getElementById("help-search-sidebar").hidden)
+ showSearchSidebar();
+
+ var searchTree = document.getElementById("help-search-tree");
+ var findText = document.getElementById("findText");
+
+ // clear any previous results.
+ clearDatabases(searchTree.database);
+
+ // if the search string is empty or contains only whitespace, purge the results tree and return
+ RE = findText.value.match(/\S+/g);
+ if (!RE) {
+ searchTree.builder.rebuild();
+ hideSearchSidebar();
+ return;
+ }
+
+ // compile the search string, which has already been split up above, into regexps
+ for (var i=0; i < RE.length; ++i) {
+ RE[i] = new RegExp(RE[i], "i");
+ }
+ emptySearch = true;
+
+ // search TOC
+ var resultsDS = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]
+ .createInstance(Ci.nsIRDFDataSource);
+ var sourceDS = helpTocPanel.database;
+ doFindOnDatasource(resultsDS, sourceDS, RDF_ROOT, 0);
+
+ // search glossary.
+ sourceDS = helpGlossaryPanel.database;
+ doFindOnDatasource(resultsDS, sourceDS, RDF_ROOT, 0);
+
+ // search index
+ sourceDS = helpIndexPanel.database;
+ doFindOnDatasource(resultsDS, sourceDS, RDF_ROOT, 0);
+
+ // search additional search datasources
+ sourceDS = helpSearchPanel.database;
+ doFindOnDatasource(resultsDS, sourceDS, RDF_ROOT, 0);
+
+ if (emptySearch)
+ assertSearchEmpty(resultsDS);
+ // Add the datasource to the search tree
+ searchTree.database.AddDataSource(resultsDS);
+ searchTree.builder.rebuild();
+}
+
+function clearDatabases(compositeDataSource) {
+ var enumDS = compositeDataSource.GetDataSources()
+ while (enumDS.hasMoreElements()) {
+ var ds = enumDS.getNext();
+ compositeDataSource.RemoveDataSource(ds);
+ }
+}
+
+function doFindOnDatasource(resultsDS, sourceDS, resource, level) {
+ if (level > MAX_LEVEL) {
+ try {
+ log("Recursive reference to resource: " + resource.Value + ".");
+ } catch (e) {
+ log("Recursive reference to unknown resource.");
+ }
+ return;
+ }
+ // find all SUBHEADING children of current resource.
+ var targets = sourceDS.GetTargets(resource, NC_SUBHEADINGS, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ // The first child of a rdf:subheading should (must) be a rdf:seq.
+ // Should we test for a SEQ here?
+ doFindOnSeq(resultsDS, sourceDS, target, level+1);
+ }
+}
+
+function doFindOnSeq(resultsDS, sourceDS, resource, level) {
+ // load up an RDFContainer so we can access the contents of the current
+ // rdf:seq.
+ RDFContainer.Init(sourceDS, resource);
+ var targets = RDFContainer.GetElements();
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ var link = sourceDS.GetTarget(target, NC_LINK, true);
+ var name = sourceDS.GetTarget(target, NC_NAME, true);
+
+ if (link && name instanceof Ci.nsIRDFLiteral && isMatch(name.Value)) {
+ // we have found a search entry - add it to the results datasource.
+ var urn = RDF.GetAnonymousResource();
+ resultsDS.Assert(urn, NC_NAME, name, true);
+ resultsDS.Assert(urn, NC_LINK, link, true);
+ resultsDS.Assert(RDF_ROOT, NC_CHILD, urn, true);
+
+ emptySearch = false;
+ }
+ // process any nested rdf:seq elements.
+ doFindOnDatasource(resultsDS, sourceDS, target, level+1);
+ }
+}
+
+function assertSearchEmpty(resultsDS) {
+ var resSearchEmpty = RDF.GetResource("urn:emptySearch");
+ resultsDS.Assert(RDF_ROOT,
+ NC_CHILD,
+ resSearchEmpty,
+ true);
+ resultsDS.Assert(resSearchEmpty,
+ NC_NAME,
+ RDF.GetLiteral(emptySearchText),
+ true);
+ resultsDS.Assert(resSearchEmpty,
+ NC_LINK,
+ RDF.GetLiteral(emptySearchLink),
+ true);
+}
+
+function isMatch(text) {
+ for (var i=0; i < RE.length; ++i ) {
+ if (!RE[i].test(text)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getAttribute(datasource, resource, attributeResourceName,
+ defaultValue) {
+ var literal = datasource.GetTarget(resource, attributeResourceName, true);
+ if (!literal) {
+ return defaultValue;
+ }
+ return getLiteralValue(literal, defaultValue);
+}
+
+function getLiteralValue(literal, defaultValue) {
+ if (literal) {
+ literal = literal.QueryInterface(Ci.nsIRDFLiteral);
+ if (literal) {
+ return literal.Value;
+ }
+ }
+ if (defaultValue) {
+ return defaultValue;
+ }
+ return null;
+}
+
+// Write debug string to error console.
+function log(aText) {
+ Services.console.logStringMessage(aText);
+}
+
+// getXulWin - Returns the current Help window as a nsIXULWindow.
+function getXulWin()
+{
+ window.QueryInterface(Ci.nsIInterfaceRequestor);
+ var webnav = window.getInterface(Ci.nsIWebNavigation);
+ var dsti = webnav.QueryInterface(Ci.nsIDocShellTreeItem);
+ var treeowner = dsti.treeOwner;
+ var ifreq = treeowner.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ return ifreq.getInterface(Ci.nsIXULWindow);
+}
+
+// toggleZLevel - Toggles whether or not the window will always appear on top. Because
+// alwaysRaised is not supported on an OS other than Windows, this code will not
+// be used in those builds.
+//
+// element - The DOM node that persists the checked state.
+function toggleZLevel(element)
+{
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ var xulwin = getXulWin();
+
+ // Now we can flip the zLevel, and set the attribute so that it persists correctly
+ if (xulwin.zLevel > xulwin.normalZ) {
+ xulwin.zLevel = xulwin.normalZ;
+ element.setAttribute("checked", "false");
+ } else {
+ xulwin.zLevel = xulwin.raisedZ;
+ element.setAttribute("checked", "true");
+ }
+}
+
+var helpContentListener = {
+ doContent: function(aContentType, aIsContentPreferred, aRequest, aContentHandler) {
+ throw Cr.NS_ERROR_UNEXPECTED;
+ },
+ isPreferred: function(aContentType, aDesiredContentType) {
+ return false;
+ },
+ canHandleContent: function(aContentType, aIsContentPreferred, aDesiredContentType) {
+ return false;
+ },
+ loadCookie: null,
+ parentContentListener: null,
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Ci.nsIURIContentListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
diff --git a/comm/suite/components/helpviewer/content/help.xul b/comm/suite/components/helpviewer/content/help.xul
new file mode 100644
index 0000000000..514d0ebb1b
--- /dev/null
+++ b/comm/suite/components/helpviewer/content/help.xul
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8"?>
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://communicator/skin/helpviewer/help.css" type="text/css"?>
+
+<?xul-overlay href="chrome://help/content/helpContextOverlay.xul"?>
+<!DOCTYPE window [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % helpDTD SYSTEM "chrome://help/locale/help.dtd">
+ %helpDTD;
+]>
+
+<window id="help"
+ windowtype="suite:help"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="700"
+ height="550"
+#ifdef XP_WIN
+ persist="width height screenX screenY zlevel"
+#else
+ persist="width height screenX screenY"
+#endif
+ onload="init();"
+ onunload="window.XULBrowserWindow.destroy();">
+
+ <script src="chrome://help/content/help.js"/>
+ <script src="chrome://global/content/viewZoomOverlay.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://communicator/content/findUtils.js"/>
+
+ <menupopup id="backMenu" position="after_start"
+ onpopupshowing="return createBackMenu(event);"
+ oncommand="gotoHistoryIndex(event);"/>
+ <menupopup id="forwardMenu" position="after_start"
+ onpopupshowing="return createForwardMenu(event);"
+ oncommand="gotoHistoryIndex(event);"/>
+ <popupset id="contentAreaContextSet"/>
+
+ <broadcasterset id="helpBroadcasters">
+ <broadcaster id="canGoBack" disabled="true"/>
+ <broadcaster id="canGoForward" disabled="true"/>
+ </broadcasterset>
+ <commandset id="globalEditMenuItems"/>
+ <commandset id="selectEditMenuItems">
+ <command id="cmd_close" oncommand="close();"/>
+ <command id="Help:Home" oncommand="goHome();"/>
+ <command id="Help:Back" oncommand="goBack();" observes="canGoBack"/>
+ <command id="Help:Forward" oncommand="goForward();" observes="canGoForward"/>
+ <command id="Help:ToggleSidebar" oncommand="toggleSidebar();"/>
+ <command id="cmd_closeWindow" oncommand="close();"/>
+ <command id="cmd_fullZoomReduce" oncommand="ZoomManager.reduce();"/>
+ <command id="cmd_fullZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
+ <command id="cmd_fullZoomReset" oncommand="ZoomManager.reset();"/>
+ <command id="cmd_find"
+ oncommand="findInPage(getFindInstData());"/>
+ <command id="cmd_findAgain"
+ oncommand="findAgainInPage(getFindInstData(), false);"/>
+ <command id="cmd_findPrevious"
+ oncommand="findAgainInPage(getFindInstData(), true);"/>
+ <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
+ <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/>
+ </commandset>
+ <keyset id="keys">
+ <key id="goHome" keycode="VK_HOME" command="Help:Home" modifiers="alt"/>
+#ifdef XP_UNIX
+ <key key="&goBackCmd.commandkey;" command="Help:Back" modifiers="accel"/>
+ <key key="&goForwardCmd.commandkey;" command="Help:Forward" modifiers="accel"/>
+#endif
+#ifdef XP_MACOSX
+ <key id="goBackKb" keycode="VK_LEFT" command="Help:Back" modifiers="accel"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Help:Forward" modifiers="accel"/>
+#else
+ <key id="goBackKb" keycode="VK_LEFT" command="Help:Back" modifiers="alt"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Help:Forward" modifiers="alt"/>
+ <key keycode="VK_BACK" command="Help:Back"/>
+ <key keycode="VK_BACK" command="Help:Forward" modifiers="shift"/>
+#endif
+ <key id="printKb" key="&printCmd.commandkey;" oncommand="print();"
+ modifiers="accel"/>
+ <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
+ <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
+ <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>
+ <key id="key_closeWindow" key="&closeWindow.commandkey;"
+ command="cmd_closeWindow" modifiers="accel"/>
+ <key id="key_closeSearchSidebar" keycode="VK_ESCAPE"
+ oncommand="hideSearchSidebar(event)"/>
+ <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge2" key="&fullZoomEnlargeCmd.commandkey2;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge3" key="&fullZoomEnlargeCmd.commandkey3;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;"
+ command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomReduce2" key="&fullZoomReduceCmd.commandkey2;"
+ command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;"
+ command="cmd_fullZoomReset" modifiers="accel"/>
+ <key id="key_fullZoomReset2" key="&fullZoomResetCmd.commandkey2;"
+ command="cmd_fullZoomReset" modifiers="accel"/>
+ <key id="key_focusSearch" key="&helpSearch.commandkey;"
+ oncommand="focusSearch()" modifiers="accel"/>
+
+ </keyset>
+ <stringbundle id="bundle_viewZoom"/>
+ <stringbundle id="findBundle"
+ src="chrome://global/locale/finddialog.properties"/>
+ <stringbundle id="bundle_help"
+ src="chrome://help/locale/help.properties"/>
+
+ <toolbox id="help-toolbox">
+ <toolbar id="HelpToolbar" class="chromeclass-toolbar">
+ <toolbarbutton id="help-back-button" type="menu-button"
+ label="&backButton.label;"
+ oncommand="if (event.target == this) goBack(); else gotoHistoryIndex(event);"
+ observes="canGoBack" context="backMenu"
+ tooltiptext="&backButton.tooltip;">
+ <menupopup context="" onpopupshowing="createBackMenu(event);"/>
+ </toolbarbutton>
+ <toolbarbutton id="help-forward-button" type="menu-button"
+ oncommand="if (event.target == this) goForward(); else gotoHistoryIndex(event);"
+ tooltiptext="&forwardButton.tooltip;"
+ observes="canGoForward">
+ <menupopup context="" onpopupshowing="createForwardMenu(event);"/>
+ </toolbarbutton>
+ <toolbarbutton id="help-home-button"
+ tooltiptext="&homeButton.tooltip;"
+ command="Help:Home"/>
+ <toolbarseparator/>
+ <toolbarbutton id="help-print-button"
+ label="&printButton.label;"
+ oncommand="print();"
+ tooltiptext="&printButton.tooltip;"/>
+ <toolbarspring flex="1"/>
+ <toolbaritem id="search-box"
+ align="center" pack="center">
+ <textbox id="findText"
+ type="search"
+ placeholder="&search.emptytext;"
+ aria-controls="help-toc-panel"
+ oncommand="showSidebar(); doFind();"/>
+ </toolbaritem>
+ </toolbar>
+ </toolbox>
+
+ <hbox flex="1">
+ <vbox id="help-sidebar" persist="width">
+ <vbox flex="1" id="help-toc-sidebar">
+ <sidebarheader align="center">
+ <label id="help-toc-sidebar-header" flex="1" crop="end" value="&toctab.label;"
+ accesskey="&toctab.accesskey;" control="help-toc-panel"/>
+ </sidebarheader>
+ <tree id="help-toc-panel" class="focusring"
+ flex="1" treelines="true" hidecolumnpicker="true"
+ datasources="rdf:null"
+ containment="http://home.netscape.com/NC-rdf#subheadings"
+ ref="urn:root" flags="dont-build-content"
+ onselect="onselect_loadURI(this)">
+ <template>
+ <rule>
+ <conditions>
+ <content uri="?uri"/>
+ <triple subject="?uri"
+ predicate="http://home.netscape.com/NC-rdf#subheadings"
+ object="?subheadings"/>
+ <member container="?subheadings"
+ child="?subheading"/>
+ <triple subject="?subheading"
+ predicate="http://home.netscape.com/NC-rdf#name"
+ object="?name"/>
+ </conditions>
+ <action>
+ <treechildren>
+ <treeitem uri="?subheading">
+ <treerow>
+ <treecell label="?name"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </action>
+ </rule>
+ </template>
+ <treecols>
+ <treecol id="NameColumn" flex="1" hideheader="true"
+ primary="true"/>
+ </treecols>
+ </tree>
+ </vbox>
+ <vbox id="help-search-sidebar" hidden="true" flex="1">
+ <sidebarheader align="center">
+ <label id="help-search-sidebar-header" flex="1" crop="end"
+ value="&searchHeader.label;"/>
+ </sidebarheader>
+ <tree id="help-search-tree" class="focusring"
+ flex="1" hidecolumnpicker="true"
+ datasources="rdf:null"
+ containment="http://home.netscape.com/NC-rdf#child"
+ ref="urn:root" flags="dont-build-content"
+ onselect="onselect_loadURI(this)">
+ <template>
+ <rule>
+ <conditions>
+ <content uri="?uri"/>
+ <member container="?uri"
+ child="?subheading"/>
+ </conditions>
+ <bindings>
+ <binding subject="?subheading"
+ predicate="http://home.netscape.com/NC-rdf#name"
+ object="?name"/>
+ </bindings>
+ <action>
+ <treechildren>
+ <treeitem uri="?subheading">
+ <treerow>
+ <treecell label="?name"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </action>
+ </rule>
+ </template>
+ <treecols>
+ <treecol id="ResultsColumn" flex="1"
+ hideheader="true" primary="true"
+ sortActive="true" sortDirection="ascending"
+ sort="?name"/>
+ </treecols>
+ </tree>
+ </vbox>
+
+ <!-- BEGIN hidden trees used for searching -->
+ <!-- xxxmpc: we need a better solution for this -->
+
+ <vbox id="help-sidebar-hidden-trees" hidden="true">
+ <tree id="help-glossary-panel"
+ flex="1" hidecolumnpicker="true"
+ datasources="rdf:null"
+ containment="http://home.netscape.com/NC-rdf#subheadings"
+ ref="urn:root" flags="dont-build-content"/>
+ <tree id="help-index-panel"
+ flex="1" datasources="rdf:null"
+ hidecolumnpicker="true"
+ containment="http://home.netscape.com/NC-rdf#subheadings"
+ ref="urn:root"
+ flags="dont-build-content dont-test-empty"/>
+ <tree id="help-search-panel"
+ flex="1" hidecolumnpicker="true"
+ datasources="rdf:null"
+ containment="http://home.netscape.com/NC-rdf#subheadings"
+ ref="urn:root" flags="dont-build-content"/>
+ </vbox>
+
+ <!-- END HIDDEN ITEMS -->
+ </vbox>
+
+ <splitter id="help-sidebar-splitter" collapse="before">
+ <grippy/>
+ </splitter>
+
+ <vbox id="appcontent" flex="3">
+ <!-- type attribute is used by frame construction to locate
+ iframes intended to hold (html) content -->
+ <browser context="contentAreaContextMenu"
+ type="content"
+ primary="true"
+ id="help-content"
+ src="about:blank"
+ flex="1"
+ onclick="return contentClick(event);"/>
+ <findbar id="FindToolbar" browserid="help-content"/>
+ <browser type="content"
+ id="help-external"
+ collapsed="true"/>
+
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/comm/suite/components/helpviewer/content/helpContextOverlay.xul b/comm/suite/components/helpviewer/content/helpContextOverlay.xul
new file mode 100644
index 0000000000..60cdaf69ca
--- /dev/null
+++ b/comm/suite/components/helpviewer/content/helpContextOverlay.xul
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!DOCTYPE overlay [
+ <!ENTITY % helpDTD SYSTEM "chrome://help/locale/help.dtd">
+ %helpDTD;
+]>
+<overlay id="contentAreaContextOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+# Help Window's right-click menu
+ <popupset id="contentAreaContextSet">
+ <menupopup id="contentAreaContextMenu"
+ onpopupshowing="goUpdateCommand('cmd_copy')">
+ <menuitem id="context-back"
+ label="&backButton.label;"
+ accesskey="&backButton.accesskey;"
+ observes="canGoBack"
+ oncommand="goBack()"/>
+ <menuitem id="context-forward"
+ label="&forwardButton.label;"
+ accesskey="&forwardButton.accesskey;"
+ observes="canGoForward"
+ oncommand="goForward()"/>
+ <menuseparator/>
+ <menuitem id="context-copy"
+ label="&copyCmd.label;"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"
+ disabled="true"/>
+ <menuitem id="context-selectall"
+ label="&selectAllCmd.label;"
+ accesskey="&selectAllCmd.accesskey;"
+ command="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem id="zoom-in"
+ label="&fullZoomEnlargeBtn.label;"
+ accesskey="&fullZoomEnlargeBtn.accesskey;"
+ oncommand="ZoomManager.enlarge();"/>
+ <menuitem id="zoom-out"
+ label="&fullZoomReduceBtn.label;"
+ accesskey="&fullZoomReduceBtn.accesskey;"
+ oncommand="ZoomManager.reduce();"/>
+#ifdef XP_WIN
+ <menuseparator/>
+ <menuitem id="context-zlevel"
+ type="checkbox"
+ checked="true"
+ persist="checked"
+ label="&zLevel.label;"
+ accesskey="&zLevel.accesskey;"
+ oncommand="toggleZLevel(this);"/>
+#endif
+ </menupopup>
+ </popupset>
+</overlay>
diff --git a/comm/suite/components/helpviewer/content/platformClasses.css b/comm/suite/components/helpviewer/content/platformClasses.css
new file mode 100644
index 0000000000..30332fdef3
--- /dev/null
+++ b/comm/suite/components/helpviewer/content/platformClasses.css
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%ifdef XP_WIN
+.noWin, .mac, .unix { display: none; }
+%else
+%ifdef XP_MACOSX
+.noMac, .win, .unix { display: none; }
+%else
+.noUnix, .win, .mac { display: none; }
+%endif
+%endif
diff --git a/comm/suite/components/helpviewer/jar.mn b/comm/suite/components/helpviewer/jar.mn
new file mode 100644
index 0000000000..d536245fef
--- /dev/null
+++ b/comm/suite/components/helpviewer/jar.mn
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+% content help %content/communicator/helpviewer/
+* content/communicator/helpviewer/help.xul (content/help.xul)
+ content/communicator/helpviewer/contextHelp.js (content/contextHelp.js)
+ content/communicator/helpviewer/help.js (content/help.js)
+* content/communicator/helpviewer/helpContextOverlay.xul (content/helpContextOverlay.xul)
+* content/communicator/helpviewer/platformClasses.css (content/platformClasses.css)
diff --git a/comm/suite/components/helpviewer/moz.build b/comm/suite/components/helpviewer/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/helpviewer/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/migration/SuiteProfileMigrator.js b/comm/suite/components/migration/SuiteProfileMigrator.js
new file mode 100644
index 0000000000..0ad4dfcca3
--- /dev/null
+++ b/comm/suite/components/migration/SuiteProfileMigrator.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } =
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const { FileUtils } =
+ ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+const { AppConstants } =
+ ChromeUtils.import('resource://gre/modules/AppConstants.jsm');
+
+ChromeUtils.defineModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+function ProfileMigrator() {
+}
+
+ProfileMigrator.prototype = {
+ migrate: function PM_migrate(aStartup) {
+ // By opening the wizard with a supplied migrator, it will automatically
+ // migrate from it.
+ let [key, migrator] = this._getDefaultMigrator();
+ if (!key)
+ return;
+
+ let params = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ params.appendElement(this._toString(key));
+ params.appendElement(migrator);
+ params.appendElement(aStartup);
+
+ Services.ww.openWindow(null,
+ "chrome://communicator/content/migration/migration.xul",
+ "_blank",
+ "chrome,dialog,modal,centerscreen,titlebar",
+ params);
+ },
+
+ _toString: function PM__toString(aStr) {
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = aStr;
+ return str;
+ },
+
+ _getMigratorIfSourceExists: function PM__getMigratorIfSourceExists(aKey) {
+ let cid = "@mozilla.org/profile/migrator;1?app=suite&type=" + aKey;
+ let migrator = Cc[cid].createInstance(Ci.nsISuiteProfileMigrator);
+ if (migrator.sourceExists)
+ return migrator;
+ return null;
+ },
+
+ // We don't yet support checking for the default browser on all platforms,
+ // needless to say we don't have migrators for all browsers. Thus, for each
+ // platform, there's a fallback list of migrators used in these cases.
+ _PLATFORM_FALLBACK_LIST:
+ ["thunderbird"],
+
+ _getDefaultMigrator: function PM__getDefaultMigrator() {
+
+ let migratorsOrdered = Array.from(this._PLATFORM_FALLBACK_LIST);
+
+ // FIXME This is all so not working currently.
+ // There are currently no migrators for browsers available.
+ // See Bug 739056.
+ if (false) {
+ let defaultBrowser = "";
+
+ if (AppConstants.platform == "win") {
+ try {
+ const REG_KEY = "SOFTWARE\\Classes\\HTTP\\shell\\open\\command";
+ let regKey = Cc["@mozilla.org/windows-registry-key;1"]
+ .createInstance(Ci.nsIWindowsRegKey);
+ regKey.open(regKey.ROOT_KEY_LOCAL_MACHINE, REG_KEY,
+ regKey.ACCESS_READ);
+ let value = regKey.readStringValue("").toLowerCase();
+ let pathMatches = value.match(/^"?(.+?\.exe)"?/);
+ if (!pathMatches) {
+ throw new Error("Could not extract path from " +
+ REG_KEY + "(" + value + ")");
+ }
+
+ // We want to find out what the default browser is but the path in and of
+ // itself isn't enough. Why? Because sometimes on Windows paths get
+ // truncated like so: C:\PROGRA~1\MOZILL~2\MOZILL~1.EXE. How do we know
+ // what product that is? Mozilla's file objects do nothing to 'normalize'
+ // the path so we need to attain an actual product descriptor from the
+ // file somehow, and in this case it means getting the "InternalName"
+ // field of the file's VERSIONINFO resource.
+ //
+ // In the file's resource segment there is a VERSIONINFO section that is
+ // laid out like this:
+ //
+ // VERSIONINFO
+ // StringFileInfo
+ // <TranslationID>
+ // InternalName "iexplore"
+ // VarFileInfo
+ // Translation <TranslationID>
+ //
+ // By Querying the VERSIONINFO section for its Tranlations, we can find
+ // out where the InternalName lives (A file can have more than one
+ // translation of its VERSIONINFO segment, but we just assume the first
+ // one).
+ let file = FileUtils.File(pathMatches[1])
+ .QueryInterface(Ci.nsILocalFileWin);
+ switch (file.getVersionInfoField("InternalName").toLowerCase()) {
+ case "iexplore":
+ defaultBrowser = "ie";
+ break;
+ case "chrome":
+ defaultBrowser = "chrome";
+ break;
+ }
+ }
+ catch (ex) {
+ Cu.reportError("Could not retrieve default browser: " + ex);
+ }
+ }
+
+ // If we found the default browser and we have support for that browser,
+ // make sure to check it before any other browser, by moving it to the head
+ // of the array.
+ if (defaultBrowser) {
+ migratorsOrdered.sort((a, b) => b == defaultBrowser ? 1 : 0);
+ }
+ }
+
+ for (let key of migratorsOrdered) {
+ let migrator = this._getMigratorIfSourceExists(key);
+ if (migrator) {
+ return [key, migrator];
+ }
+ }
+
+ return ["", null];
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIProfileMigrator]),
+ classDescription: "Profile Migrator",
+ contractID: "@mozilla.org/toolkit/profile-migrator;1",
+ classID: Components.ID("{d5148b7c-ba4e-4f7a-a80b-1ae48b90b910}"),
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([ProfileMigrator]);
diff --git a/comm/suite/components/migration/SuiteProfileMigrator.manifest b/comm/suite/components/migration/SuiteProfileMigrator.manifest
new file mode 100644
index 0000000000..a57376b855
--- /dev/null
+++ b/comm/suite/components/migration/SuiteProfileMigrator.manifest
@@ -0,0 +1,2 @@
+component {d5148b7c-ba4e-4f7a-a80b-1ae48b90b910} SuiteProfileMigrator.js
+contract @mozilla.org/toolkit/profile-migrator;1 {d5148b7c-ba4e-4f7a-a80b-1ae48b90b910}
diff --git a/comm/suite/components/migration/content/migration.js b/comm/suite/components/migration/content/migration.js
new file mode 100644
index 0000000000..b106e51e22
--- /dev/null
+++ b/comm/suite/components/migration/content/migration.js
@@ -0,0 +1,413 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const NS_PROFILE_MIGRATOR_CONTRACTID =
+ "@mozilla.org/profile/migrator;1?app=suite&type=";
+
+var MigrationWizard = {
+ _source: "", // Source Profile Migrator ContractID suffix
+ _itemsFlags: Ci.nsISuiteProfileMigrator.ALL, // Selected Import Data Sources
+ _selectedProfile: null, // Selected Profile name to import from
+ _wiz: null, // Shortcut to the wizard
+ _migrator: null, // The actual profile migrator.
+ _autoMigrate: null, // Whether or not we are actually migrating.
+ _singleItem: false, // Are we choosing just to import a single
+ // item into the current profile?
+ _newHomePage: null, // Are we setting a new home page - what to?
+
+ init: function() {
+ Services.obs.addObserver(this, "Migration:Started");
+ Services.obs.addObserver(this, "Migration:ItemBeforeMigrate");
+ Services.obs.addObserver(this, "Migration:ItemAfterMigrate");
+ Services.obs.addObserver(this, "Migration:Ended");
+ Services.obs.addObserver(this, "Migration:Progress");
+
+ this._wiz = document.documentElement;
+ this._wiz.canRewind = false;
+
+ if ("arguments" in window) {
+ if ("arguments" in window && window.arguments[0] == "bookmarks") {
+ this._singleItem = true;
+ this._itemsFlags = Ci.nsISuiteProfileMigrator.BOOKMARKS;
+ document.getElementById("fromFile").hidden = false;
+ document.getElementById("importBookmarks").hidden = false;
+ document.getElementById("importAll").hidden = true;
+ }
+ else if (window.arguments.length > 1) {
+ this._source = window.arguments[0];
+ this._migrator =
+ window.arguments[1] instanceof Ci.nsISuiteProfileMigrator ?
+ window.arguments[1] : null;
+ this._autoMigrate = window.arguments[2]
+ .QueryInterface(Ci.nsIProfileStartup);
+ // Show the "nothing" option in the automigrate case to provide an
+ // easily identifiable way to avoid migration and create a new profile.
+ document.getElementById("nothing").hidden = false;
+ }
+ }
+
+ this.onImportSourcePageShow();
+ },
+
+ uninit: function() {
+ Services.obs.removeObserver(this, "Migration:Started");
+ Services.obs.removeObserver(this, "Migration:ItemBeforeMigrate");
+ Services.obs.removeObserver(this, "Migration:ItemAfterMigrate");
+ Services.obs.removeObserver(this, "Migration:Ended");
+ Services.obs.removeObserver(this, "Migration:Progress");
+ },
+
+ // 1 - Import Source
+ onImportSourcePageShow: function() {
+ // Figure out what source apps are are available to import from:
+ var group = document.getElementById("importSourceGroup");
+ var firstSelectable = null;
+ for (var i = 0; i < group.childNodes.length; ++i) {
+ var suffix = group.childNodes[i].id;
+ if (suffix != "nothing" && suffix != "fromFile") {
+ var contractID = NS_PROFILE_MIGRATOR_CONTRACTID + suffix;
+ var migrator = null;
+ if (contractID in Cc) {
+ migrator = Cc[contractID]
+ .createInstance(Ci.nsISuiteProfileMigrator);
+ } else {
+ dump("*** invalid contractID =" + contractID + "\n");
+ // This is an invalid contract id, therefore hide this element
+ // and allow things to continue - that way we should be able to
+ // copy with anything happening.
+ group.childNodes[i].hidden = true;
+ break;
+ }
+
+ // Ensure that we only allow import selections for profile
+ // migrators that support the requested action.
+ if (!migrator.sourceExists ||
+ !(migrator.supportedItems & this._itemsFlags)) {
+ group.childNodes[i].hidden = true;
+ break;
+ }
+
+ if (!firstSelectable && !group.childNodes[i].disabled &&
+ !group.childNodes[i].hidden) {
+ firstSelectable = group.childNodes[i];
+ }
+ }
+ }
+
+ if (this._source) {
+ // Somehow the Profile Migrator got confused, and gave us a migrate source
+ // that doesn't actually exist. This could be because of a bogus registry
+ // state. Set the _source property to null so the first visible item in
+ // the list is selected instead.
+ if (document.getElementById(this._source).hidden)
+ this._source = null;
+ }
+ group.selectedItem = this._source ?
+ document.getElementById(this._source) : firstSelectable;
+ },
+
+ onImportSourcePageAdvanced: function() {
+ var newSource = document.getElementById("importSourceGroup").value;
+
+ if (newSource == "nothing" || newSource == "fromFile") {
+ if (newSource == "fromFile") {
+ window.opener.fromFile = true;
+ }
+ document.documentElement.cancel();
+ // Don't let the wizard migrate to the next page event though we've
+ // called cancel - cancel may not get processed first.
+ return false;
+ }
+
+ if (!this._migrator || newSource != this._source) {
+ // Create the migrator for the selected source.
+ var contractID = NS_PROFILE_MIGRATOR_CONTRACTID + newSource;
+ this._migrator = Cc[contractID]
+ .createInstance(Ci.nsISuiteProfileMigrator);
+
+ this._selectedProfile = null;
+ }
+ this._source = newSource;
+
+ // check for more than one source profile
+ if (this._migrator.sourceHasMultipleProfiles)
+ this._wiz.currentPage.next = "selectProfile";
+ else {
+ if (this._autoMigrate)
+ this._wiz.currentPage.next = "homePageImport";
+ else if (this._singleItem)
+ this._wiz.currentPage.next = "migrating";
+ else
+ this._wiz.currentPage.next = "importItems";
+
+ var sourceProfiles = this._migrator.sourceProfiles;
+ if (sourceProfiles && sourceProfiles.length == 1) {
+ this._selectedProfile = sourceProfiles
+ .queryElementAt(0, Ci.nsISupportsString).data;
+ }
+ else
+ this._selectedProfile = "";
+ }
+ return true;
+ },
+
+ // 2 - [Profile Selection]
+ onSelectProfilePageShow: function() {
+ var profiles = document.getElementById("profiles");
+ while (profiles.hasChildNodes())
+ profiles.lastChild.remove();
+
+ var sourceProfiles = this._migrator.sourceProfiles;
+ var count = sourceProfiles.length;
+ for (var i = 0; i < count; ++i) {
+ var item = document.createElement("radio");
+ item.id = sourceProfiles.queryElementAt(i, Ci.nsISupportsString).data;
+ item.setAttribute("label", item.id);
+ profiles.appendChild(item);
+ }
+
+ profiles.selectedItem = this._selectedProfile ?
+ document.getElementById(this._selectedProfile) : profiles.firstChild;
+ },
+
+ onSelectProfilePageAdvanced: function() {
+ this._selectedProfile = document.getElementById("profiles").selectedItem.id;
+
+ // If we're automigrating or just doing bookmarks don't show the item
+ // selection page
+ if (this._autoMigrate)
+ this._wiz.currentPage.next = "homePageImport";
+ else if (this._singleItem)
+ this._wiz.currentPage.next = "migrating"
+ },
+
+ // 3 - ImportItems
+ onImportItemsPageShow: function() {
+ var dataSources = document.getElementById("dataSources");
+ while (dataSources.hasChildNodes())
+ dataSources.lastChild.remove();
+
+ var bundle = document.getElementById("bundle");
+
+ var items = this._migrator.getMigrateData(this._selectedProfile,
+ this._autoMigrate);
+
+ for (var i = 0; i < 16; ++i) {
+ var itemID = items & (1 << i);
+ if (itemID) {
+ var checkbox = document.createElement("checkbox");
+ checkbox.id = itemID;
+ try {
+ checkbox.setAttribute("label",
+ bundle.getString(itemID + "_" + this._source));
+ }
+ catch (ex) {
+ checkbox.setAttribute("label",
+ bundle.getString(itemID + "_generic"));
+ }
+ dataSources.appendChild(checkbox);
+ if (this._itemsFlags & itemID)
+ checkbox.setAttribute("checked", true);
+ }
+ }
+ },
+
+ onImportItemsPageRewound: function() {
+ this._wiz.canAdvance = true;
+ this.onImportItemsPageAdvanced();
+ },
+
+ onImportItemsPageAdvanced: function() {
+ var dataSources = document.getElementById("dataSources");
+ this._itemsFlags = 0;
+ for (var i = 0; i < dataSources.childNodes.length; ++i) {
+ var checkbox = dataSources.childNodes[i];
+ if (checkbox.checked)
+ this._itemsFlags |= checkbox.id;
+ }
+ },
+
+ onImportItemCommand: function(aEvent) {
+ this._wiz.canAdvance = document
+ .getElementById("dataSources")
+ .getElementsByAttribute("checked", "true").item(0) != null;
+ },
+
+ // 4 - Home Page Selection
+ onHomePageMigrationPageShow: function() {
+ // only want this on the first run
+ if (!this._autoMigrate) {
+ this._wiz.advance();
+ return;
+ }
+
+ var bundle = document.getElementById("bundle");
+ var pageTitle = bundle.getString("homePageMigrationPageTitle");
+ var pageDesc = bundle.getString("homePageMigrationDescription");
+
+ document.getElementById("homePageImport").setAttribute("label", pageTitle);
+ document.getElementById("homePageImportDesc")
+ .setAttribute("value", pageDesc);
+
+ this._wiz._adjustWizardHeader();
+
+ var homePageStart = document.getElementById("homePageStart");
+
+ // Find out if the target profile already has a homepage or not
+ var mainStr = this.targetHasHomePageURL() ?
+ bundle.getString("homePageStartCurrent") :
+ bundle.getString("homePageStartDefault");
+
+ homePageStart.setAttribute("label", mainStr);
+
+ var source = null;
+ if (this._source != "") {
+ source = "sourceName" + this._source;
+ }
+
+ var availableItems = this._migrator.getMigrateData(this._selectedProfile,
+ this._autoMigrate);
+
+ if (source && (availableItems & Ci.nsISuiteProfileMigrator.HOMEPAGEDATA)) {
+ var appName = document.getElementById("bundle").getString(source);
+ var oldHomePageLabel = bundle.getFormattedString("homePageImport",
+ [appName]);
+ var oldHomePage = document.getElementById("oldHomePage");
+ oldHomePage.setAttribute("label", oldHomePageLabel);
+ oldHomePage.setAttribute("value", "source");
+ oldHomePage.removeAttribute("hidden");
+ oldHomePage.focus();
+
+ document.getElementById("homePageRadioGroup").selectedItem = oldHomePage;
+ }
+ else {
+ // if we don't have at least two options, just advance
+ this._wiz.advance();
+ }
+ },
+
+ onHomePageMigrationPageAdvanced: function() {
+ // we might not have a selectedItem if we're in fallback mode
+ try {
+ this._newHomePage = document.getElementById("homePageRadioGroup")
+ .value;
+ } catch(ex) {}
+ },
+
+ // 5 - Migrating
+ onMigratingPageShow: function() {
+ this._wiz.getButton("cancel").disabled = true;
+ this._wiz.canRewind = false;
+ this._wiz.canAdvance = false;
+
+ // When migrating a profile on startup, show all of the data that can be
+ // received from this source, but exclude home pages if the user didn't
+ // want to migrate it.
+ if (this._autoMigrate) {
+ this._itemsFlags = this._migrator.getMigrateData(this._selectedProfile,
+ this._autoMigrate);
+ if (!this._newHomePage)
+ this._itemsFlags &= ~Ci.nsISuiteProfileMigrator.HOMEPAGEDATA;
+ }
+
+ this._listItems("migratingItems");
+ setTimeout(this.onMigratingMigrate, 0, this);
+ },
+
+ onMigratingMigrate: function(aOuter) {
+ aOuter._migrator.migrate(aOuter._itemsFlags,
+ aOuter._autoMigrate,
+ aOuter._selectedProfile);
+ },
+
+ _listItems: function(aID) {
+ var items = document.getElementById(aID);
+ while (items.hasChildNodes())
+ items.lastChild.remove();
+
+ var bundle = document.getElementById("bundle");
+ var itemID;
+ for (var x = 1; x < Ci.nsISuiteProfileMigrator.ALL;
+ x = x << 1) {
+ if (x & this._itemsFlags) {
+ var label = document.createElement("label");
+ label.id = x + "_migrated";
+ try {
+ label.setAttribute("value",
+ bundle.stringBundle
+ .GetStringFromName(x + "_"+ this._source));
+ label.setAttribute("class", "migration-pending");
+ items.appendChild(label);
+ }
+ catch (ex) {
+ try {
+ label.setAttribute("value", bundle.getString(x + "_generic"));
+ label.setAttribute("class", "migration-pending");
+ items.appendChild(label);
+ }
+ catch (e) {
+ // if the block above throws, we've enumerated all the import
+ // data types we currently support and are now just wasting time.
+ break;
+ }
+ }
+ }
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Migration:Progress":
+ document.getElementById("progressBar").value = aData;
+ break;
+ case "Migration:Started":
+ break;
+ case "Migration:ItemBeforeMigrate":
+ var label = document.getElementById(aData + "_migrated");
+ if (label)
+ label.setAttribute("class", "migration-in-progress");
+ break;
+ case "Migration:ItemAfterMigrate":
+ var label = document.getElementById(aData + "_migrated");
+ if (label)
+ label.setAttribute("class", "migration-finished");
+ break;
+ case "Migration:Ended":
+ // We're done now.
+ this._wiz.canAdvance = true;
+ this._wiz.advance();
+
+ if (this._autoMigrate)
+ setTimeout(close, 5000);
+
+ break;
+ }
+ },
+
+ onDonePageShow: function() {
+ this._wiz.getButton("cancel").disabled = true;
+ this._wiz.canRewind = false;
+ this._listItems("doneItems");
+ },
+
+ targetHasHomePageURL: function() {
+ var targetPrefsFile = this._autoMigrate.directory;
+
+ targetPrefsFile.append("prefs.js");
+
+ // If the target prefs file doesn't exist, then we can't have a
+ // homepage set in the target profile.
+ if (!targetPrefsFile.exists())
+ return false;
+
+ Services.prefs.resetPrefs();
+
+ Services.prefs.readUserPrefsFromFile(targetPrefsFile);
+
+ return Services.prefs.prefHasUserValue("browser.startup.homepage");
+ }
+};
diff --git a/comm/suite/components/migration/content/migration.xul b/comm/suite/components/migration/content/migration.xul
new file mode 100644
index 0000000000..ad8b279361
--- /dev/null
+++ b/comm/suite/components/migration/content/migration.xul
@@ -0,0 +1,95 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/migration/migration.dtd" >
+
+<wizard id="migrationWizard"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&migrationWizard.title;"
+ onload="MigrationWizard.init()"
+ onunload="MigrationWizard.uninit()"
+ style="width: 40em; height: 32em;"
+ branded="true"
+ buttons="accept,cancel"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
+
+ <script src="chrome://communicator/content/migration/migration.js"/>
+
+ <stringbundle id="bundle"
+ src="chrome://communicator/locale/migration/migration.properties"/>
+
+ <wizardpage id="importSource" pageid="importSource" next="selectProfile"
+ label="&importSource.title;"
+ onpageadvanced="return MigrationWizard.onImportSourcePageAdvanced();">
+ <label id="importAll"
+ control="importSourceGroup">&importAllFrom.label;</label>
+ <label id="importBookmarks" control="importSourceGroup"
+ hidden="true">&importBookmarksFrom.label;</label>
+
+ <radiogroup id="importSourceGroup" align="start">
+ <radio id="thunderbird" label="&importFromThunderbird.label;"
+ accesskey="&importFromThunderbird.accesskey;"
+ value="thunderbird"/>
+ <!-- fromfile is used for bookmark importing -->
+ <radio id="fromFile" label="&importFromFile.label;" value="fromFile"
+ accesskey="&importFromFile.accesskey;" hidden="true"/>
+ <radio id="nothing" label="&importFromNothing.label;" value="nothing"
+ accesskey="&importFromNothing.accesskey;" hidden="true"/>
+ </radiogroup>
+ </wizardpage>
+
+ <wizardpage id="selectProfile" pageid="selectProfile"
+ label="&selectProfile.title;" next="importItems"
+ onpageshow="return MigrationWizard.onSelectProfilePageShow();"
+ onpageadvanced="return MigrationWizard.onSelectProfilePageAdvanced();">
+ <label control="profiles">&selectProfile.label;</label>
+
+ <radiogroup id="profiles" align="left"/>
+ </wizardpage>
+
+ <wizardpage id="importItems" pageid="importItems" label="&importItems.title;"
+ next="homePageImport"
+ onpageshow="return MigrationWizard.onImportItemsPageShow();"
+ onpageadvanced="return MigrationWizard.onImportItemsPageAdvanced();"
+ oncommand="MigrationWizard.onImportItemCommand();">
+ <label control="dataSources">&importItems.label;</label>
+
+ <vbox id="dataSources" style="overflow-y: auto;"
+ align="left" flex="1" role="group"/>
+ </wizardpage>
+
+ <wizardpage id="homePageImport" pageid="homePageImport"
+ next="migrating"
+ onpageshow="return MigrationWizard.onHomePageMigrationPageShow();"
+ onpageadvanced="return MigrationWizard.onHomePageMigrationPageAdvanced();">
+
+ <label id="homePageImportDesc" control="homePageRadioGroup"/>
+ <radiogroup id="homePageRadioGroup" align="start">
+ <radio id="oldHomePage" hidden="true"/>
+ <radio id="homePageStart" selected="true"/>
+ </radiogroup>
+ </wizardpage>
+
+ <wizardpage id="migrating" pageid="migrating" label="&migrating.title;"
+ next="done" onpageshow="MigrationWizard.onMigratingPageShow();">
+ <label control="migratingItems">&migrating.label;</label>
+
+ <vbox id="migratingItems" style="overflow-y: auto;" align="left" role="group"/>
+
+ <hbox>
+ <progressmeter class="progressmeter-statusbar" id="progressBar"
+ flex="1" mode="normal" value="0"/>
+ </hbox>
+ </wizardpage>
+
+ <wizardpage id="done" pageid="done" label="&done.title;"
+ onpageshow="MigrationWizard.onDonePageShow();">
+ <label control="doneItems">&done.label;</label>
+
+ <vbox id="doneItems" style="overflow-y: auto;" align="left" role="group"/>
+ </wizardpage>
+</wizard>
diff --git a/comm/suite/components/migration/jar.mn b/comm/suite/components/migration/jar.mn
new file mode 100644
index 0000000000..a202ce87a1
--- /dev/null
+++ b/comm/suite/components/migration/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/migration/migration.xul (content/migration.xul)
+ content/communicator/migration/migration.js (content/migration.js)
diff --git a/comm/suite/components/migration/moz.build b/comm/suite/components/migration/moz.build
new file mode 100644
index 0000000000..ef93d7617a
--- /dev/null
+++ b/comm/suite/components/migration/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+EXTRA_COMPONENTS += [
+ "SuiteProfileMigrator.js",
+ "SuiteProfileMigrator.manifest",
+]
+
+FINAL_LIBRARY = "suite"
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/migration/public/moz.build b/comm/suite/components/migration/public/moz.build
new file mode 100644
index 0000000000..a25b10157b
--- /dev/null
+++ b/comm/suite/components/migration/public/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsISuiteProfileMigrator.idl",
+]
+
+XPIDL_MODULE = "suitemigration"
+
+EXPORTS += [
+ "nsSuiteMigrationCID.h",
+]
diff --git a/comm/suite/components/migration/public/nsISuiteProfileMigrator.idl b/comm/suite/components/migration/public/nsISuiteProfileMigrator.idl
new file mode 100644
index 0000000000..ca0bd98413
--- /dev/null
+++ b/comm/suite/components/migration/public/nsISuiteProfileMigrator.idl
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIProfileStartup;
+
+[scriptable, uuid(b6adb2b8-5e3b-4fdd-b085-d58998b5c21a)]
+interface nsISuiteProfileMigrator : nsISupports
+{
+ /**
+ * profile items to migrate. use with migrate().
+ */
+ const unsigned short SETTINGS = 0x0001;
+ const unsigned short COOKIES = 0x0002;
+ const unsigned short HISTORY = 0x0004;
+ const unsigned short HOMEPAGEDATA = 0x0008;
+ const unsigned short PASSWORDS = 0x0010;
+ const unsigned short BOOKMARKS = 0x0020;
+ const unsigned short OTHERDATA = 0x0040;
+ const unsigned short ACCOUNT_SETTINGS = 0x0080;
+ const unsigned short ADDRESSBOOK_DATA = 0x0100;
+ const unsigned short JUNKTRAINING = 0x0200;
+ const unsigned short NEWSDATA = 0x0400;
+ const unsigned short MAILDATA = 0x0800;
+ const unsigned short ALL = 0x0FFF;
+
+ /**
+ * Copy user profile information to the current active profile.
+ *
+ * @param aItems list of data items to migrate. see above for values.
+ * @param aReplace replace or append current data where applicable.
+ * @param aProfile profile to migrate from, if there is more than one.
+ */
+ void migrate(in unsigned short aItems, in nsIProfileStartup aStartup,
+ in wstring aProfile);
+
+ /**
+ * A bit field containing profile items that this migrator is able
+ * to import for a specified source profile.
+ *
+ * @param aProfile the profile that we are looking for available data
+ * to import
+ * @param aStarting "true" if the profile is not currently being used.
+ * @returns bit field containing profile items (see above)
+ */
+ unsigned short getMigrateData(in wstring aProfile, in boolean aDoingStartup);
+
+ /**
+ * A bit field containing profile items that this migrator may be able
+ * to import for any source profile of its type.
+ */
+ readonly attribute unsigned short supportedItems;
+
+ /**
+ * Whether or not there is any data that can be imported from this
+ * browser (i.e. whether or not it is installed, and there exists
+ * a user profile)
+ */
+ readonly attribute boolean sourceExists;
+
+ /**
+ * Whether or not the import source implementing this interface
+ * has multiple user profiles configured.
+ */
+ readonly attribute boolean sourceHasMultipleProfiles;
+
+ /**
+ * An enumeration of available profiles. If the import source does
+ * not support profiles, this attribute is null.
+ */
+ readonly attribute nsIArray sourceProfiles;
+};
diff --git a/comm/suite/components/migration/public/nsSuiteMigrationCID.h b/comm/suite/components/migration/public/nsSuiteMigrationCID.h
new file mode 100644
index 0000000000..252f5ed9df
--- /dev/null
+++ b/comm/suite/components/migration/public/nsSuiteMigrationCID.h
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#define NS_SUITEPROFILEMIGRATOR_CONTRACTID_PREFIX "@mozilla.org/profile/migrator;1?app=suite&type="
+
+#define NS_THUNDERBIRDPROFILEMIGRATOR_CID \
+{ 0x6ba91adb, 0xa4ed, 0x405f, { 0xbd, 0x6c, 0xe9, 0x04, 0xa9, 0x9d, 0x9a, 0xd8 } }
diff --git a/comm/suite/components/migration/src/moz.build b/comm/suite/components/migration/src/moz.build
new file mode 100644
index 0000000000..367fecf45e
--- /dev/null
+++ b/comm/suite/components/migration/src/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "nsSuiteProfileMigratorBase.cpp",
+ "nsSuiteProfileMigratorUtils.cpp",
+ "nsThunderbirdProfileMigrator.cpp",
+]
+
+FINAL_LIBRARY = "suite"
diff --git a/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.cpp b/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.cpp
new file mode 100644
index 0000000000..521fa32215
--- /dev/null
+++ b/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.cpp
@@ -0,0 +1,844 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsSuiteProfileMigratorUtils.h"
+#include "nsCRT.h"
+#include "nsIFile.h"
+#include "nsILineInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIPrefService.h"
+#include "nsIServiceManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIURL.h"
+#include "nsSuiteProfileMigratorBase.h"
+#include "nsNetUtil.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsServiceManagerUtils.h"
+#include "prtime.h"
+#include "nsINIParser.h"
+#include "nsArrayUtils.h"
+
+#define MAIL_DIR_50_NAME u"Mail"_ns
+#define IMAP_MAIL_DIR_50_NAME u"ImapMail"_ns
+#define NEWS_DIR_50_NAME u"News"_ns
+#define DIR_NAME_CHROME u"chrome"_ns
+
+NS_IMPL_ISUPPORTS(nsSuiteProfileMigratorBase, nsISuiteProfileMigrator,
+ nsITimerCallback)
+
+using namespace mozilla;
+
+///////////////////////////////////////////////////////////////////////////////
+// nsITimerCallback
+
+NS_IMETHODIMP
+nsSuiteProfileMigratorBase::Notify(nsITimer *timer) {
+ CopyNextFolder();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSuiteProfileMigratorBase::GetName(nsACString& aName) {
+ aName.AssignLiteral("nsSuiteProfileMigratorBase");
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsSuiteProfileMigratorBase
+
+nsSuiteProfileMigratorBase::nsSuiteProfileMigratorBase() {
+ mFileCopyTransactionIndex = 0;
+ mObserverService = do_GetService("@mozilla.org/observer-service;1");
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsISuiteProfileMigrator methods
+
+NS_IMETHODIMP
+nsSuiteProfileMigratorBase::GetSourceExists(bool* aResult) {
+ nsCOMPtr<nsIArray> profiles;
+ GetSourceProfiles(getter_AddRefs(profiles));
+
+ if (profiles) {
+ uint32_t count;
+ profiles->GetLength(&count);
+ *aResult = count > 0;
+ }
+ else
+ *aResult = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSuiteProfileMigratorBase::GetSourceHasMultipleProfiles(bool* aResult) {
+ nsCOMPtr<nsIArray> profiles;
+ GetSourceProfiles(getter_AddRefs(profiles));
+
+ if (profiles) {
+ uint32_t count;
+ profiles->GetLength(&count);
+ *aResult = count > 1;
+ }
+ else
+ *aResult = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSuiteProfileMigratorBase::GetSourceProfiles(nsIArray** aResult) {
+ if (!mProfileNames && !mProfileLocations) {
+ nsresult rv;
+ mProfileNames = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mProfileLocations = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ // Fills mProfileNames and mProfileLocations
+ FillProfileDataFromRegistry();
+ }
+
+ NS_IF_ADDREF(*aResult = mProfileNames);
+ return NS_OK;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Pref Transform methods
+
+#define GETPREF(xform, method, value) \
+ xform->prefHasValue = NS_SUCCEEDED(aBranch->method(xform->sourcePrefName, value)); \
+ return NS_OK;
+
+#define SETPREF(xform, method, value) \
+ if (xform->prefHasValue) { \
+ return aBranch->method(xform->targetPrefName ? \
+ xform->targetPrefName : \
+ xform->sourcePrefName, value); \
+ } \
+ return NS_OK;
+
+nsresult
+nsSuiteProfileMigratorBase::GetString(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ PrefTransform* xform = (PrefTransform*)aTransform;
+ nsCString str;
+ nsresult rv = aBranch->GetCharPref(xform->sourcePrefName, str);
+ if (NS_SUCCEEDED(rv)) {
+ xform->prefHasValue = true;
+ xform->stringValue = moz_xstrdup(str.get());
+ }
+ return rv;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::SetString(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ PrefTransform* xform = (PrefTransform*) aTransform;
+ SETPREF(aTransform, SetCharPref,
+ nsDependentCString(xform->stringValue));
+}
+
+nsresult
+nsSuiteProfileMigratorBase::GetBool(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ GETPREF(aTransform, GetBoolPref, &aTransform->boolValue)
+}
+
+nsresult
+nsSuiteProfileMigratorBase::SetBool(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ SETPREF(aTransform, SetBoolPref, aTransform->boolValue)
+}
+
+nsresult
+nsSuiteProfileMigratorBase::GetInt(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ GETPREF(aTransform, GetIntPref, &aTransform->intValue)
+}
+
+nsresult
+nsSuiteProfileMigratorBase::SetInt(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ SETPREF(aTransform, SetIntPref, aTransform->intValue)
+}
+
+nsresult
+nsSuiteProfileMigratorBase::SetFile(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ // In this case targetPrefName is just an additional preference
+ // that needs to be modified and not what the sourcePrefName is
+ // going to be saved to once it is modified.
+ nsresult rv = NS_OK;
+ if (aTransform->prefHasValue) {
+ nsCOMPtr<nsIProtocolHandler> handler;
+ nsCOMPtr<nsIIOService> ioService(do_GetIOService());
+ if (!ioService)
+ return NS_OK;
+ rv = ioService->GetProtocolHandler("file", getter_AddRefs(handler));
+ if (NS_FAILED(rv))
+ return NS_OK;
+ nsCOMPtr<nsIFileProtocolHandler> fileHandler(do_QueryInterface(handler));
+ if (!fileHandler)
+ return NS_OK;
+
+ nsCString fileURL(aTransform->stringValue);
+ nsCOMPtr<nsIFile> file;
+ // Start off by assuming fileURL is a URL spec and
+ // try and get a File from it.
+ rv = fileHandler->GetFileFromURLSpec(fileURL, getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ // Okay it wasn't a URL spec so assume it is a localfile,
+ // if this fails then just don't set anything.
+ rv = NS_NewNativeLocalFile(fileURL, false, getter_AddRefs(file));
+ if (NS_FAILED(rv))
+ return NS_OK;
+ }
+ // Now test to see if File exists and is an actual file.
+ bool exists = false;
+ rv = file->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists)
+ rv = file->IsFile(&exists);
+
+ if (NS_SUCCEEDED(rv) && exists) {
+ // After all that let's just get the URL spec and set the pref to it.
+ rv = fileHandler->GetURLSpecFromFile(file, fileURL);
+ if (NS_FAILED(rv))
+ return NS_OK;
+ rv = aBranch->SetCharPref(aTransform->sourcePrefName, fileURL);
+ if (NS_SUCCEEDED(rv) && aTransform->targetPrefName)
+ rv = aBranch->SetIntPref(aTransform->targetPrefName, 1);
+ }
+ }
+ return rv;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::SetImage(PrefTransform* aTransform,
+ nsIPrefBranch* aBranch) {
+ if (aTransform->prefHasValue)
+ // This transforms network.image.imageBehavior into
+ // permissions.default.image
+ return aBranch->SetIntPref("permissions.default.image",
+ aTransform->intValue == 1 ? 3 :
+ aTransform->intValue == 2 ? 2 : 1);
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// General Utility Methods
+
+nsresult
+nsSuiteProfileMigratorBase::GetSourceProfile(const char16_t* aProfile) {
+ uint32_t count;
+ mProfileNames->GetLength(&count);
+ for (uint32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISupportsString> str(do_QueryElementAt(mProfileNames, i));
+ nsString profileName;
+ str->GetData(profileName);
+ if (profileName.Equals(aProfile))
+ {
+ mSourceProfile = do_QueryElementAt(mProfileLocations, i);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::GetProfileDataFromProfilesIni(nsIFile* aDataDir,
+ nsIMutableArray* aProfileNames,
+ nsIMutableArray* aProfileLocations) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> profileIni;
+ rv = aDataDir->Clone(getter_AddRefs(profileIni));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ profileIni->Append(u"profiles.ini"_ns);
+
+ // Does it exist?
+ bool profileFileExists = false;
+ rv = profileIni->Exists(&profileFileExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!profileFileExists)
+ return NS_ERROR_FILE_NOT_FOUND;
+
+ nsINIParser parser;
+ rv = parser.Init(profileIni);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString buffer, filePath;
+ bool isRelative;
+
+ unsigned int c = 0;
+ for (c = 0; true; ++c) {
+ nsAutoCString profileID("Profile");
+ profileID.AppendInt(c);
+
+ rv = parser.GetString(profileID.get(), "IsRelative", buffer);
+ if (NS_FAILED(rv))
+ break;
+
+ isRelative = buffer.EqualsLiteral("1");
+
+ rv = parser.GetString(profileID.get(), "Path", filePath);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Malformed profiles.ini: Path= not found");
+ continue;
+ }
+
+ rv = parser.GetString(profileID.get(), "Name", buffer);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Malformed profiles.ini: Name= not found");
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> rootDir;
+ rv = NS_NewNativeLocalFile(EmptyCString(), true, getter_AddRefs(rootDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isRelative)
+ rv = rootDir->SetRelativeDescriptor(aDataDir, filePath);
+ else
+ rv = rootDir->SetPersistentDescriptor(filePath);
+
+ if (NS_FAILED(rv)) continue;
+
+ bool exists;
+ rootDir->Exists(&exists);
+
+ if (exists) {
+ aProfileLocations->AppendElement(rootDir);
+
+ nsCOMPtr<nsISupportsString> profileNameString(
+ do_CreateInstance("@mozilla.org/supports-string;1"));
+
+ profileNameString->SetData(NS_ConvertUTF8toUTF16(buffer));
+ aProfileNames->AppendElement(profileNameString);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::CopyFile(const char* aSourceFileName,
+ const char* aTargetFileName) {
+ nsCOMPtr<nsIFile> sourceFile;
+ mSourceProfile->Clone(getter_AddRefs(sourceFile));
+
+ sourceFile->AppendNative(nsDependentCString(aSourceFileName));
+ bool exists = false;
+ sourceFile->Exists(&exists);
+ if (!exists)
+ return NS_OK;
+
+ nsCOMPtr<nsIFile> targetFile;
+ mTargetProfile->Clone(getter_AddRefs(targetFile));
+
+ targetFile->AppendNative(nsDependentCString(aTargetFileName));
+ targetFile->Exists(&exists);
+ if (exists)
+ targetFile->Remove(false);
+
+ return sourceFile->CopyToNative(mTargetProfile,
+ nsDependentCString(aTargetFileName));
+}
+
+// helper function, copies the contents of srcDir into destDir.
+// destDir will be created if it doesn't exist.
+nsresult
+nsSuiteProfileMigratorBase::RecursiveCopy(nsIFile* srcDir,
+ nsIFile* destDir) {
+ bool exists;
+ nsresult rv = srcDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists)
+ // We do not want to fail if the source folder does not exist because then
+ // parts of the migration process following this would not get executed
+ return NS_OK;
+
+ bool isDir;
+
+ rv = srcDir->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!isDir)
+ return NS_ERROR_INVALID_ARG;
+
+ rv = destDir->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = destDir->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> dirIterator;
+ rv = srcDir->GetDirectoryEntries(getter_AddRefs(dirIterator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(dirIterator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIFile> dirEntry;
+ rv = dirIterator->GetNextFile(getter_AddRefs(dirEntry));
+ if (NS_SUCCEEDED(rv) && dirEntry) {
+ rv = dirEntry->IsDirectory(&isDir);
+ if (NS_SUCCEEDED(rv)) {
+ if (isDir) {
+ nsCOMPtr<nsIFile> newChild;
+ rv = destDir->Clone(getter_AddRefs(newChild));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString leafName;
+ dirEntry->GetLeafName(leafName);
+
+ newChild->AppendRelativePath(leafName);
+ rv = newChild->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = newChild->Create(nsIFile::DIRECTORY_TYPE, 0775);
+
+ rv = RecursiveCopy(dirEntry, newChild);
+ }
+ }
+ else {
+ // we aren't going to do any actual file copying here. Instead,
+ // add this to our file transaction list so we can copy files
+ // asynchronously...
+ fileTransactionEntry fileEntry;
+
+ fileEntry.srcFile = dirEntry;
+ fileEntry.destFile = destDir;
+
+ mFileCopyTransactions.AppendElement(fileEntry);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+void
+nsSuiteProfileMigratorBase::ReadBranch(const char * branchName,
+ nsIPrefService* aPrefService,
+ PBStructArray &aPrefs) {
+ // Enumerate the branch
+ nsCOMPtr<nsIPrefBranch> branch;
+ aPrefService->GetBranch(branchName, getter_AddRefs(branch));
+
+ nsTArray<nsCString> prefs;
+
+ nsresult rv = branch->GetChildList("", prefs);
+ if (NS_FAILED(rv))
+ return;
+
+ for (auto& pref : prefs) {
+ // Save each pref's value into an array
+ char* currPref = moz_xstrdup(pref.get());
+ int32_t type;
+ branch->GetPrefType(currPref, &type);
+
+ PrefBranchStruct* prefBranch = new PrefBranchStruct;
+ if (!prefBranch) {
+ NS_WARNING("Could not create new PrefBranchStruct");
+ free(currPref);
+ return;
+ }
+ prefBranch->prefName = currPref;
+ prefBranch->type = type;
+
+ switch (type) {
+ case nsIPrefBranch::PREF_STRING: {
+ nsCString str;
+ rv = branch->GetCharPref(currPref, str);
+ prefBranch->stringValue = moz_xstrdup(str.get());
+ break;
+ }
+ case nsIPrefBranch::PREF_BOOL:
+ rv = branch->GetBoolPref(currPref, &prefBranch->boolValue);
+ break;
+ case nsIPrefBranch::PREF_INT:
+ rv = branch->GetIntPref(currPref, &prefBranch->intValue);
+ break;
+ default:
+ NS_WARNING("Invalid prefBranch Type in "
+ "nsSuiteProfileMigratorBase::ReadBranch\n");
+ break;
+ }
+
+ if (NS_SUCCEEDED(rv))
+ aPrefs.AppendElement(prefBranch);
+ }
+}
+
+void
+nsSuiteProfileMigratorBase::WriteBranch(const char * branchName,
+ nsIPrefService* aPrefService,
+ PBStructArray &aPrefs) {
+ // Enumerate the branch
+ nsCOMPtr<nsIPrefBranch> branch;
+ aPrefService->GetBranch(branchName, getter_AddRefs(branch));
+
+ uint32_t count = aPrefs.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ PrefBranchStruct* pref = aPrefs.ElementAt(i);
+
+ switch (pref->type) {
+ case nsIPrefBranch::PREF_STRING: {
+ branch->SetCharPref(pref->prefName,
+ nsDependentCString(pref->stringValue));
+ free(pref->stringValue);
+ pref->stringValue = nullptr;
+ break;
+ }
+ case nsIPrefBranch::PREF_BOOL:
+ branch->SetBoolPref(pref->prefName, pref->boolValue);
+ break;
+ case nsIPrefBranch::PREF_INT:
+ branch->SetIntPref(pref->prefName, pref->intValue);
+ break;
+ default:
+ NS_WARNING("Invalid Pref Type in "
+ "nsSuiteProfileMigratorBase::WriteBranch\n");
+ break;
+ }
+ free(pref->prefName);
+ pref->prefName = nullptr;
+ delete pref;
+ pref = nullptr;
+ }
+ aPrefs.Clear();
+}
+
+nsresult
+nsSuiteProfileMigratorBase::GetFileValue(nsIPrefBranch* aPrefBranch,
+ const char* aRelPrefName,
+ const char* aPrefName,
+ nsIFile** aReturnFile) {
+ nsCString prefValue;
+ nsCOMPtr<nsIFile> theFile;
+ nsresult rv = aPrefBranch->GetCharPref(aRelPrefName, prefValue);
+ if (NS_SUCCEEDED(rv)) {
+ // The pref has the format: [ProfD]a/b/c
+ if (!StringBeginsWith(prefValue, "[ProfD]"_ns))
+ return NS_ERROR_FILE_NOT_FOUND;
+
+ rv = NS_NewNativeLocalFile(EmptyCString(), true, getter_AddRefs(theFile));
+ if (NS_FAILED(rv))
+ return rv;
+
+ rv = theFile->SetRelativeDescriptor(mSourceProfile, Substring(prefValue, 7));
+ if (NS_FAILED(rv))
+ return rv;
+ } else {
+ rv = aPrefBranch->GetComplexValue(aPrefName,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(theFile));
+ }
+
+ theFile.forget(aReturnFile);
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Mail Import Functions
+
+nsresult
+nsSuiteProfileMigratorBase::CopyAddressBookDirectories(PBStructArray &aLdapServers,
+ nsIPrefService* aPrefService) {
+ // each server has a pref ending with .filename. The value of that pref
+ // points to a profile which we need to migrate.
+ nsAutoString index;
+ index.AppendInt(nsISuiteProfileMigrator::ADDRESSBOOK_DATA);
+ NOTIFY_OBSERVERS(MIGRATION_ITEMBEFOREMIGRATE, index.get());
+
+ uint32_t count = aLdapServers.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ PrefBranchStruct* pref = aLdapServers.ElementAt(i);
+ nsDependentCString prefName(pref->prefName);
+
+ if (StringEndsWith(prefName, ".filename"_ns)) {
+ CopyFile(pref->stringValue, pref->stringValue);
+ }
+
+ // we don't need to do anything to the fileName pref itself
+ }
+
+ NOTIFY_OBSERVERS(MIGRATION_ITEMAFTERMIGRATE, index.get());
+
+ return NS_OK;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::CopySignatureFiles(PBStructArray &aIdentities,
+ nsIPrefService* aPrefService) {
+ nsresult rv = NS_OK;
+
+ uint32_t count = aIdentities.Length();
+ for (uint32_t i = 0; i < count; ++i)
+ {
+ PrefBranchStruct* pref = aIdentities.ElementAt(i);
+ nsDependentCString prefName(pref->prefName);
+
+ // a partial fix for bug #255043
+ // if the user's signature file from seamonkey lives in the
+ // old profile root, we'll copy it over to the new profile root and
+ // then set the pref to the new value. Note, this doesn't work for
+ // multiple signatures that live below the seamonkey profile root
+ if (StringEndsWith(prefName, ".sig_file"_ns))
+ {
+ // turn the pref into a nsIFile
+ nsCOMPtr<nsIFile> srcSigFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ rv = srcSigFile->SetPersistentDescriptor(nsDependentCString(pref->stringValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> targetSigFile;
+ rv = mTargetProfile->Clone(getter_AddRefs(targetSigFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now make the copy
+ bool exists;
+ srcSigFile->Exists(&exists);
+ if (exists)
+ {
+ nsAutoString leafName;
+ srcSigFile->GetLeafName(leafName);
+ // will fail if we've already copied a sig file here
+ srcSigFile->CopyTo(targetSigFile, leafName);
+ targetSigFile->Append(leafName);
+
+ // now write out the new descriptor
+ nsAutoCString descriptorString;
+ rv = targetSigFile->GetPersistentDescriptor(descriptorString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ free(pref->stringValue);
+ pref->stringValue = ToNewCString(descriptorString);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::CopyJunkTraining(bool aReplace)
+{
+ return aReplace ? CopyFile(FILE_NAME_JUNKTRAINING,
+ FILE_NAME_JUNKTRAINING) : NS_OK;
+}
+
+nsresult
+nsSuiteProfileMigratorBase::CopyMailFolderPrefs(PBStructArray &aMailServers,
+ nsIPrefService* aPrefService) {
+ // Each server has a .directory pref which points to the location of the
+ // mail data for that server. We need to do two things for that case...
+ // (1) Fix up the directory path for the new profile
+ // (2) copy the mail folder data from the source directory pref to the
+ // destination directory pref
+ CopyFile(FILE_NAME_VIRTUALFOLDERS, FILE_NAME_VIRTUALFOLDERS);
+
+ int32_t count = aMailServers.Length();
+ for (int32_t i = 0; i < count; ++i) {
+ PrefBranchStruct* pref = aMailServers.ElementAt(i);
+ nsDependentCString prefName(pref->prefName);
+
+ if (StringEndsWith(prefName, ".directory"_ns)) {
+ // let's try to get a branch for this particular server to simplify things
+ prefName.Cut(prefName.Length() - strlen("directory"),
+ strlen("directory"));
+ prefName.Insert("mail.server.", 0);
+
+ nsCOMPtr<nsIPrefBranch> serverBranch;
+ aPrefService->GetBranch(prefName.get(), getter_AddRefs(serverBranch));
+
+ if (!serverBranch)
+ break; // should we clear out this server pref from aMailServers?
+
+ nsCString serverType;
+ serverBranch->GetCharPref("type", serverType);
+
+ nsCOMPtr<nsIFile> sourceMailFolder;
+ nsresult rv = GetFileValue(serverBranch, "directory-rel", "directory",
+ getter_AddRefs(sourceMailFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now based on type, we need to build a new destination path for the
+ // mail folders for this server
+ nsCOMPtr<nsIFile> targetMailFolder;
+ if (serverType.Equals("imap")) {
+ mTargetProfile->Clone(getter_AddRefs(targetMailFolder));
+ targetMailFolder->Append(IMAP_MAIL_DIR_50_NAME);
+ }
+ else if (serverType.Equals("none") || serverType.Equals("pop3")) {
+ // local folders and POP3 servers go under <profile>\Mail
+ mTargetProfile->Clone(getter_AddRefs(targetMailFolder));
+ targetMailFolder->Append(MAIL_DIR_50_NAME);
+ }
+ else if (serverType.Equals("nntp")) {
+ mTargetProfile->Clone(getter_AddRefs(targetMailFolder));
+ targetMailFolder->Append(NEWS_DIR_50_NAME);
+ }
+
+ if (targetMailFolder) {
+ // for all of our server types, append the host name to the directory
+ // as part of the new location
+ nsCString hostName;
+ serverBranch->GetCharPref("hostname", hostName);
+ targetMailFolder->Append(NS_ConvertASCIItoUTF16(hostName));
+
+ // we should make sure the host name based directory we are going to
+ // migrate the accounts into is unique. This protects against the
+ // case where the user has multiple servers with the same host name.
+ rv = targetMailFolder->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RecursiveCopy(sourceMailFolder, targetMailFolder);
+ // now we want to make sure the actual directory pref that gets
+ // transformed into the new profile's pref.js has the right file
+ // location.
+ nsAutoCString descriptorString;
+ rv = targetMailFolder->GetPersistentDescriptor(descriptorString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ free(pref->stringValue);
+ pref->stringValue = ToNewCString(descriptorString);
+ }
+ }
+ else if (StringEndsWith(prefName, ".newsrc.file"_ns)) {
+ // copy the news RC file into \News. this won't work if the user has
+ // different newsrc files for each account I don't know what to do in
+ // that situation.
+ nsCOMPtr<nsIFile> targetNewsRCFile;
+ mTargetProfile->Clone(getter_AddRefs(targetNewsRCFile));
+ targetNewsRCFile->Append(NEWS_DIR_50_NAME);
+
+ // turn the pref into a nsIFile
+ nsCOMPtr<nsIFile> srcNewsRCFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ nsresult rv = srcNewsRCFile->SetPersistentDescriptor(
+ nsDependentCString(pref->stringValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now make the copy
+ bool exists;
+ srcNewsRCFile->Exists(&exists);
+ if (exists) {
+ nsAutoString leafName;
+ srcNewsRCFile->GetLeafName(leafName);
+ // will fail if we've already copied a newsrc file here
+ srcNewsRCFile->CopyTo(targetNewsRCFile,leafName);
+ targetNewsRCFile->Append(leafName);
+
+ // now write out the new descriptor
+ nsAutoCString descriptorString;
+ rv = targetNewsRCFile->GetPersistentDescriptor(descriptorString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ free(pref->stringValue);
+ pref->stringValue = ToNewCString(descriptorString);
+ }
+ }
+ }
+
+ // Remove all .directory-rel prefs as those might have changed; MailNews
+ // will create those prefs again on first use
+ for (int32_t i = count; i-- > 0; ) {
+ PrefBranchStruct* pref = aMailServers.ElementAt(i);
+ nsDependentCString prefName(pref->prefName);
+
+ if (StringEndsWith(prefName, ".directory-rel"_ns)) {
+ if (pref->type == nsIPrefBranch::PREF_STRING)
+ free(pref->stringValue);
+
+ aMailServers.RemoveElementAt(i);
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+nsSuiteProfileMigratorBase::CopyMailFolders() {
+ nsAutoString index;
+ index.AppendInt(nsISuiteProfileMigrator::MAILDATA);
+ NOTIFY_OBSERVERS(MIGRATION_ITEMBEFOREMIGRATE, index.get());
+
+ // Generate the max progress value now that we know all of the files we
+ // need to copy
+ uint32_t count = mFileCopyTransactions.Length();
+ mMaxProgress = 0;
+ mCurrentProgress = 0;
+
+ for (uint32_t i = 0; i < count; ++i) {
+ fileTransactionEntry fileTransaction = mFileCopyTransactions[i];
+ int64_t fileSize;
+ fileTransaction.srcFile->GetFileSize(&fileSize);
+ mMaxProgress += fileSize;
+ }
+
+ CopyNextFolder();
+}
+
+void
+nsSuiteProfileMigratorBase::CopyNextFolder() {
+ if (mFileCopyTransactionIndex < mFileCopyTransactions.Length()) {
+ fileTransactionEntry fileTransaction =
+ mFileCopyTransactions.ElementAt(mFileCopyTransactionIndex++);
+
+ // copy the file
+ fileTransaction.srcFile->CopyTo(fileTransaction.destFile,
+ EmptyString());
+
+ // add to our current progress
+ int64_t fileSize;
+ fileTransaction.srcFile->GetFileSize(&fileSize);
+ mCurrentProgress += fileSize;
+
+ uint32_t percentage = (uint32_t)(mCurrentProgress * 100 / mMaxProgress);
+
+ nsAutoString index;
+ index.AppendInt(percentage);
+
+ NOTIFY_OBSERVERS(MIGRATION_PROGRESS, index.get());
+
+ if (mFileCopyTransactionIndex == mFileCopyTransactions.Length())
+ {
+ EndCopyFolders();
+ return;
+ }
+
+ // fire a timer to handle the next one.
+ mFileIOTimer = do_CreateInstance("@mozilla.org/timer;1");
+
+ if (mFileIOTimer)
+ mFileIOTimer->InitWithCallback(static_cast<nsITimerCallback *>(this),
+ 1, nsITimer::TYPE_ONE_SHOT);
+ }
+ else
+ EndCopyFolders();
+
+ return;
+}
+
+void
+nsSuiteProfileMigratorBase::EndCopyFolders() {
+ mFileCopyTransactions.Clear();
+ mFileCopyTransactionIndex = 0;
+
+ // notify the UI that we are done with the migration process
+ nsAutoString index;
+ index.AppendInt(nsISuiteProfileMigrator::MAILDATA);
+ NOTIFY_OBSERVERS(MIGRATION_ITEMAFTERMIGRATE, index.get());
+
+ NOTIFY_OBSERVERS(MIGRATION_ENDED, nullptr);
+}
diff --git a/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.h b/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.h
new file mode 100644
index 0000000000..6a4d40e3df
--- /dev/null
+++ b/comm/suite/components/migration/src/nsSuiteProfileMigratorBase.h
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSuiteProfileMigratorBase___h___
+#define nsSuiteProfileMigratorBase___h___
+
+#include "nsAttrValue.h"
+#include "nsIFile.h"
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+#include "nsITimer.h"
+#include "nsIObserverService.h"
+#include "nsISuiteProfileMigrator.h"
+#include "nsSuiteMigrationCID.h"
+
+class nsIPrefBranch;
+class nsIPrefService;
+
+struct fileTransactionEntry {
+ nsCOMPtr<nsIFile> srcFile; // the src path including leaf name
+ nsCOMPtr<nsIFile> destFile; // the destination path
+ nsString newName; // only valid if the file should be renamed after
+ // getting copied
+};
+
+#define FILE_NAME_PREFS "prefs.js"
+#define FILE_NAME_JUNKTRAINING "training.dat"
+#define FILE_NAME_VIRTUALFOLDERS "virtualFolders.dat"
+
+#define F(a) nsSuiteProfileMigratorBase::a
+
+#define MAKEPREFTRANSFORM(pref, newpref, getmethod, setmethod) \
+ { pref, newpref, F(Get##getmethod), F(Set##setmethod), false, { -1 } }
+
+#define MAKESAMETYPEPREFTRANSFORM(pref, method) \
+ { pref, 0, F(Get##method), F(Set##method), false, { -1 } }
+
+class nsSuiteProfileMigratorBase : public nsISuiteProfileMigrator,
+ public nsITimerCallback,
+ public nsINamed
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ nsSuiteProfileMigratorBase();
+
+ struct PrefTransform;
+ typedef nsresult(*prefConverter)(PrefTransform*, nsIPrefBranch*);
+
+ struct PrefTransform {
+ const char* sourcePrefName;
+ const char* targetPrefName;
+ prefConverter prefGetterFunc;
+ prefConverter prefSetterFunc;
+ bool prefHasValue;
+ union {
+ int32_t intValue;
+ bool boolValue;
+ char* stringValue;
+ };
+ };
+
+ struct PrefBranchStruct {
+ char* prefName;
+ int32_t type;
+ union {
+ char* stringValue;
+ int32_t intValue;
+ bool boolValue;
+ };
+ };
+
+ typedef nsTArray<PrefBranchStruct*> PBStructArray;
+
+ // nsISuiteProfileMigrator methods
+ NS_IMETHOD GetSourceExists(bool* aSourceExists) override;
+ NS_IMETHOD GetSourceHasMultipleProfiles(bool* aSourceHasMultipleProfiles) override;
+ NS_IMETHOD GetSourceProfiles(nsIArray** aResult) override;
+
+ // Pref Transform Methods
+ static nsresult GetString(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetString(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult GetBool(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetBool(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult GetInt(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetInt(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetFile(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetImage(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+ static nsresult SetCookie(PrefTransform* aTransform, nsIPrefBranch* aBranch);
+
+protected:
+ virtual ~nsSuiteProfileMigratorBase() {}
+ // This function is designed to be overriden by derived classes so that
+ // the required profile data for the specific application can be obtained.
+ virtual nsresult FillProfileDataFromRegistry() = 0;
+
+ // General Utility Methods
+ nsresult GetSourceProfile(const char16_t* aProfile);
+ nsresult GetProfileDataFromProfilesIni(nsIFile* aDataDir,
+ nsIMutableArray* aProfileNames,
+ nsIMutableArray* aProfileLocations);
+ nsresult GetFileValue(nsIPrefBranch* aPrefBranch, const char* aRelPrefName,
+ const char* aPrefName, nsIFile** aReturnFile);
+ nsresult CopyFile(const char* aSourceFileName,
+ const char* aTargetFileName);
+ nsresult RecursiveCopy(nsIFile* srcDir, nsIFile* destDir);
+ void ReadBranch(const char * branchName, nsIPrefService* aPrefService,
+ PBStructArray &aPrefs);
+ void WriteBranch(const char * branchName, nsIPrefService* aPrefService,
+ PBStructArray &aPrefs);
+
+ // Mail Import Functions
+ nsresult CopyAddressBookDirectories(PBStructArray &aLdapServers,
+ nsIPrefService* aPrefService);
+ nsresult CopySignatureFiles(PBStructArray &aIdentities,
+ nsIPrefService* aPrefService);
+ nsresult CopyJunkTraining(bool aReplace);
+ nsresult CopyMailFolderPrefs(PBStructArray &aMailServers,
+ nsIPrefService* aPrefService);
+ void CopyMailFolders();
+ void CopyNextFolder();
+ void EndCopyFolders();
+
+ // Source & Target profiles
+ nsCOMPtr<nsIFile> mSourceProfile;
+ nsCOMPtr<nsIFile> mTargetProfile;
+
+ // list of src/destination files we still have to copy into the new profile
+ // directory
+ nsTArray<fileTransactionEntry> mFileCopyTransactions;
+ uint32_t mFileCopyTransactionIndex;
+
+ nsCOMPtr<nsIObserverService> mObserverService;
+ int64_t mMaxProgress;
+ int64_t mCurrentProgress;
+
+ nsCOMPtr<nsIMutableArray> mProfileNames;
+ nsCOMPtr<nsIMutableArray> mProfileLocations;
+
+ nsCOMPtr<nsITimer> mFileIOTimer;
+};
+
+#endif
diff --git a/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.cpp b/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.cpp
new file mode 100644
index 0000000000..091a96540d
--- /dev/null
+++ b/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSuiteProfileMigratorUtils.h"
+#include "nsIPrefBranch.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsILineInputStream.h"
+#include "nsIProfileMigrator.h"
+
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsIProperties.h"
+#include "nsServiceManagerUtils.h"
+#include "nsISupportsPrimitives.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIStringBundle.h"
+#include "nsCRT.h"
+
+#define MIGRATION_BUNDLE "chrome://communicator/migration/locale/migration.properties"
+
+using namespace mozilla;
+
+void GetMigrateDataFromArray(MigrationData* aDataArray,
+ int32_t aDataArrayLength,
+ bool aReplace, nsIFile* aSourceProfile,
+ uint16_t* aResult)
+{
+ nsCOMPtr<nsIFile> sourceFile;
+ bool exists;
+ MigrationData* cursor;
+ MigrationData* end = aDataArray + aDataArrayLength;
+ for (cursor = aDataArray; cursor < end; ++cursor) {
+ // When in replace mode, all items can be imported.
+ // When in non-replace mode, only items that do not require file
+ // replacement can be imported.
+ if (aReplace || !cursor->replaceOnly) {
+ aSourceProfile->Clone(getter_AddRefs(sourceFile));
+ sourceFile->AppendNative(nsDependentCString(cursor->fileName));
+ sourceFile->Exists(&exists);
+ if (exists)
+ *aResult |= cursor->sourceFlag;
+ }
+ }
+}
+
+void
+GetProfilePath(nsIProfileStartup* aStartup, nsIFile** aProfileDir)
+{
+ *aProfileDir = nullptr;
+
+ if (aStartup) {
+ aStartup->GetDirectory(aProfileDir);
+ }
+ else {
+ nsCOMPtr<nsIProperties> dirSvc
+ (do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
+ if (dirSvc) {
+ dirSvc->Get(NS_APP_USER_PROFILE_50_DIR, NS_GET_IID(nsIFile),
+ (void**)aProfileDir);
+ }
+ }
+}
diff --git a/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.h b/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.h
new file mode 100644
index 0000000000..e469f5723d
--- /dev/null
+++ b/comm/suite/components/migration/src/nsSuiteProfileMigratorUtils.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SuiteProfileMigratorUtils_h__
+#define SuiteProfileMigratorUtils_h__
+
+#define MIGRATION_ITEMBEFOREMIGRATE "Migration:ItemBeforeMigrate"
+#define MIGRATION_ITEMAFTERMIGRATE "Migration:ItemAfterMigrate"
+#define MIGRATION_STARTED "Migration:Started"
+#define MIGRATION_ENDED "Migration:Ended"
+#define MIGRATION_PROGRESS "Migration:Progress"
+
+#define NOTIFY_OBSERVERS(message, item) \
+ mObserverService->NotifyObservers(nullptr, message, item)
+
+#define COPY_DATA(func, replace, itemIndex) \
+ if (NS_SUCCEEDED(rv) && (aItems & itemIndex || !aItems)) { \
+ nsAutoString index; \
+ index.AppendInt(itemIndex); \
+ NOTIFY_OBSERVERS(MIGRATION_ITEMBEFOREMIGRATE, index.get()); \
+ rv = func(replace); \
+ NOTIFY_OBSERVERS(MIGRATION_ITEMAFTERMIGRATE, index.get()); \
+ }
+
+#define MAKEPREFTRANSFORM(pref, newpref, getmethod, setmethod) \
+ { pref, newpref, F(Get##getmethod), F(Set##setmethod), false, { -1 } }
+
+#define MAKESAMETYPEPREFTRANSFORM(pref, method) \
+ { pref, 0, F(Get##method), F(Set##method), false, { -1 } }
+
+#include "nsString.h"
+#include "nscore.h"
+#include "nsCOMPtr.h"
+
+class nsIPrefBranch;
+class nsIProfileStartup;
+class nsIFile;
+
+struct MigrationData {
+ const char* fileName;
+ uint32_t sourceFlag;
+ bool replaceOnly;
+};
+
+class nsIFile;
+void GetMigrateDataFromArray(MigrationData* aDataArray,
+ int32_t aDataArrayLength,
+ bool aReplace,
+ nsIFile* aSourceProfile,
+ uint16_t* aResult);
+
+
+// get the base directory of the *target* profile
+// this is already cloned, modify it to your heart's content
+void GetProfilePath(nsIProfileStartup* aStartup,
+ nsIFile** aProfileDir);
+
+#endif
diff --git a/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.cpp b/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.cpp
new file mode 100644
index 0000000000..3f5365f12a
--- /dev/null
+++ b/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.cpp
@@ -0,0 +1,571 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSuiteProfileMigratorUtils.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsCRT.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIObserverService.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIPrefService.h"
+#include "nsIServiceManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsIProperties.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThunderbirdProfileMigrator.h"
+#include "prprf.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// nsThunderbirdProfileMigrator
+
+#define FILE_NAME_SITEPERM_OLD "cookperm.txt"
+#define FILE_NAME_SITEPERM_NEW "hostperm.1"
+#define FILE_NAME_CERT8DB "cert8.db"
+#define FILE_NAME_KEY3DB "key3.db"
+#define FILE_NAME_SECMODDB "secmod.db"
+#define FILE_NAME_HISTORY "history.dat"
+#define FILE_NAME_SIGNONS "signons.sqlite"
+#define FILE_NAME_MIMETYPES "mimeTypes.rdf"
+#define FILE_NAME_USER_PREFS "user.js"
+#define FILE_NAME_PERSONALDICTIONARY "persdict.dat"
+#define FILE_NAME_MAILVIEWS "mailViews.dat"
+
+NS_IMPL_ISUPPORTS(nsThunderbirdProfileMigrator, nsISuiteProfileMigrator,
+ nsITimerCallback)
+
+nsThunderbirdProfileMigrator::nsThunderbirdProfileMigrator()
+{
+}
+
+nsThunderbirdProfileMigrator::~nsThunderbirdProfileMigrator()
+{
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsISuiteProfileMigrator
+
+NS_IMETHODIMP
+nsThunderbirdProfileMigrator::Migrate(uint16_t aItems,
+ nsIProfileStartup* aStartup,
+ const char16_t* aProfile)
+{
+ nsresult rv = NS_OK;
+ bool aReplace = aStartup ? true : false;
+
+ if (!mTargetProfile) {
+ GetProfilePath(aStartup, getter_AddRefs(mTargetProfile));
+ if (!mTargetProfile)
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ if (!mSourceProfile) {
+ GetSourceProfile(aProfile);
+ if (!mSourceProfile)
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ NOTIFY_OBSERVERS(MIGRATION_STARTED, nullptr);
+
+ COPY_DATA(CopyPreferences, aReplace, nsISuiteProfileMigrator::SETTINGS);
+ COPY_DATA(CopyHistory, aReplace, nsISuiteProfileMigrator::HISTORY);
+ COPY_DATA(CopyPasswords, aReplace, nsISuiteProfileMigrator::PASSWORDS);
+
+ // fake notifications for things we've already imported as part of
+ // CopyPreferences
+ nsAutoString index;
+ index.AppendInt(nsISuiteProfileMigrator::ACCOUNT_SETTINGS);
+ NOTIFY_OBSERVERS(MIGRATION_ITEMBEFOREMIGRATE, index.get());
+ NOTIFY_OBSERVERS(MIGRATION_ITEMAFTERMIGRATE, index.get());
+
+ index.Truncate();
+ index.AppendInt(nsISuiteProfileMigrator::NEWSDATA);
+ NOTIFY_OBSERVERS(MIGRATION_ITEMBEFOREMIGRATE, index.get());
+ NOTIFY_OBSERVERS(MIGRATION_ITEMAFTERMIGRATE, index.get());
+
+ // copy junk mail training file
+ COPY_DATA(CopyJunkTraining, aReplace, nsISuiteProfileMigrator::JUNKTRAINING);
+
+ if (aReplace &&
+ (aItems & nsISuiteProfileMigrator::SETTINGS ||
+ aItems & nsISuiteProfileMigrator::PASSWORDS ||
+ !aItems)) {
+ // Permissions (Images)
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_SITEPERM_NEW, FILE_NAME_SITEPERM_NEW);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_SITEPERM_OLD, FILE_NAME_SITEPERM_OLD);
+ }
+
+ // the last thing to do is to actually copy over any mail folders
+ // we have marked for copying we want to do this last and it will be
+ // asynchronous so the UI doesn't freeze up while we perform
+ // this potentially very long operation.
+ CopyMailFolders();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsThunderbirdProfileMigrator::GetMigrateData(const char16_t* aProfile,
+ bool aReplace,
+ uint16_t* aResult)
+{
+ *aResult = 0;
+
+ if (!mSourceProfile) {
+ GetSourceProfile(aProfile);
+ if (!mSourceProfile)
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ // migration fields for things we always migrate
+ *aResult =
+ nsISuiteProfileMigrator::ACCOUNT_SETTINGS |
+ nsISuiteProfileMigrator::MAILDATA |
+ nsISuiteProfileMigrator::NEWSDATA |
+ nsISuiteProfileMigrator::ADDRESSBOOK_DATA;
+
+ MigrationData data[] = { { FILE_NAME_PREFS,
+ nsISuiteProfileMigrator::SETTINGS,
+ true },
+ { FILE_NAME_USER_PREFS,
+ nsISuiteProfileMigrator::SETTINGS,
+ true },
+ { FILE_NAME_HISTORY,
+ nsISuiteProfileMigrator::HISTORY,
+ true },
+ { FILE_NAME_SIGNONS,
+ nsISuiteProfileMigrator::PASSWORDS,
+ true },
+ { FILE_NAME_JUNKTRAINING,
+ nsISuiteProfileMigrator::JUNKTRAINING,
+ true } };
+
+ GetMigrateDataFromArray(data, sizeof(data)/sizeof(MigrationData),
+ aReplace, mSourceProfile, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsThunderbirdProfileMigrator::GetSupportedItems(uint16_t *aSupportedItems)
+{
+ NS_ENSURE_ARG_POINTER(aSupportedItems);
+
+ *aSupportedItems = nsISuiteProfileMigrator::SETTINGS |
+ nsISuiteProfileMigrator::HISTORY |
+ nsISuiteProfileMigrator::JUNKTRAINING |
+ nsISuiteProfileMigrator::PASSWORDS |
+ nsISuiteProfileMigrator::ACCOUNT_SETTINGS |
+ nsISuiteProfileMigrator::MAILDATA |
+ nsISuiteProfileMigrator::NEWSDATA |
+ nsISuiteProfileMigrator::ADDRESSBOOK_DATA;
+
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsThunderbirdProfileMigrator
+
+nsresult
+nsThunderbirdProfileMigrator::FillProfileDataFromRegistry()
+{
+ // Find the Thunderbird Registry
+ nsCOMPtr<nsIProperties> fileLocator(
+ do_GetService("@mozilla.org/file/directory_service;1"));
+ nsCOMPtr<nsIFile> thunderbirdData;
+#ifdef XP_WIN
+ fileLocator->Get(NS_WIN_APPDATA_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(thunderbirdData));
+
+ thunderbirdData->Append(u"Thunderbird"_ns);
+
+#elif defined(XP_MACOSX)
+ fileLocator->Get(NS_MAC_USER_LIB_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(thunderbirdData));
+
+ thunderbirdData->Append(u"Thunderbird"_ns);
+
+#elif defined(XP_UNIX)
+ fileLocator->Get(NS_UNIX_HOME_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(thunderbirdData));
+
+ thunderbirdData->Append(u".thunderbird"_ns);
+
+#else
+ // On other OS just abort
+ return NS_ERROR_FILE_NOT_FOUND;
+#endif
+
+ // Try profiles.ini first
+ return GetProfileDataFromProfilesIni(thunderbirdData,
+ mProfileNames,
+ mProfileLocations);
+}
+
+static
+nsThunderbirdProfileMigrator::PrefTransform gTransforms[] = {
+ MAKESAMETYPEPREFTRANSFORM("accessibility.typeaheadfind.autostart", Bool),
+ MAKESAMETYPEPREFTRANSFORM("accessibility.typeaheadfind.linksonly", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("browser.anchor_color", String),
+ MAKESAMETYPEPREFTRANSFORM("browser.active_color", String),
+ MAKESAMETYPEPREFTRANSFORM("browser.display.background_color", String),
+ MAKESAMETYPEPREFTRANSFORM("browser.display.foreground_color", String),
+ MAKESAMETYPEPREFTRANSFORM("browser.display.use_system_colors", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.display.document_color_use", Int),
+ MAKESAMETYPEPREFTRANSFORM("browser.display.use_document_fonts", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.enable_automatic_image_resizing", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.tabs.autoHide", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.tabs.loadInBackground", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.underline_anchors", Bool),
+ MAKESAMETYPEPREFTRANSFORM("browser.visited_color", String),
+
+ MAKESAMETYPEPREFTRANSFORM("dom.disable_open_during_load", Bool),
+ MAKESAMETYPEPREFTRANSFORM("dom.disable_window_move_resize", Bool),
+ MAKESAMETYPEPREFTRANSFORM("dom.disable_window_flip", Bool),
+ MAKESAMETYPEPREFTRANSFORM("dom.disable_window_open_feature.status", Bool),
+ MAKESAMETYPEPREFTRANSFORM("dom.disable_window_status_change", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("extensions.spellcheck.inline.max-misspellings",Int),
+
+ MAKESAMETYPEPREFTRANSFORM("general.warnOnAboutConfig", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("intl.accept_charsets", String),
+ MAKESAMETYPEPREFTRANSFORM("intl.accept_languages", String),
+ MAKESAMETYPEPREFTRANSFORM("intl.charset.fallback.override", String),
+
+ MAKESAMETYPEPREFTRANSFORM("javascript.enabled", Bool),
+ MAKESAMETYPEPREFTRANSFORM("javascript.options.relimit", Bool),
+ MAKESAMETYPEPREFTRANSFORM("javascript.options.showInConsole", Bool),
+ MAKESAMETYPEPREFTRANSFORM("javascript.options.strict", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("layout.spellcheckDefault", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.accountmanager.accounts", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.accountmanager.defaultaccount", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.accountmanager.localfoldersserver", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.accountwizard.deferstorage", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.adaptivefilters.junk_threshold", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.autoComplete.highlightNonMatches", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.animate_doc_icon", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.play_sound", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.play_sound.type", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.play_sound.url", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.show_alert", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.biff.show_tray_icon", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.check_all_imap_folders_for_new", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.citation_color", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.collect_addressbook", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.collect_email_address_incoming", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.collect_email_address_newsgroup", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.collect_email_address_outgoing", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.compose.add_undisclosed_recipients", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.compose.autosave", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.compose.autosaveinterval", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.compose.dontWarnMail2Newsgroup", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.compose.other.header", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.content_disposition_type", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.default_html_action", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.default_sendlater_uri", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.delete_matches_sort_order", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.display_glyph", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.display_struct", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.enable_autocomplete", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.fcc_folder", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.file_attach_binary", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.fixed_width_messages", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.forward_message_mode", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.incorporate.return_receipt", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.inline_attachments", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.label_ascii_only_mail_as_us_ascii", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.notification.sound", String),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.pane_config.dynamic", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.password_protect_local_cache", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.phishing.detection.enabled", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.pop3.deleteFromServerOnMove", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.prompt_purge_threshhold", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.purge_threshhold", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.purge.ask", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.purge.min_delay", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.purge.timer_interval", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.quoteasblock", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.quoted_graphical", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.quoted_size", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.quoted_style", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.receipt.request_header_type", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.receipt.request_return_receipt_on", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.send_struct", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.show_headers", Int),
+ MAKESAMETYPEPREFTRANSFORM("mail.smtp.useMatchingDomainServer", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.smtp.useMatchingHostNameServer", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.smtp.defaultserver", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.smtpservers", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.spellcheck.inline", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.SpellCheckBeforeSend", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.startup.enabledMailCheckOnce", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.strict_threading", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.strictly_mime", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.strictly_mime_headers", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.strictly_mime.parm_folding", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mail.thread_without_re", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.trusteddomains", String),
+ MAKESAMETYPEPREFTRANSFORM("mail.warn_on_send_accel_key", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.warn_filter_changed", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mail.wrap_long_lines", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.account_central_page.url", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.confirm.moveFoldersToTrash", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.customDBHeaders", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.customHeaders", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.default_sort_order", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.default_sort_type", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.default_news_sort_order", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.default_news_sort_type", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display.disable_format_flowed_support", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display.disallow_mime_handlers", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display.html_as", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display_html_sanitzer.allowed_tags", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display.original_date", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.display.prefer_plaintext", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.force_ascii_search", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.force_charset_override", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.headers.showOrganization", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.headers.showUserAgent", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.headers.extraExpandedHeaders", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.html_domains", String),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.mark_message_read.delay", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.mark_message_read.delay.interval", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.message_display.disable_remote_image", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.nav_crosses_folders", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("mailnews.offline_sync_mail", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.offline_sych_news", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.offline_sync_send_unsent", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.offline_sync_work_offline", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.open_window_warning", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.plaintext_domains", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.remember_selected_message", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.reply_in_default_charset", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.reuse_message_window", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.scroll_to_new_message", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.search_date_format", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.search_date_leading_zeros", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.search_date_separator", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.send_default_charset", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.send_plaintext_flowed", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.show_send_progress", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.start_page.enabled", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.start_page.url", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.tcptimeout", Int),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.thread_pane_column_unthreads", Bool),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.view_default_charset", String),
+ MAKESAMETYPEPREFTRANSFORM("mailnews.wraplength", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("messenger.save.dir", String),
+
+ MAKESAMETYPEPREFTRANSFORM("msgcompose.background_color", String),
+ MAKESAMETYPEPREFTRANSFORM("msgcompose.font_face", String),
+ MAKESAMETYPEPREFTRANSFORM("msgcompose.font_size", String),
+ MAKESAMETYPEPREFTRANSFORM("msgcompose.text_color", String),
+
+ MAKESAMETYPEPREFTRANSFORM("news.get_messages_on_select", Bool),
+ MAKESAMETYPEPREFTRANSFORM("news.show_size_in_lines", Bool),
+ MAKESAMETYPEPREFTRANSFORM("news.update_unread_on_expand", Bool),
+
+ // pdi is the new preference, but nii is the old one - so do nii first, and
+ // then do pdi to account for both situations
+ MAKEPREFTRANSFORM("network.image.imageBehavior", 0, Int, Image),
+ MAKESAMETYPEPREFTRANSFORM("permissions.default.image", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.autoconfig_url", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.ftp", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.ftp_port", Int),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.http", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.http_port", Int),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.no_proxies_on", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.socks", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.socks_port", Int),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.ssl", String),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.ssl_port", Int),
+ MAKESAMETYPEPREFTRANSFORM("network.proxy.type", Int),
+
+ MAKESAMETYPEPREFTRANSFORM("offline.autodetect", Bool),
+ MAKESAMETYPEPREFTRANSFORM("offline.download.download_messages", Int),
+ MAKESAMETYPEPREFTRANSFORM("offline.send.unsent_messages", Int),
+ MAKESAMETYPEPREFTRANSFORM("offline.startup_state", Int),
+ MAKESAMETYPEPREFTRANSFORM("security.default_personal_cert", String),
+ MAKESAMETYPEPREFTRANSFORM("security.password_lifetime", Int),
+ MAKESAMETYPEPREFTRANSFORM("security.tls.version.min", Int),
+ MAKESAMETYPEPREFTRANSFORM("security.tls.version.max", Int),
+ MAKESAMETYPEPREFTRANSFORM("security.warn_entering_secure", Bool),
+ MAKESAMETYPEPREFTRANSFORM("security.warn_leaving_secure", Bool),
+ MAKESAMETYPEPREFTRANSFORM("security.warn_submit_insecure", Bool),
+ MAKESAMETYPEPREFTRANSFORM("security.warn_viewing_mixed", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("signon.rememberSignons", Bool),
+
+ MAKESAMETYPEPREFTRANSFORM("slider.snapMultiplier", Int),
+ MAKESAMETYPEPREFTRANSFORM("startup.homepage_override_url", String),
+
+#ifdef XP_UNIX
+ MAKESAMETYPEPREFTRANSFORM("ui.allow_platform_file_picker", Bool),
+#endif
+ MAKESAMETYPEPREFTRANSFORM("ui.click_hold_context_menus", Bool)
+};
+
+nsresult
+nsThunderbirdProfileMigrator::TransformPreferences(
+ const char* aSourcePrefFileName,
+ const char* aTargetPrefFileName)
+{
+ PrefTransform* transform;
+ PrefTransform* end = gTransforms + sizeof(gTransforms)/sizeof(PrefTransform);
+
+ // Load the source pref file
+ nsCOMPtr<nsIPrefService> psvc(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ psvc->ResetPrefs();
+
+ nsCOMPtr<nsIFile> sourcePrefsFile;
+ mSourceProfile->Clone(getter_AddRefs(sourcePrefsFile));
+ sourcePrefsFile->AppendNative(nsDependentCString(aSourcePrefFileName));
+ psvc->ReadUserPrefsFromFile(sourcePrefsFile);
+
+ nsCOMPtr<nsIPrefBranch> branch(do_QueryInterface(psvc));
+ for (transform = gTransforms; transform < end; ++transform)
+ transform->prefGetterFunc(transform, branch);
+
+ // read in the various pref branch trees for accounts, identities, servers,
+ // etc.
+ static const char* branchNames[] =
+ {
+ // Keep the three below first, or change the indexes below
+ "mail.identity.",
+ "mail.server.",
+ "ldap_2.",
+ "accessibility.",
+ "applications.",
+ "bidi.",
+ "dom.",
+ "editor.",
+ "font.",
+ "helpers.",
+ "mail.account.",
+ "mail.addr_book.",
+ "mail.imap.",
+ "mail.mdn.",
+ "mail.smtpserver.",
+ "mail.spam.",
+ "mail.toolbars.",
+ "mailnews.labels.",
+ "mailnews.reply_",
+ "mailnews.tags.",
+ "middlemouse.",
+ "mousewheel.",
+ "network.http.",
+ "print.",
+ "privacy.",
+ "security.OSCP.",
+ "security.crl.",
+ "ui.key.",
+ "wallet."
+ };
+
+ PBStructArray branches[MOZ_ARRAY_LENGTH(branchNames)];
+ uint32_t i;
+ for (i = 0; i < MOZ_ARRAY_LENGTH(branchNames); ++i)
+ ReadBranch(branchNames[i], psvc, branches[i]);
+
+ // the signature file prefs may be paths to files in the thunderbird profile
+ // path so we need to copy them over and fix these paths up before we write
+ // them out to the new prefs.js
+ CopySignatureFiles(branches[0], psvc);
+
+ // certain mail prefs may actually be absolute paths instead of profile
+ // relative paths we need to fix these paths up before we write them out to
+ // the new prefs.js
+ CopyMailFolderPrefs(branches[1], psvc);
+
+ CopyAddressBookDirectories(branches[2], psvc);
+
+ // Now that we have all the pref data in memory, load the target pref file,
+ // and write it back out
+ psvc->ResetPrefs();
+
+ nsCOMPtr<nsIFile> targetPrefsFile;
+ mTargetProfile->Clone(getter_AddRefs(targetPrefsFile));
+ targetPrefsFile->AppendNative(nsDependentCString(aTargetPrefFileName));
+
+ // Don't use nullptr here as we're too early in the cycle for the prefs
+ // service to get its default file (because the NS_GetDirectoryService items
+ // aren't fully set up yet).
+ psvc->ReadUserPrefsFromFile(targetPrefsFile);
+
+ for (transform = gTransforms; transform < end; ++transform)
+ transform->prefSetterFunc(transform, branch);
+
+ for (i = 0; i < MOZ_ARRAY_LENGTH(branchNames); ++i)
+ WriteBranch(branchNames[i], psvc, branches[i]);
+
+ psvc->SavePrefFile(targetPrefsFile);
+
+ return NS_OK;
+}
+
+nsresult
+nsThunderbirdProfileMigrator::CopyPreferences(bool aReplace)
+{
+ nsresult rv = NS_OK;
+ if (!aReplace)
+ return rv;
+
+ if (NS_SUCCEEDED(rv))
+ rv = TransformPreferences(FILE_NAME_PREFS, FILE_NAME_PREFS);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_USER_PREFS, FILE_NAME_USER_PREFS);
+
+ // Security Stuff
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_CERT8DB, FILE_NAME_CERT8DB);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_KEY3DB, FILE_NAME_KEY3DB);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_SECMODDB, FILE_NAME_SECMODDB);
+
+ // User MIME Type overrides
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_MIMETYPES, FILE_NAME_MIMETYPES);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_PERSONALDICTIONARY, FILE_NAME_PERSONALDICTIONARY);
+ if (NS_SUCCEEDED(rv))
+ rv = CopyFile(FILE_NAME_MAILVIEWS, FILE_NAME_MAILVIEWS);
+
+ return rv;
+}
+
+nsresult
+nsThunderbirdProfileMigrator::CopyHistory(bool aReplace)
+{
+ return aReplace ? CopyFile(FILE_NAME_HISTORY, FILE_NAME_HISTORY) : NS_OK;
+}
+
+nsresult
+nsThunderbirdProfileMigrator::CopyPasswords(bool aReplace)
+{
+ return aReplace ? CopyFile(FILE_NAME_SIGNONS, FILE_NAME_SIGNONS) : NS_OK;
+}
diff --git a/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.h b/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.h
new file mode 100644
index 0000000000..2b3bbf87ec
--- /dev/null
+++ b/comm/suite/components/migration/src/nsThunderbirdProfileMigrator.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ThunderbirdProfileMigrator_h__
+#define ThunderbirdProfileMigrator_h__
+
+#include "nsISuiteProfileMigrator.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsSuiteProfileMigratorBase.h"
+#include "nsITimer.h"
+#include "mozilla/Attributes.h"
+
+class nsIFile;
+class nsIPrefBranch;
+class nsIPrefService;
+
+class nsThunderbirdProfileMigrator final : public nsSuiteProfileMigratorBase
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsThunderbirdProfileMigrator();
+
+ // nsISuiteProfileMigrator methods
+ NS_IMETHOD Migrate(uint16_t aItems, nsIProfileStartup *aStartup,
+ const char16_t *aProfile) override;
+ NS_IMETHOD GetMigrateData(const char16_t *aProfile, bool aDoingStartup,
+ uint16_t *_retval) override;
+ NS_IMETHOD GetSupportedItems(uint16_t *aSupportedItems) override;
+
+protected:
+ virtual ~nsThunderbirdProfileMigrator();
+ nsresult FillProfileDataFromRegistry() override;
+ nsresult CopyPreferences(bool aReplace);
+ nsresult TransformPreferences(const char* aSourcePrefFileName,
+ const char* aTargetPrefFileName);
+ nsresult CopyHistory(bool aReplace);
+ nsresult CopyPasswords(bool aReplace);
+};
+
+#endif
diff --git a/comm/suite/components/moz.build b/comm/suite/components/moz.build
new file mode 100644
index 0000000000..2e3b677e92
--- /dev/null
+++ b/comm/suite/components/moz.build
@@ -0,0 +1,52 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "autocomplete",
+ "bindings",
+ "console",
+ "dataman",
+ "downloads",
+ "feeds",
+ "helpviewer",
+ "migration",
+ "permissions",
+ "places",
+ "pref",
+ "profile",
+ "sanitize",
+ "search",
+ "security",
+ "sessionstore",
+ "shell",
+ "sidebar",
+]
+
+# build is always last as it adds the local includes from the other components.
+DIRS += [
+ "build",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser/browser.ini",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "tests/chrome/chrome.ini",
+]
+
+XPIDL_SOURCES += [
+ "nsISuiteGlue.idl",
+]
+
+XPIDL_MODULE = "suite-components"
+
+EXTRA_COMPONENTS += [
+ "nsAbout.js",
+ "nsGopherProtocolStubHandler.js",
+ "nsSuiteGlue.js",
+ "SuiteComponents.manifest",
+]
diff --git a/comm/suite/components/nsAbout.js b/comm/suite/components/nsAbout.js
new file mode 100644
index 0000000000..eb93ce0dda
--- /dev/null
+++ b/comm/suite/components/nsAbout.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const SCRIPT = Ci.nsIAboutModule.ALLOW_SCRIPT;
+const UNTRUSTED = Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT;
+const HIDE = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+const INDEXEDDB = Ci.nsIAboutModule.ENABLE_INDEXED_DB;
+
+function About() { }
+About.prototype = {
+ Flags: SCRIPT,
+ URI: "chrome://communicator/content/about.xhtml",
+ blockedFlags: SCRIPT | UNTRUSTED | HIDE,
+ blockedURI: "chrome://communicator/content/blockedSite.xhtml",
+ certerrorFlags: SCRIPT | UNTRUSTED | HIDE,
+ certerrorURI: "chrome://communicator/content/certError.xhtml",
+ dataFlags: SCRIPT,
+ dataURI: "chrome://communicator/content/dataman/dataman.xul",
+ feedsFlags: SCRIPT | UNTRUSTED | HIDE,
+ feedsURI: "chrome://communicator/content/feeds/subscribe.xhtml",
+ lifeFlags: SCRIPT | UNTRUSTED | HIDE,
+ lifeURI: "chrome://communicator/content/aboutLife.xhtml",
+ newserrorFlags: SCRIPT | HIDE,
+ newserrorURI: "chrome://messenger/content/newsError.xhtml",
+ privatebrowsingFlags: SCRIPT,
+ privatebrowsingURI: "chrome://communicator/content/aboutPrivateBrowsing.xul",
+ rightsFlags: SCRIPT | UNTRUSTED,
+ rightsURI: "chrome://branding/content/aboutRights.xhtml",
+ sessionrestoreFlags: SCRIPT | HIDE,
+ sessionrestoreURI: "chrome://communicator/content/aboutSessionRestore.xhtml",
+ // synctabsFlags: SCRIPT,
+ // synctabsURI: "chrome://communicator/content/aboutSyncTabs.xul",
+
+ classID: Components.ID("{d54f2c89-8fd6-4eeb-a7a4-51d4dcdf460f}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ getModule: function(aURI) {
+ return aURI.pathQueryRef.replace(/-|\W.*$/g, "").toLowerCase();
+ },
+
+ getURIFlags: function(aURI) {
+ return this[this.getModule(aURI) + "Flags"];
+ },
+
+ newChannel: function(aURI, aLoadInfo) {
+ let module = this.getModule(aURI);
+ let newURI = Services.io.newURI(this[module + "URI"]);
+
+ // We want a happy family which is always providing a loadInfo object.
+ if (!aLoadInfo) {
+ // Write out an error so that we have a stack and can fix the caller.
+ Cu.reportError('aLoadInfo was not provided in nsAbout.newChannel!');
+ }
+
+ let channel = aLoadInfo ?
+ Services.io.newChannelFromURIWithLoadInfo(newURI, aLoadInfo) :
+ Services.io.newChannelFromURI(newURI, null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ channel.originalURI = aURI;
+ if (this[module + "Flags"] & UNTRUSTED) {
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(aURI, {});
+ channel.owner = principal;
+ }
+ return channel;
+ },
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([About]);
diff --git a/comm/suite/components/nsGopherProtocolStubHandler.js b/comm/suite/components/nsGopherProtocolStubHandler.js
new file mode 100644
index 0000000000..5f4451d9e9
--- /dev/null
+++ b/comm/suite/components/nsGopherProtocolStubHandler.js
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/* This is a simple module which can be used as a template for any newly
+ unsupported protocol. In this case, it redirects gopher:// protocol
+ requests to the Mozilla Add-Ons page for OverbiteFF, which is a
+ cross-platform extension for Gopherspace. This gives a soft-landing for
+ support, which was withdrawn in Mozilla 2.0. See bugs 388195 and 572000. */
+
+function GopherProtocol()
+{
+}
+
+GopherProtocol.prototype = {
+ classDescription: "Gopher protocol handler stub",
+ classID: Components.ID("{22042bdb-56e4-47c6-8b12-fdfa859c05a9}"),
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
+
+ // nsIProtocolHandler
+ scheme: "gopher",
+ defaultPort: 70,
+ protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_NOAUTH |
+ Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE,
+
+ allowPort: function GP_allowPort(port, scheme) {
+ return false; // meaningless.
+ },
+
+ newURI: function GP_newURI(spec, charset, baseURI) {
+ var uri = Cc["@mozilla.org/network/standard-url;1"]
+ .createInstance(Ci.nsIStandardURL);
+ uri.init(Ci.nsIStandardURL.URLTYPE_STANDARD,
+ this.defaultPort, spec, charset, baseURI)
+ return uri;
+ },
+
+ newChannel: function GP_newChannel(inputURI) {
+ return this.newChannel2(inputURI, null);
+ },
+
+ newChannel2: function GP_newChannel2(inputURI, loadinfo) {
+ var newURI = Services.io.newURI("chrome://communicator/content/gopherAddon.xhtml");
+ // Create a chrome channel, and de-chrome it, to our information page.
+ var chan =
+ loadinfo ? Services.io.newChannelFromURIWithLoadInfo(newURI, loadinfo) :
+ Services.io.newChannelFromURI(newURI, null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ chan.originalURI = inputURI;
+ chan.owner = Services.scriptSecurityManager.createCodebasePrincipal(inputURI, {});
+ return chan;
+ }
+};
+
+/* Make our factory. */
+var components = [ GopherProtocol ];
+var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/comm/suite/components/nsISuiteGlue.idl b/comm/suite/components/nsISuiteGlue.idl
new file mode 100644
index 0000000000..d2cc03aea2
--- /dev/null
+++ b/comm/suite/components/nsISuiteGlue.idl
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+
+/**
+ * ***** this is the suite version of nsIBrowserGlue *****
+ * nsISuiteGlue is a dirty and rather fluid interface to host shared utility
+ * methods used by suite UI code, but which are not local to a suite window.
+ * The component implementing this interface is meant to be a singleton
+ * (service) and should progressively replace some of the shared "glue" code
+ * scattered in suite/ (e.g. bits of utilOverlay.js,
+ * contentAreaUtils.js, globalOverlay.js), avoiding dynamic
+ * inclusion and initialization of a ton of JS code for *each* window.
+ * Due to its nature and origin, this interface won't probably be the most
+ * elegant or stable in the mozilla codebase, but its aim is rather pragmatic:
+ * 1) reducing the performance overhead which affects browser window load;
+ * 2) allow global hooks (e.g. startup and shutdown observers) which survive
+ * suite windows to accomplish suite-related activities, such as shutdown
+ * sanitization (see bug #284086)
+ */
+
+[scriptable, uuid(b3a787fd-4c05-4518-98e3-20bc10a0f586)]
+interface nsISuiteGlue : nsISupports
+{
+ /**
+ * Opens the Download Manager.
+ */
+ void showDownloadManager( [optional] in boolean newDownload );
+
+ /**
+ * Deletes privacy sensitive data according to user preferences
+ *
+ * @param aParentWindow an optionally null window which is the parent of the
+ * sanitization dialog (if it has to be shown per user preferences)
+ *
+ */
+ void sanitize(in nsIDOMWindow aParentWindow);
+};
diff --git a/comm/suite/components/nsSuiteGlue.js b/comm/suite/components/nsSuiteGlue.js
new file mode 100644
index 0000000000..2d0b5600f4
--- /dev/null
+++ b/comm/suite/components/nsSuiteGlue.js
@@ -0,0 +1,1676 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var { migrateMailnews } =
+ ChromeUtils.import("resource:///modules/mailnewsMigrator.js");
+var { ExtensionSupport } =
+ ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { LightweightThemeConsumer } =
+ ChromeUtils.import("resource://gre/modules/LightweightThemeConsumer.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ FileUtils: "resource://gre/modules/FileUtils.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PlacesBackups: "resource://gre/modules/PlacesBackups.jsm",
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
+ AutoCompletePopup: "resource://gre/modules/AutoCompletePopup.jsm",
+ DateTimePickerHelper: "resource://gre/modules/DateTimePickerHelper.jsm",
+ BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.jsm",
+ BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
+ RecentWindow: "resource:///modules/RecentWindow.jsm",
+ Sanitizer: "resource:///modules/Sanitizer.jsm",
+ ShellService: "resource:///modules/ShellService.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ Integration: "resource://gre/modules/Integration.jsm",
+ PermissionUI: "resource:///modules/PermissionUI.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
+ var tmp = {};
+ ChromeUtils.import("resource://devtools/shared/Loader.jsm", tmp);
+ return tmp.require("devtools/server/main").DebuggerServer;
+});
+
+var global = this;
+
+var listeners = {
+ mm: {
+ // PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN nsBrowserGlue.js
+ "RemoteLogins:findLogins": ["LoginManagerParent"],
+ "RemoteLogins:findRecipes": ["LoginManagerParent"],
+ "RemoteLogins:onFormSubmit": ["LoginManagerParent"],
+ "RemoteLogins:autoCompleteLogins": ["LoginManagerParent"],
+ "RemoteLogins:removeLogin": ["LoginManagerParent"],
+ "RemoteLogins:insecureLoginFormPresent": ["LoginManagerParent"],
+ // PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN nsBrowserGlue.js
+ },
+
+ receiveMessage(modules, data) {
+ let val;
+ for (let module of modules[data.name]) {
+ try {
+ val = global[module].receiveMessage(data) || val;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ return val;
+ },
+
+ init() {
+ let receiveMessageMM = this.receiveMessage.bind(this, this.mm);
+ for (let message of Object.keys(this.mm)) {
+ Services.mm.addMessageListener(message, receiveMessageMM);
+ }
+ }
+};
+
+// We try to backup bookmarks at idle times, to avoid doing that at shutdown.
+// Number of idle seconds before trying to backup bookmarks 8 minutes.
+const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 15 * 60;
+// Minimum interval between backups. We try to not create more than one backup
+// per interval.
+const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
+
+// Devtools Preferences
+const DEBUGGER_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
+const DEBUGGER_REMOTE_PORT = "devtools.debugger.remote-port";
+const DEBUGGER_FORCE_LOCAL = "devtools.debugger.force-local";
+const DEBUGGER_WIFI_VISIBLE = "devtools.remote.wifi.visible";
+const DOWNLOAD_MANAGER_URL = "chrome://communicator/content/downloads/downloadmanager.xul";
+const PREF_FOCUS_WHEN_STARTING = "browser.download.manager.focusWhenStarting";
+const PREF_FLASH_COUNT = "browser.download.manager.flashCount";
+
+var gDownloadManager;
+
+// Constructor
+function SuiteGlue() {
+ XPCOMUtils.defineLazyServiceGetter(this, "_idleService",
+ "@mozilla.org/widget/idleservice;1",
+ "nsIIdleService");
+
+ this._init();
+ extensionDefaults(); // extensionSupport.jsm
+}
+
+SuiteGlue.prototype = {
+ _saveSession: false,
+ _isIdleObserver: false,
+ _isPlacesDatabaseLocked: false,
+ _migrationImportsDefaultBookmarks: false,
+
+ _setPrefToSaveSession: function()
+ {
+ Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
+ },
+
+ _logConsoleAPI: function(aEvent)
+ {
+ const nsIScriptError = Ci.nsIScriptError;
+ var flg = nsIScriptError.errorFlag;
+ switch (aEvent.level) {
+ case "warn":
+ flg = nsIScriptError.warningFlag;
+ case "error":
+ var scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(nsIScriptError);
+ scriptError.initWithWindowID(Array.from(aEvent.arguments),
+ aEvent.filename, "", aEvent.lineNumber, 0,
+ flg, "content javascript", aEvent.innerID);
+ Services.console.logMessage(scriptError);
+ break;
+ case "log":
+ case "info":
+ Services.console.logStringMessage(Array.from(aEvent.arguments));
+ break;
+ }
+ },
+
+ _setSyncAutoconnectDelay: function BG__setSyncAutoconnectDelay() {
+ // Assume that a non-zero value for services.sync.autoconnectDelay should override
+ if (Services.prefs.prefHasUserValue("services.sync.autoconnectDelay")) {
+ let prefDelay = Services.prefs.getIntPref("services.sync.autoconnectDelay");
+
+ if (prefDelay > 0)
+ return;
+ }
+
+ // delays are in seconds
+ const MAX_DELAY = 300;
+ let delay = 3;
+ let browserEnum = Services.wm.getEnumerator("navigator:browser");
+ while (browserEnum.hasMoreElements()) {
+ delay += browserEnum.getNext().gBrowser.tabs.length;
+ }
+ delay = delay <= MAX_DELAY ? delay : MAX_DELAY;
+
+ const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+ Weave.Service.scheduler.delayedAutoConnect(delay);
+ },
+
+ // nsIObserver implementation
+ observe: function(subject, topic, data)
+ {
+ switch(topic) {
+ case "nsPref:changed":
+ switch (data) {
+ case DEBUGGER_REMOTE_ENABLED:
+ if (this.dbgIsEnabled)
+ this.dbgStart();
+ else
+ this.dbgStop();
+ break;
+ case DEBUGGER_REMOTE_PORT:
+ case DEBUGGER_FORCE_LOCAL:
+ /**
+ * If the server is not on, port changes have nothing to affect.
+ * The new value will be picked up if the server is started.
+ */
+ if (this.dbgIsEnabled)
+ this.dbgRestart();
+ break;
+ case DEBUGGER_WIFI_VISIBLE:
+ // Wifi visibility has changed, we need to restart the debugger
+ // server.
+ if (this.dbgIsEnabled && !Services.prefs.getBoolPref(DEBUGGER_FORCE_LOCAL))
+ this.dbgRestart();
+ break;
+ }
+ break;
+ case "profile-before-change":
+ // Any component depending on Places should be finalized in
+ // _onPlacesShutdown. Any component that doesn't need to act after
+ // the UI has gone should be finalized in _onQuitApplicationGranted.
+ this._dispose();
+ break;
+ case "profile-after-change":
+ this._onProfileAfterChange();
+ break;
+ case "chrome-document-global-created":
+ // Set up lwt, but only if the "lightweightthemes" attr is set on the root
+ // (i.e. in messenger.xul).
+ subject.addEventListener("DOMContentLoaded", () => {
+ if (subject.document.documentElement.hasAttribute("lightweightthemes")) {
+ new LightweightThemeConsumer(subject.document);
+ }
+ }, {once: true});
+ break;
+ case "final-ui-startup":
+ this._onProfileStartup();
+ this._promptForMasterPassword();
+ this._checkForNewAddons();
+ Services.search.init();
+ listeners.init();
+
+ Services.mm.loadFrameScript("chrome://navigator/content/content.js",
+ true);
+ ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
+ break;
+ case "browser-delayed-startup-finished":
+ // Intended fallthrough.
+ case "mail-startup-done":
+ Services.obs.removeObserver(this, "browser-delayed-startup-finished");
+ Services.obs.removeObserver(this, "mail-startup-done");
+ this._onFirstWindowLoaded(subject);
+ break;
+ case "sessionstore-windows-restored":
+ this._onBrowserStartup(subject);
+ break;
+ case "browser:purge-session-history":
+ // reset the console service's error buffer
+ Services.console.logStringMessage(null); // clear the console (in case it's open)
+ Services.console.reset();
+ break;
+ case "quit-application-requested":
+ this._onQuitRequest(subject, data);
+ break;
+ case "quit-application-granted":
+ this._onQuitApplicationGranted();
+ break;
+ case "browser-lastwindow-close-requested":
+ // The application is not actually quitting, but the last full browser
+ // window is about to be closed.
+ this._onQuitRequest(subject, "lastwindow");
+ break;
+ case "browser-lastwindow-close-granted":
+ if (this._saveSession)
+ this._setPrefToSaveSession();
+ break;
+ case "console-api-log-event":
+ if (Services.prefs.getBoolPref("browser.dom.window.console.enabled"))
+ this._logConsoleAPI(subject.wrappedJSObject);
+ break;
+// case "weave:service:ready":
+// this._setSyncAutoconnectDelay();
+// break;
+// case "weave:engine:clients:display-uri":
+// this._onDisplaySyncURI(subject);
+// break;
+ case "session-save":
+ this._setPrefToSaveSession();
+ subject.QueryInterface(Ci.nsISupportsPRBool);
+ subject.data = true;
+ break;
+ case "places-init-complete":
+ if (!this._migrationImportsDefaultBookmarks)
+ this._initPlaces(false);
+
+ Services.obs.removeObserver(this, "places-init-complete");
+ break;
+ case "idle":
+ this._backupBookmarks();
+ break;
+ case "initial-migration":
+ this._initialMigrationPerformed = true;
+ break;
+ case "browser-search-engine-modified":
+ break;
+ case "notifications-open-settings":
+ // Since this is a web notification, there's probably a browser window.
+ var mostRecentBrowserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ if (mostRecentBrowserWindow)
+ mostRecentBrowserWindow.toDataManager("|permissions");
+ break;
+ case "timer-callback":
+ // Load the Login Manager data from disk off the main thread, some time
+ // after startup. If the data is required before the timeout, for example
+ // because a restored page contains a password field, it will be loaded on
+ // the main thread, and this initialization request will be ignored.
+ Services.logins;
+ break;
+ case "handle-xul-text-link":
+ let linkHandled = subject.QueryInterface(Ci.nsISupportsPRBool);
+ if (!linkHandled.data) {
+ let mostRecentBrowserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ if (mostRecentBrowserWindow) {
+ let dataObj = JSON.parse(data);
+ let where = mostRecentBrowserWindow.whereToOpenLink(dataObj, false, true, true);
+ // Preserve legacy behavior of non-modifier left-clicks
+ // opening in a new selected tab.
+ if (where == "current") {
+ where = "tabfocused";
+ }
+ mostRecentBrowserWindow.openUILinkIn(dataObj.href, where);
+ linkHandled.data = true;
+ }
+ }
+ break;
+ }
+ },
+
+ // nsIWebProgressListener partial implementation
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ if (aWebProgress.isTopLevel &&
+ aWebProgress instanceof Ci.nsIDocShell &&
+ aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL &&
+ aWebProgress.useGlobalHistory &&
+ aWebProgress instanceof Ci.nsILoadContext &&
+ !aWebProgress.usePrivateBrowsing) {
+ switch (aLocation.scheme) {
+ case "about":
+ case "imap":
+ case "news":
+ case "mailbox":
+ case "moz-anno":
+ case "view-source":
+ case "chrome":
+ case "resource":
+ case "data":
+ case "wyciwyg":
+ case "javascript":
+ break;
+ default:
+ Services.prefs.setStringPref("browser.history.last_page_visited",
+ aLocation.spec);
+ break;
+ }
+ }
+ },
+
+ // initialization (called on application startup)
+ _init: function()
+ {
+ // observer registration
+ Services.obs.addObserver(this, "profile-before-change", true);
+ Services.obs.addObserver(this, "profile-after-change", true);
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "browser-delayed-startup-finished", true);
+ Services.obs.addObserver(this, "mail-startup-done", true);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+ Services.obs.addObserver(this, "browser:purge-session-history", true);
+ Services.obs.addObserver(this, "quit-application-requested", true);
+ Services.obs.addObserver(this, "quit-application-granted", true);
+ Services.obs.addObserver(this, "browser-lastwindow-close-requested", true);
+ Services.obs.addObserver(this, "browser-lastwindow-close-granted", true);
+ Services.obs.addObserver(this, "console-api-log-event", true);
+ Services.obs.addObserver(this, "weave:service:ready", true);
+ Services.obs.addObserver(this, "weave:engine:clients:display-uri", true);
+ Services.obs.addObserver(this, "session-save", true);
+ Services.obs.addObserver(this, "places-init-complete", true);
+ Services.obs.addObserver(this, "browser-search-engine-modified", true);
+ Services.obs.addObserver(this, "notifications-open-settings", true);
+ Services.obs.addObserver(this, "chrome-document-global-created", true);
+ Services.prefs.addObserver("devtools.debugger.", this, true);
+ Services.obs.addObserver(this, "handle-xul-text-link", true);
+ Cc['@mozilla.org/docloaderservice;1']
+ .getService(Ci.nsIWebProgress)
+ .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ },
+
+ // cleanup (called on application shutdown)
+ _dispose: function BG__dispose() {
+ try {
+ Services.obs.removeObserver(this, "chrome-document-global-created");
+ }
+ catch (ex) {}
+ if (this._isIdleObserver) {
+ this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME_SEC);
+ delete this._isIdleObserver;
+ }
+ },
+
+ // profile is available
+ _onProfileAfterChange: function()
+ {
+ // check if we're in safe mode
+ if (Services.appinfo.inSafeMode) {
+ Services.ww.openWindow(null, "chrome://communicator/content/safeMode.xul",
+ "_blank", "chrome,centerscreen,modal,resizable=no", null);
+ }
+ this._copyDefaultProfileFiles();
+ },
+
+ // profile startup handler (contains profile initialization routines)
+ _onProfileStartup: function()
+ {
+ this._migrateUI();
+ this._migrateUI2();
+ migrateMailnews(); // mailnewsMigrator.js
+
+ Sanitizer.onStartup();
+
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ timer.init(this, 3000, timer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Determine if the UI has been upgraded for this release. If not
+ * reset or migrate some user configurations depending on the migration
+ * level.
+ */
+ _migrateUI() {
+ const UI_VERSION = 10;
+
+ // If the pref is not set this is a new or pre SeaMonkey 2.49 profile.
+ // We can't tell so we just run migration with version 0.
+ let currentUIVersion =
+ Services.prefs.getIntPref("suite.migration.version", 0);
+
+ if (currentUIVersion >= UI_VERSION)
+ return;
+
+ if (currentUIVersion < 1) {
+ // Run any migrations due prior to 2.49.
+ this._updatePrefs();
+ this._migrateDownloadPrefs();
+
+ // Migrate remote content exceptions for email addresses which are
+ // encoded as chrome URIs.
+ let permissionsDB =
+ Services.dirsvc.get("ProfD", Ci.nsIFile);
+ permissionsDB.append("permissions.sqlite");
+ let db = Services.storage.openDatabase(permissionsDB);
+
+ try {
+ let statement = db.createStatement(
+ "select origin, permission from moz_perms where " +
+ // Avoid 'like' here which needs to be escaped.
+ " substr(origin, 1, 28) = 'chrome://messenger/content/?';");
+
+ try {
+ while (statement.executeStep()) {
+ let origin = statement.getUTF8String(0);
+ let permission = statement.getInt32(1);
+ Services.console.logStringMessage("Mail-Image-Perm Mig: " + origin);
+ Services.perms.remove(
+ Services.io.newURI(origin), "image");
+ origin = origin.replace("chrome://messenger/content/?",
+ "chrome://messenger/content/");
+ Services.perms.add(
+ Services.io.newURI(origin), "image", permission);
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ // Sadly we still need to clear the database manually. Experiments
+ // showed that the permissions manager deletes only one record.
+ db.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE;
+ db.beginTransaction();
+
+ try {
+ db.executeSimpleSQL("delete from moz_perms where " +
+ " substr(origin, 1, 28) = 'chrome://messenger/content/?';");
+ db.commitTransaction();
+ } catch (ex) {
+ db.rollbackTransaction();
+ throw ex;
+ }
+ } finally {
+ db.close();
+ }
+ }
+
+ // Migration of disabled safebrowsing-phishing setting after pref renaming.
+ if (currentUIVersion < 2) {
+ try {
+ if (!Services.prefs.getBoolPref("browser.safebrowsing.enabled")) {
+ Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", false);
+ Services.prefs.clearUserPref("browser.safebrowsing.enabled");
+ }
+ } catch (ex) {}
+ }
+
+ // Pretend currentUIVersion 3 never happened (used in 2.57 for a time).
+
+ // Remove obsolete download preferences set by user.
+ if (currentUIVersion < 4) {
+ try {
+ if (Services.prefs.prefHasUserValue("browser.download.manager.showAlertOnComplete")) {
+ Services.prefs.clearUserPref("browser.download.manager.showAlertOnComplete");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.showAlertInterval")) {
+ Services.prefs.clearUserPref("browser.download.manager.showAlertInterval");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.retention")) {
+ Services.prefs.clearUserPref("browser.download.manager.retention");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.quitBehavior")) {
+ Services.prefs.clearUserPref("browser.download.manager.quitBehavior");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.scanWhenDone")) {
+ Services.prefs.clearUserPref("browser.download.manager.scanWhenDone");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.showWhenStarting")) {
+ Services.prefs.clearUserPref("browser.download.manager.showWhenStarting");
+ }
+ if (Services.prefs.prefHasUserValue("browser.download.manager.closeWhenDone")) {
+ Services.prefs.clearUserPref("browser.download.manager.closeWhenDone");
+ }
+ } catch (ex) {}
+ }
+
+ if (currentUIVersion < 5) {
+ // Delete obsolete ssl and strict transport security permissions.
+ let perms = Services.perms.enumerator;
+ while (perms.hasMoreElements()) {
+ let perm = perms.getNext();
+ if (perm.type == "falsestart-rc4" ||
+ perm.type == "falsestart-rsa" ||
+ perm.type == "sts/use" ||
+ perm.type == "sts/subd") {
+ Services.perms.removePermission(perm);
+ }
+ }
+ }
+
+ // Pretend currentUIVersion 6 and 7 never happened (used in 2.57 for a
+ // time).
+
+ // Migrate sanitizer options.
+ if (currentUIVersion < 8) {
+ const prefs = [ "history", "urlbar", "formdata", "passwords",
+ "downloads", "cookies", "cache", "sessions",
+ "offlineApps" ];
+
+ for (let pref of prefs) {
+ try {
+ let prefOld = "privacy.item." + pref;
+
+ // Migrate user value otherwise use default.
+ // Only the names have changed but not the default values.
+ if (Services.prefs.prefHasUserValue(prefOld)) {
+ let prefCpd = "privacy.cpd." + pref;
+ let prefShutdown = "privacy.clearOnShutdown." + pref;
+
+ // If it has a value this should never fail.
+ let oldValue = Services.prefs.getBoolPref(prefOld);
+ Services.prefs.setBoolPref(prefCpd, oldValue);
+ Services.prefs.setBoolPref(prefShutdown, oldValue);
+ Services.prefs.clearUserPref(prefOld);
+ }
+ } catch (ex) {
+ // Better safe than sorry.
+ Cu.reportError(ex);
+ }
+ }
+
+ // We might bring this back later but currently set to default.
+ Services.prefs.clearUserPref("privacy.sanitize.promptOnSanitize");
+
+ // As a precaution set to default if the user has enabled
+ // clearing data on shutdown because there will no longer be
+ // a possible prompt.
+ Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown");
+ }
+
+ // Migrate mail tab options.
+ if (currentUIVersion < 9) {
+ const tabPrefs = [ "autoHide", "opentabfor.doubleclick",
+ "opentabfor.middleclick" ];
+ for (let pref of tabPrefs) {
+ try {
+ let prefBT = "browser.tabs." + pref;
+
+ // Copy user value otherwise use default.
+ if (Services.prefs.prefHasUserValue(prefBT)) {
+ let prefMT = "mail.tabs." + pref;
+
+ // If it has a value this should never fail.
+ let valueBT = Services.prefs.getBoolPref(prefBT);
+ Services.prefs.setBoolPref(prefMT, valueBT);
+ }
+ } catch (ex) {
+ // Better safe than sorry.
+ Cu.reportError(ex);
+ }
+ }
+
+ // We might bring this back later but currently set to default.
+ Services.prefs.clearUserPref("browser.tabs.opentabfor.doubleclick");
+ }
+
+ // Migrate the old requested locales prefs to use the new model
+ if (currentUIVersion < 10) {
+ const SELECTED_LOCALE_PREF = "general.useragent.locale";
+ const MATCHOS_LOCALE_PREF = "intl.locale.matchOS";
+
+ if (Services.prefs.prefHasUserValue(MATCHOS_LOCALE_PREF) ||
+ Services.prefs.prefHasUserValue(SELECTED_LOCALE_PREF)) {
+ if (Services.prefs.getBoolPref(MATCHOS_LOCALE_PREF, false)) {
+ Services.locale.setRequestedLocales([]);
+ } else {
+ let locale = Services.prefs.getComplexValue(SELECTED_LOCALE_PREF,
+ Ci.nsIPrefLocalizedString);
+ if (locale) {
+ try {
+ Services.locale.setRequestedLocales([locale.data]);
+ } catch (e) {
+ /* Don't panic if the value is not a valid locale code. */
+ }
+ }
+ }
+ Services.prefs.clearUserPref(SELECTED_LOCALE_PREF);
+ Services.prefs.clearUserPref(MATCHOS_LOCALE_PREF);
+ }
+ }
+
+ // Update the migration version.
+ Services.prefs.setIntPref("suite.migration.version", UI_VERSION);
+ },
+
+ /**
+ * Determine if the UI has been upgraded for this 2.57 or later release.
+ * If not reset or migrate some user configurations depending on the
+ * migration level.
+ * Only migration steps for 2.57 and higher are included in this function.
+ * When the 2.53 branch is retired this function can be merged with
+ * _migrateUI again.
+ */
+ _migrateUI2() {
+ const UI_VERSION2 = 1;
+
+ // If the pref is not set this is a new or pre SeaMonkey 2.57 profile.
+ // We can't tell so we just run migration with version 0.
+ let currentUIVersion2 =
+ Services.prefs.getIntPref("suite.migration.version2", 0);
+
+ if (currentUIVersion2 >= UI_VERSION2)
+ return;
+
+ // Run any migrations due prior to 2.57.
+ if (currentUIVersion2 < 1) {
+ // The XUL directory viewer is no longer provided.
+ try {
+ if (Services.prefs.getIntPref("network.dir.format") == 3) {
+ Services.prefs.setIntPref("network.dir.format", 2);
+ }
+ } catch (ex) {}
+ }
+
+ // Update the migration version.
+ Services.prefs.setIntPref("suite.migration.version2", UI_VERSION2);
+ },
+
+ // Copies additional profile files from the default profile tho the current profile.
+ // Only files not covered by the regular profile creation process.
+ // Currently only the userchrome examples.
+ _copyDefaultProfileFiles: function()
+ {
+ // Copy default chrome example files if they do not exist in the current profile.
+ var profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ profileDir.append("chrome");
+
+ // The chrome directory in the current/new profile already exists so no copying.
+ if (profileDir.exists())
+ return;
+
+ let defaultProfileDir = Services.dirsvc.get("DefRt",
+ Ci.nsIFile);
+ defaultProfileDir.append("profile");
+ defaultProfileDir.append("chrome");
+
+ if (defaultProfileDir.exists() && defaultProfileDir.isDirectory()) {
+ try {
+ this._copyDir(defaultProfileDir, profileDir);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ // Simple copy function for copying complete aSource Directory to aDestiniation.
+ _copyDir: function(aSource, aDestination)
+ {
+ let enumerator = aSource.directoryEntries;
+
+ while (enumerator.hasMoreElements()) {
+ let file = enumerator.nextFile;
+
+ if (file.isDirectory()) {
+ let subdir = aDestination.clone();
+ subdir.append(file.leafName);
+
+ // Create the target directory. If it already exists continue copying files.
+ try {
+ subdir.create(Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS)
+ throw ex;
+ }
+ // Directory created. Now copy the files.
+ this._copyDir(file, subdir);
+ } else {
+ try {
+ file.copyTo(aDestination, null);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ },
+
+ // Browser startup complete. All initial windows have opened.
+ _onBrowserStartup: function(aWindow) {
+ // For any add-ons that were installed disabled and can be enabled offer
+ // them to the user.
+ var browser = aWindow.getBrowser();
+ var changedIDs = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED);
+ if (changedIDs.length) {
+ AddonManager.getAddonsByIDs(changedIDs, function(aAddons) {
+ aAddons.forEach(function(aAddon) {
+ // If the add-on isn't user disabled or can't be enabled then skip it.
+ if (!aAddon.userDisabled || !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE))
+ return;
+
+ browser.selectedTab = browser.addTab("about:newaddon?id=" + aAddon.id);
+ })
+ });
+ }
+
+ var notifyBox = browser.getNotificationBox();
+
+ // Show about:rights notification, if needed.
+ if (this._shouldShowRights())
+ this._showRightsNotification(notifyBox);
+
+ // Load the "more info" page for a locked places.sqlite
+ // This property is set earlier by places-database-locked topic.
+ if (this._isPlacesDatabaseLocked) {
+ notifyBox.showPlacesLockedWarning();
+ }
+
+ // Detect if updates are off and warn for outdated builds.
+ if (this._shouldShowUpdateWarning())
+ notifyBox.showUpdateWarning();
+
+ this._checkForDefaultClient(aWindow);
+ },
+
+ // First mail or browser window loaded.
+ _onFirstWindowLoaded: function(aWindow) {
+ AutoCompletePopup.init();
+ DateTimePickerHelper.init();
+
+ if ("@mozilla.org/windows-taskbar;1" in Cc &&
+ Cc["@mozilla.org/windows-taskbar;1"]
+ .getService(Ci.nsIWinTaskbar).available) {
+ let temp = {};
+ ChromeUtils.import("resource:///modules/WindowsJumpLists.jsm", temp);
+ temp.WinTaskbarJumpList.startup();
+ }
+
+ // Initialize the download manager after the app starts so that
+ // auto-resume downloads begin (such as after crashing or quitting with
+ // active downloads) and speeds up the first-load of the download manager.
+ // If the user manually opens the download manager before the init is
+ // done, the downloads will start right away, and initializing again
+ // won't hurt.
+ // Afterwards init the taskbar and eventuall show the download progress if
+ // on a supported platform.
+ (async () => {
+ DownloadsCommon.init();
+ })().catch(ex => {
+ Cu.reportError(ex);
+ }).then(() => {
+ ChromeUtils.import("resource:///modules/DownloadsTaskbar.jsm", {})
+ .DownloadsTaskbar.registerIndicator(aWindow);
+ });
+ },
+
+ /**
+ * Application shutdown handler.
+ */
+ _onQuitApplicationGranted: function()
+ {
+ if (this._saveSession) {
+ this._setPrefToSaveSession();
+ }
+ AutoCompletePopup.uninit();
+ DateTimePickerHelper.uninit();
+ },
+
+ _promptForMasterPassword: function()
+ {
+ if (!Services.prefs.getBoolPref("signon.startup.prompt"))
+ return;
+
+ // Try to avoid the multiple master password prompts on startup scenario
+ // by prompting for the master password upfront.
+ let token = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB)
+ .getInternalKeyToken();
+
+ // Only log in to the internal token if it is already initialized,
+ // otherwise we get a "Change Master Password" dialog.
+ try {
+ if (!token.needsUserInit)
+ token.login(false);
+ } catch (ex) {
+ // If user cancels an exception is expected.
+ }
+ },
+
+ // If new add-ons were installed during startup, open the add-ons manager.
+ _checkForNewAddons: function()
+ {
+ const PREF_EM_NEW_ADDONS_LIST = "extensions.newAddons";
+
+ if (!Services.prefs.prefHasUserValue(PREF_EM_NEW_ADDONS_LIST))
+ return;
+
+ const args = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ args.appendElement(str);
+ str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = Services.prefs.getCharPref(PREF_EM_NEW_ADDONS_LIST);
+ args.appendElement(str);
+ const EMURL = "chrome://mozapps/content/extensions/extensions.xul";
+ // This window is the "first" to open.
+ // 'alwaysRaised' makes sure it stays in the foreground (though unfocused)
+ // so it is noticed.
+ const EMFEATURES = "all,dialog=no,alwaysRaised";
+ Services.ww.openWindow(null, EMURL, "_blank", EMFEATURES, args);
+
+ Services.prefs.clearUserPref(PREF_EM_NEW_ADDONS_LIST);
+ },
+
+ _onQuitRequest: function(aCancelQuit, aQuitType)
+ {
+ // If user has already dismissed quit request, then do nothing
+ if ((aCancelQuit instanceof Ci.nsISupportsPRBool) && aCancelQuit.data)
+ return;
+
+ var windowcount = 0;
+ var pagecount = 0;
+ var browserEnum = Services.wm.getEnumerator("navigator:browser");
+ while (browserEnum.hasMoreElements()) {
+ // XXXbz should we skip closed windows here?
+ windowcount++;
+
+ var browser = browserEnum.getNext();
+ var tabbrowser = browser.document.getElementById("content");
+ if (tabbrowser)
+ pagecount += tabbrowser.browsers.length;
+ }
+
+ this._saveSession = false;
+ if (pagecount < 2)
+ return;
+
+ if (aQuitType != "restart" && aQuitType != "lastwindow")
+ aQuitType = "quit";
+
+ var showPrompt = true;
+ try {
+ // browser.warnOnQuit is a hidden global boolean to override all quit prompts
+ // browser.warnOnRestart specifically covers app-initiated restarts where we restart the app
+ // browser.tabs.warnOnClose is the global "warn when closing multiple tabs" pref
+ if (Services.prefs.getIntPref("browser.startup.page") == 3 ||
+ Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
+ !Services.prefs.getBoolPref("browser.warnOnQuit"))
+ showPrompt = false;
+ else if (aQuitType == "restart")
+ showPrompt = Services.prefs.getBoolPref("browser.warnOnRestart");
+ else
+ showPrompt = Services.prefs.getBoolPref("browser.tabs.warnOnClose");
+ } catch (ex) {}
+
+ if (showPrompt) {
+ var quitBundle = Services.strings.createBundle("chrome://communicator/locale/quitDialog.properties");
+ var brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+
+ var appName = brandBundle.GetStringFromName("brandShortName");
+ var quitDialogTitle = quitBundle.formatStringFromName(aQuitType + "DialogTitle",
+ [appName], 1);
+
+ var message;
+ if (aQuitType == "restart")
+ message = quitBundle.formatStringFromName("messageRestart",
+ [appName], 1);
+ else if (windowcount == 1) /* close browser only, or quit application with only 1 browser window */
+ message = quitBundle.formatStringFromName("messageNoWindows",
+ [appName], 1);
+ else /* quit application with 2 or more windows */
+ message = quitBundle.formatStringFromName("message",
+ [appName], 1);
+
+ var flags = Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1 +
+ Services.prompt.BUTTON_POS_0_DEFAULT;
+
+ var neverAsk = {value:false};
+ var button0Title, button1Title, button2Title;
+ var neverAskText = quitBundle.GetStringFromName("neverAsk");
+
+ if (aQuitType == "restart") {
+ button0Title = quitBundle.GetStringFromName("restartNowTitle");
+ button1Title = quitBundle.GetStringFromName("restartLaterTitle");
+ } else {
+ flags += Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2;
+ button0Title = quitBundle.GetStringFromName(
+ (aQuitType == "quit" ? "saveTitle" : "savelastwindowTitle"));
+ button1Title = quitBundle.GetStringFromName("cancelTitle");
+ button2Title = quitBundle.GetStringFromName(aQuitType + "Title"); /* "quitTitle" or "lastwindowTitle" */
+ }
+
+ var mostRecentBrowserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ var buttonChoice = Services.prompt.confirmEx(mostRecentBrowserWindow, quitDialogTitle, message,
+ flags, button0Title, button1Title, button2Title,
+ neverAskText, neverAsk);
+
+ switch (buttonChoice) {
+ case 2:
+ if (neverAsk.value)
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+ break;
+ case 1:
+ aCancelQuit.QueryInterface(Ci.nsISupportsPRBool);
+ aCancelQuit.data = true;
+ break;
+ case 0:
+ this._saveSession = true;
+ if (neverAsk.value) {
+ if (aQuitType == "restart")
+ Services.prefs.setBoolPref("browser.warnOnRestart", false);
+ else {
+ // always save state when shutting down
+ Services.prefs.setIntPref("browser.startup.page", 3);
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ /*
+ * _shouldShowRights - Determines if the user should be shown the
+ * about:rights notification. The notification should *not* be shown if
+ * we've already shown the current version, or if the override pref says to
+ * never show it. The notification *should* be shown if it's never been seen
+ * before, if a newer version is available, or if the override pref says to
+ * always show it.
+ */
+ _shouldShowRights: function () {
+ // Look for an unconditional override pref. If set, do what it says.
+ // (true --> never show, false --> always show)
+ try {
+ return !Services.prefs.getBoolPref("browser.rights.override");
+ } catch (e) { }
+ // Ditto, for the legacy EULA pref (tinderbox testing profile sets this).
+ try {
+ return !Services.prefs.getBoolPref("browser.EULA.override");
+ } catch (e) { }
+
+ // Look to see if the user has seen the current version or not.
+ var currentVersion = Services.prefs.getIntPref("browser.rights.version");
+ try {
+ return !Services.prefs.getBoolPref("browser.rights." + currentVersion + ".shown");
+ } catch (e) { }
+
+ // We haven't shown the notification before, so do so now.
+ return true;
+ },
+
+ _showRightsNotification: function(aNotifyBox) {
+ // Stick the notification onto the selected tab of the active browser window.
+ aNotifyBox.showRightsNotification();
+
+ // Set pref to indicate we've shown the notficiation.
+ var currentVersion = Services.prefs.getIntPref("browser.rights.version");
+ Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true);
+ },
+
+ /*
+ * _shouldShowUpdateWarning - Determines if the user should be warned about
+ * having updates off and an old build that likely should be updated.
+ */
+ _shouldShowUpdateWarning: function () {
+ // If the Updater is not available we don't show the warning.
+ if (!AppConstants.MOZ_UPDATER) {
+ return false;
+ }
+ // Look for an unconditional override pref. If set, do what it says.
+ // (true --> never show, false --> always show)
+ try {
+ return !Services.prefs.getBoolPref("app.updatecheck.override");
+ } catch (e) { }
+ // If updates are enabled, we don't need to worry.
+ if (Services.prefs.getBoolPref("app.update.enabled"))
+ return false;
+ var maxAge = 90 * 86400; // 90 days
+ var now = Math.round(Date.now() / 1000);
+ // If there was an automated update tried in the interval, don't worry.
+ const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
+ var lastUpdateTime = Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LASTUPDATETIME) ?
+ Services.prefs.getIntPref(PREF_APP_UPDATE_LASTUPDATETIME) : 0;
+ if (lastUpdateTime + maxAge > now)
+ return false;
+
+ var buildID = Services.appinfo.appBuildID;
+ // construct build date from ID
+ var buildDate = new Date(buildID.substr(0, 4),
+ buildID.substr(4, 2) - 1,
+ buildID.substr(6, 2));
+ var buildTime = Math.round(buildDate / 1000);
+ // We should warn if the build is older than the max age.
+ return (buildTime + maxAge <= now);
+ },
+
+ // This method gets the shell service and has it check its settings.
+ // This will do nothing on platforms without a shell service.
+ _checkForDefaultClient: function checkForDefaultClient(aWindow)
+ {
+ if (ShellService) try {
+ var appTypes = ShellService.shouldBeDefaultClientFor;
+
+ // Show the default client dialog only if we should check for the default
+ // client and we aren't already the default for the stored app types in
+ // shell.checkDefaultApps.
+ if (appTypes && ShellService.shouldCheckDefaultClient &&
+ !ShellService.isDefaultClient(true, appTypes)) {
+ aWindow.openDialog("chrome://communicator/content/defaultClientDialog.xul",
+ "DefaultClient",
+ "modal,centerscreen,chrome,resizable=no");
+ }
+ } catch (e) {}
+ },
+
+ /**
+ * Initialize Places
+ * - imports the bookmarks html file if bookmarks database is empty, try to
+ * restore bookmarks from a JSON backup if the backend indicates that the
+ * database was corrupt.
+ *
+ * These prefs can be set up by the frontend:
+ *
+ * WARNING: setting these preferences to true will overwite existing bookmarks
+ *
+ * - browser.places.importBookmarksHTML
+ * Set to true will import the bookmarks.html file from the profile folder.
+ * - browser.places.smartBookmarksVersion
+ * Set during HTML import to indicate that Smart Bookmarks were created.
+ * Set to -1 to disable Smart Bookmarks creation.
+ * Set to 0 to restore current Smart Bookmarks.
+ * - browser.bookmarks.restore_default_bookmarks
+ * Set to true by safe-mode dialog to indicate we must restore default
+ * bookmarks.
+ */
+ _initPlaces: function BG__initPlaces(aInitialMigrationPerformed) {
+ // We must instantiate the history service since it will tell us if we
+ // need to import or restore bookmarks due to first-run, corruption or
+ // forced migration (due to a major schema change).
+ // If the database is corrupt or has been newly created we should
+ // import bookmarks.
+ let dbStatus = PlacesUtils.history.databaseStatus;
+
+ // The places.sqlite database is locked. We show a notification box for
+ // it in _onBrowserStartup.
+ if (dbStatus == PlacesUtils.history.DATABASE_STATUS_LOCKED) {
+ this._isPlacesDatabaseLocked = true;
+ Services.console.logStringMessage("places.sqlite is locked");
+ // Note: initPlaces should always happen when the first window is ready,
+ // in any case, better safe than sorry.
+ Services.obs.notifyObservers(null, "places-browser-init-complete");
+ return;
+ }
+
+ let importBookmarks = !aInitialMigrationPerformed &&
+ (dbStatus == PlacesUtils.history.DATABASE_STATUS_CREATE ||
+ dbStatus == PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // Check if user or an extension has required to import bookmarks.html.
+ let importBookmarksHTML = false;
+ try {
+ importBookmarksHTML =
+ Services.prefs.getBoolPref("browser.places.importBookmarksHTML");
+ if (importBookmarksHTML)
+ importBookmarks = true;
+ } catch (ex) {}
+
+ // Support legacy bookmarks.html format for apps that depend on that format.
+ // Default if the pref does not exists is 'Do not export'.
+ let autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML", false);
+
+ if (autoExportHTML) {
+ // Sqlite.jsm and Places shutdown happen at profile-before-change, thus,
+ // to be on the safe side, this should run earlier.
+ AsyncShutdown.profileChangeTeardown.addBlocker(
+ "Places: export bookmarks.html",
+ () => BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks",
+ Ci.nsIFile).path));
+ }
+
+ (async () => {
+ // Check if Safe Mode or the user has required to restore bookmarks from
+ // default profile's bookmarks.html.
+ let restoreDefaultBookmarks = false;
+ try {
+ restoreDefaultBookmarks =
+ Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks");
+ if (restoreDefaultBookmarks) {
+ // Ensure that we already have a bookmarks backup for today.
+ await this._backupBookmarks();
+ importBookmarks = true;
+ }
+ } catch (ex) {}
+
+ // This may be reused later, check for "=== undefined" to see if it has
+ // been populated already.
+ let lastBackupFile;
+
+ // If the user did not require to restore default bookmarks, or import
+ // from bookmarks.html, we will try to restore from JSON.
+ if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
+ // Get latest JSON backup.
+ lastBackupFile = await PlacesBackups.getMostRecentBackup();
+ if (lastBackupFile) {
+ // Restore from JSON backup.
+ await BookmarkJSONUtils.importFromFile(lastBackupFile, true);
+ importBookmarks = false;
+ } else {
+ // We have created a new database but we don't have any backup available.
+ importBookmarks = true;
+ let bookmarksHTMLFile = Services.dirsvc.get("BMarks", Ci.nsIFile);
+ if (bookmarksHTMLFile.exists(bookmarksHTMLFile)) {
+ // If bookmarks.html is available in current profile import it...
+ importBookmarksHTML = true;
+ } else {
+ // ...otherwise we will restore defaults.
+ restoreDefaultBookmarks = true;
+ }
+ }
+ }
+
+ // If bookmarks are not imported, then initialize smart bookmarks. This
+ // happens during a common startup.
+ // Otherwise, if any kind of import runs, smart bookmarks creation should
+ // be delayed till the import operations has finished. Not doing so would
+ // cause them to be overwritten by the newly imported bookmarks.
+ if (!importBookmarks) {
+ try {
+ await this.ensurePlacesDefaultQueriesInitialized();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ } else {
+ // An import operation is about to run.
+ // Don't try to recreate smart bookmarks if autoExportHTML is true or
+ // smart bookmarks are disabled.
+ let smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion", 0);
+ if (!autoExportHTML && smartBookmarksVersion != -1)
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+
+ let bookmarksURI = null;
+ if (restoreDefaultBookmarks) {
+ // User wants to restore bookmarks.html file from default profile folder
+ bookmarksURI = Services.io.newURI("resource:///defaults/profile/bookmarks.html");
+ } else {
+ let bookmarksFile = Services.dirsvc.get("BMarks", Ci.nsIFile);
+ if (bookmarksFile.exists(bookmarksFile)) {
+ bookmarksURI = Services.io.newFileURI(bookmarksFile);
+ }
+ }
+
+ if (bookmarksURI) {
+ // Import from bookmarks.html file.
+ try {
+ await BookmarkHTMLUtils.importFromURL(bookmarksURI.spec, true);
+ } catch (e) {
+ Cu.reportError("Bookmarks.html file could be corrupt. " + e);
+ }
+ try {
+ // Ensure that smart bookmarks are created once the operation is
+ // complete.
+ await this.ensurePlacesDefaultQueriesInitialized();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ } else {
+ Cu.reportError(new Error("Unable to find bookmarks.html file."));
+ }
+
+ // Reset preferences, so we won't try to import again at next run
+ if (importBookmarksHTML)
+ Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false);
+ if (restoreDefaultBookmarks)
+ Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks",
+ false);
+ }
+
+ AsyncShutdown.quitApplicationGranted.addBlocker(
+ "Places: export bookmarks at dawn",
+ () => this._backupBookmarks());
+
+ // Initialize bookmark archiving on idle.
+ if (!this._isIdleObserver) {
+ this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME_SEC);
+ this._isIdleObserver = true;
+ }
+
+ })().catch(ex => {
+ Cu.reportError(ex);
+ }).then(() => {
+ // NB: deliberately after the catch so that we always do this, even if
+ // we threw halfway through initializing in the Task above.
+ Services.obs.notifyObservers(null, "places-browser-init-complete");
+ });
+ },
+
+ /**
+ * If a backup for today doesn't exist, this creates one.
+ */
+ _backupBookmarks: function BG__backupBookmarks() {
+ return (async function() {
+ let lastBackupFile = await PlacesBackups.getMostRecentBackup();
+ // Should backup bookmarks if there are no backups or the maximum
+ // interval between backups elapsed.
+ if (!lastBackupFile ||
+ new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000) {
+ let maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
+ await PlacesBackups.create(maxBackups);
+ }
+ })();
+ },
+
+ _updatePrefs: function()
+ {
+ // Make sure that the doNotTrack value conforms to the conversion from
+ // three-state to two-state. (This reverts a setting of "please track me"
+ // to the default "don't say anything").
+ try {
+ if (Services.prefs.getIntPref("privacy.donottrackheader.value") != 1) {
+ Services.prefs.clearUserPref("privacy.donottrackheader.enabled");
+ Services.prefs.clearUserPref("privacy.donottrackheader.value");
+ }
+ } catch (ex) {}
+
+ // Migration of document-color preference which changed from boolean to
+ // tri-state; 0=always but not accessibility themes, 1=always, 2=never
+ try {
+ if (!Services.prefs.getBoolPref("browser.display.use_document_colors")) {
+ Services.prefs.setIntPref("browser.display.document_color_use", 2);
+ Services.prefs.clearUserPref("browser.display.use_document_colors");
+ }
+ } catch (ex) {}
+
+ // Try to get dictionary preference and adjust if not valid.
+ var prefName = "spellchecker.dictionary";
+ var prefValue = Services.prefs.getCharPref(prefName);
+
+ // replace underscore with dash if found in language
+ if (/_/.test(prefValue)) {
+ prefValue = prefValue.replace(/_/g, "-");
+ Services.prefs.setCharPref(prefName, prefValue);
+ }
+
+ var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+ var dictList = spellChecker.getDictionaryList();
+ // If the preference contains an invalid dictionary, set it to a valid
+ // dictionary, any dictionary will do.
+ if (dictList.length && !dictList.includes(prefValue))
+ Services.prefs.setCharPref(prefName, dictList[0]);
+ },
+
+ _migrateDownloadPrefs: function()
+ {
+ // Migration of download-manager preferences
+ if (Services.prefs.getPrefType("browser.download.dir") == Services.prefs.PREF_INVALID ||
+ Services.prefs.getPrefType("browser.download.lastDir") != Services.prefs.PREF_INVALID)
+ return; //Do nothing if .dir does not exist, or if it exists and lastDir does not
+
+ try {
+ Services.prefs.setComplexValue("browser.download.lastDir",
+ Ci.nsIFile,
+ Services.prefs.getComplexValue("browser.download.dir",
+ Ci.nsIFile));
+ } catch (ex) {
+ // Ensure that even if we don't end up migrating to a lastDir that we
+ // don't attempt another update. This will throw when QI'ed to
+ // nsIFile, but it does fallback gracefully.
+ Services.prefs.setCharPref("browser.download.lastDir", "");
+ }
+
+ try {
+ Services.prefs.setBoolPref("browser.download.useDownloadDir",
+ Services.prefs.getBoolPref("browser.download.autoDownload"));
+ } catch (ex) {}
+
+ try {
+ Services.prefs.setIntPref("browser.download.manager.behavior",
+ Services.prefs.getIntPref("browser.downloadmanager.behavior"));
+ } catch (ex) {}
+
+ try {
+ Services.prefs.setBoolPref("browser.download.progress.closeWhenDone",
+ !Services.prefs.getBoolPref("browser.download.progressDnldDialog.keepAlive"));
+ } catch (ex) {}
+ },
+
+ /**
+ * Devtools Debugger
+ */
+ get dbgIsEnabled()
+ {
+ return Services.prefs.getBoolPref(DEBUGGER_REMOTE_ENABLED);
+ },
+
+ dbgStart: function()
+ {
+ var port = Services.prefs.getIntPref(DEBUGGER_REMOTE_PORT);
+
+ // Make sure chrome debugging is enabled, no sense in starting otherwise.
+ DebuggerServer.allowChromeProcess = true;
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ try {
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = port;
+
+ // Expose this listener via wifi discovery, if enabled.
+ if (Services.prefs.getBoolPref(DEBUGGER_WIFI_VISIBLE) &&
+ !Services.prefs.getBoolPref(DEBUGGER_FORCE_LOCAL)) {
+ listener.discoverable = true;
+ }
+
+ listener.open();
+ } catch(e) {}
+ },
+
+ dbgStop: function()
+ {
+ if (DebuggerServer.initialized)
+ DebuggerServer.closeAllListeners();
+ },
+
+ dbgRestart: function()
+ {
+ this.dbgStop();
+ this.dbgStart();
+ },
+
+ // ------------------------------
+ // public nsISuiteGlue members
+ // ------------------------------
+
+ showDownloadManager: function(newDownload)
+ {
+ if (!gDownloadManager) {
+ // Use an empty arguments string or the download manager window
+ // will miss the toolbar and other features.
+ var argString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ argString.data = "";
+ gDownloadManager = Services.ww.openWindow(null, DOWNLOAD_MANAGER_URL,
+ null,
+ "all,dialog=no,non-private",
+ argString);
+ gDownloadManager.addEventListener("load", function() {
+ gDownloadManager.addEventListener("unload", function() {
+ gDownloadManager = null;
+ });
+ // Attach the taskbar progress meter to the download manager window.
+ ChromeUtils.import("resource:///modules/DownloadsTaskbar.jsm", {})
+ .DownloadsTaskbar.attachIndicator(gDownloadManager);
+ });
+ } else if (!newDownload ||
+ Services.prefs.getBoolPref(PREF_FOCUS_WHEN_STARTING)) {
+ gDownloadManager.focus();
+ } else {
+ // This preference may not be set, so defaulting to two.
+ var flashCount = 2;
+ try {
+ flashCount = Services.prefs.getIntPref(PREF_FLASH_COUNT);
+ } catch (e) { }
+ gDownloadManager.getAttentionWithCycleCount(flashCount);
+ }
+ },
+
+ sanitize(aParentWindow) {
+ Sanitizer.showUI(aParentWindow);
+ },
+
+ async ensurePlacesDefaultQueriesInitialized() {
+ // This is the current smart bookmarks version, it must be increased every
+ // time they change.
+ // When adding a new smart bookmark below, its newInVersion property must
+ // be set to the version it has been added in. We will compare its value
+ // to users' smartBookmarksVersion and add new smart bookmarks without
+ // recreating old deleted ones.
+ const SMART_BOOKMARKS_VERSION = 7;
+ const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
+ const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
+
+ // TODO bug 399268: should this be a pref?
+ const MAX_RESULTS = 10;
+
+ // Get current smart bookmarks version. If not set, create them.
+ let smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF, 0);
+
+ // If version is current, or smart bookmarks are disabled, bail out.
+ if (smartBookmarksCurrentVersion == -1 ||
+ smartBookmarksCurrentVersion >= SMART_BOOKMARKS_VERSION) {
+ return;
+ }
+
+ try {
+ let menuIndex = 0;
+ let toolbarIndex = 0;
+ let bundle = Services.strings.createBundle("chrome://communicator/locale/places/places.properties");
+ let queryOptions = Ci.nsINavHistoryQueryOptions;
+
+ let smartBookmarks = {
+ MostVisited: {
+ title: bundle.GetStringFromName("mostVisitedTitle"),
+ url: "place:sort=" + queryOptions.SORT_BY_VISITCOUNT_DESCENDING +
+ "&maxResults=" + MAX_RESULTS,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ newInVersion: 1
+ },
+ RecentlyBookmarked: {
+ title: bundle.GetStringFromName("recentlyBookmarkedTitle"),
+ url: "place:folder=BOOKMARKS_MENU" + "&folder=UNFILED_BOOKMARKS" +
+ "&folder=TOOLBAR" +
+ "&queryType=" + queryOptions.QUERY_TYPE_BOOKMARKS +
+ "&sort=" + queryOptions.SORT_BY_DATEADDED_DESCENDING +
+ "&maxResults=" + MAX_RESULTS +
+ "&excludeQueries=1",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ newInVersion: 1
+ },
+ RecentTags: {
+ title: bundle.GetStringFromName("recentTagsTitle"),
+ url: "place:type=" + queryOptions.RESULTS_AS_TAG_QUERY +
+ "&sort=" + queryOptions.SORT_BY_LASTMODIFIED_DESCENDING +
+ "&maxResults=" + MAX_RESULTS,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ newInVersion: 1
+ },
+ };
+
+ // Set current guid, parentGuid and index of existing Smart Bookmarks.
+ // We will use those to create a new version of the bookmark at the same
+ // position.
+ let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ for (let itemId of smartBookmarkItemIds) {
+ let queryId = PlacesUtils.annotations.getItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
+ if (queryId in smartBookmarks) {
+ // Known smart bookmark.
+ let smartBookmark = smartBookmarks[queryId];
+ smartBookmark.guid = await PlacesUtils.promiseItemGuid(itemId);
+
+ if (!smartBookmark.url) {
+ await PlacesUtils.bookmarks.remove(smartBookmark.guid);
+ continue;
+ }
+
+ let bm = await PlacesUtils.bookmarks.fetch(smartBookmark.guid);
+ smartBookmark.parentGuid = bm.parentGuid;
+ smartBookmark.index = bm.index;
+ } else {
+ // We don't remove old Smart Bookmarks because user could still
+ // find them useful, or could have personalized them.
+ // Instead we remove the Smart Bookmark annotation.
+ PlacesUtils.annotations.removeItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
+ }
+ }
+
+ for (let queryId of Object.keys(smartBookmarks)) {
+ let smartBookmark = smartBookmarks[queryId];
+
+ // We update or create only changed or new smart bookmarks.
+ // Also we respect user choices, so we won't try to create a smart
+ // bookmark if it has been removed.
+ if (smartBookmarksCurrentVersion > 0 &&
+ smartBookmark.newInVersion <= smartBookmarksCurrentVersion &&
+ !smartBookmark.guid || !smartBookmark.url)
+ continue;
+
+ // Remove old version of the smart bookmark if it exists, since it
+ // will be replaced in place.
+ if (smartBookmark.guid) {
+ await PlacesUtils.bookmarks.remove(smartBookmark.guid);
+ }
+
+ // Create the new smart bookmark and store its updated guid.
+ if (!("index" in smartBookmark)) {
+ if (smartBookmark.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
+ smartBookmark.index = toolbarIndex++;
+ else if (smartBookmark.parentGuid == PlacesUtils.bookmarks.menuGuid)
+ smartBookmark.index = menuIndex++;
+ }
+ smartBookmark = await PlacesUtils.bookmarks.insert(smartBookmark);
+ let itemId = await PlacesUtils.promiseItemId(smartBookmark.guid);
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ SMART_BOOKMARKS_ANNO,
+ queryId, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ }
+
+ // If we are creating all Smart Bookmarks from ground up, add a
+ // separator below them in the bookmarks menu.
+ if (smartBookmarksCurrentVersion == 0 &&
+ smartBookmarkItemIds.length == 0) {
+ let bm = await PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: menuIndex });
+ // Don't add a separator if the menu was empty or there is one already.
+ if (bm && bm.type != PlacesUtils.bookmarks.TYPE_SEPARATOR) {
+ await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: menuIndex });
+ }
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ Services.prefs.setIntPref(SMART_BOOKMARKS_PREF, SMART_BOOKMARKS_VERSION);
+ Services.prefs.savePrefFile(null);
+ }
+ },
+
+ /**
+ * Called as an observer when Sync's "display URI" notification is fired.
+ */
+ _onDisplaySyncURI: function _onDisplaySyncURI(data) {
+ try {
+ var url = data.wrappedJSObject.object.uri;
+ var mostRecentBrowserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ if (mostRecentBrowserWindow) {
+ mostRecentBrowserWindow.getBrowser().addTab(url, { focusNewTab: true });
+ mostRecentBrowserWindow.content.focus();
+ } else {
+ var args = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ args.data = url;
+ var chromeURL = Services.prefs.getCharPref("browser.chromeURL");
+ Services.ww.openWindow(null, chromeURL, "_blank", "chrome,all,dialog=no", args);
+ }
+ } catch (e) {
+ Cu.reportError("Error displaying tab received by Sync: " + e);
+ }
+ },
+
+ // for XPCOM
+ classID: Components.ID("{bbbbe845-5a1b-40ee-813c-f84b8faaa07c}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISuiteGlue])
+
+}
+
+/**
+ * ContentPermissionIntegration is responsible for showing the user
+ * simple permission prompts when content requests additional
+ * capabilities.
+ *
+ * While there are some built-in permission prompts, createPermissionPrompt
+ * can also be overridden by system add-ons or tests to provide new ones.
+ *
+ * This override ability is provided by Integration.jsm. See
+ * PermissionUI.jsm for an example of how to provide a new prompt
+ * from an add-on.
+ */
+var ContentPermissionIntegration = {
+ /**
+ * Creates a PermissionPrompt for a given permission type and
+ * nsIContentPermissionRequest.
+ *
+ * @param {string} type
+ * The type of the permission request from content. This normally
+ * matches the "type" field of an nsIContentPermissionType, but it
+ * can be something else if the permission does not use the
+ * nsIContentPermissionRequest model. Note that this type might also
+ * be different from the permission key used in the permissions
+ * database.
+ * Example: "geolocation"
+ * @param {nsIContentPermissionRequest} request
+ * The request for a permission from content.
+ * @return {PermissionPrompt} (see PermissionUI.jsm),
+ * or undefined if the type cannot be handled.
+ */
+ createPermissionPrompt(type, request) {
+ switch (type) {
+ case "geolocation": {
+ return new PermissionUI.GeolocationPermissionPrompt(request);
+ }
+ case "desktop-notification": {
+ return new PermissionUI.DesktopNotificationPermissionPrompt(request);
+ }
+ case "persistent-storage": {
+ if (Services.prefs.getBoolPref("browser.storageManager.enabled")) {
+ return new PermissionUI.PersistentStoragePermissionPrompt(request);
+ }
+ }
+ }
+ return undefined;
+ },
+};
+
+function ContentPermissionPrompt() {}
+
+ContentPermissionPrompt.prototype = {
+ classID: Components.ID("{9d4c845d-3f09-402a-b66d-50f291d7d50f}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
+
+ /**
+ * This implementation of nsIContentPermissionPrompt.prompt ensures
+ * that there's only one nsIContentPermissionType in the request,
+ * and that it's of type nsIContentPermissionType. Failing to
+ * satisfy either of these conditions will result in this method
+ * throwing NS_ERRORs. If the combined ContentPermissionIntegration
+ * cannot construct a prompt for this particular request, an
+ * NS_ERROR_FAILURE will be thrown.
+ *
+ * Any time an error is thrown, the nsIContentPermissionRequest is
+ * cancelled automatically.
+ *
+ * @param {nsIContentPermissionRequest} request
+ * The request that we're to show a prompt for.
+ */
+ prompt(request) {
+ try {
+ // Only allow exactly one permission request here.
+ let types = request.types.QueryInterface(Ci.nsIArray);
+ if (types.length != 1) {
+ throw Components.Exception(
+ "Expected an nsIContentPermissionRequest with only 1 type.",
+ Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let type = types.queryElementAt(0, Ci.nsIContentPermissionType).type;
+ let combinedIntegration =
+ Integration.contentPermission.getCombined(ContentPermissionIntegration);
+
+ let permissionPrompt =
+ combinedIntegration.createPermissionPrompt(type, request);
+ if (!permissionPrompt) {
+ throw Components.Exception(
+ "Failed to handle permission of type ${type}",
+ Cr.NS_ERROR_FAILURE);
+ }
+
+ permissionPrompt.prompt();
+ } catch (ex) {
+ Cu.reportError(ex);
+ request.cancel();
+ throw ex;
+ }
+ },
+};
+
+//module initialization
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([SuiteGlue, ContentPermissionPrompt]);
diff --git a/comm/suite/components/permissions/content/cookieViewer.js b/comm/suite/components/permissions/content/cookieViewer.js
new file mode 100644
index 0000000000..d864f72908
--- /dev/null
+++ b/comm/suite/components/permissions/content/cookieViewer.js
@@ -0,0 +1,531 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+// cookies and permissions list
+var cookies = [];
+var permissions = [];
+var allCookies = [];
+var deletedCookies = [];
+var deletedPermissions = [];
+
+var cookieBundle;
+var gUpdatingBatch = "";
+var lastCookieSortColumn;
+var lastCookieSortAscending;
+var lastPermissionSortColumn;
+var lastPermissionSortAscending;
+
+function Startup() {
+
+ // arguments passed to this routine:
+ // cookieManager
+
+ // intialize string bundle
+ cookieBundle = document.getElementById("cookieBundle");
+
+ // load in the cookies and permissions
+ cookiesTree = document.getElementById("cookiesTree");
+ lastCookieSortAscending = (cookiesTree.getAttribute("sortAscending") == "true");
+ lastCookieSortColumn = cookiesTree.getAttribute("sortColumn");
+ permissionsTree = document.getElementById("permissionsTree");
+ lastPermissionSortAscending = (permissionsTree.getAttribute("sortAscending") == "true");
+ lastPermissionSortColumn = permissionsTree.getAttribute("sortColumn");
+ loadCookies();
+ loadPermissions();
+
+ // be prepared to reload the display if anything changes
+ Services.obs.addObserver(cookieReloadDisplay, "cookie-changed", false);
+ Services.obs.addObserver(cookieReloadDisplay, "perm-changed", false);
+
+ // filter the table if requested by caller
+ if (window.arguments &&
+ window.arguments[0] &&
+ window.arguments[0].filterString)
+ setFilter(window.arguments[0].filterString);
+
+ document.getElementById("filter").focus();
+}
+
+function Shutdown() {
+ Services.obs.removeObserver(cookieReloadDisplay, "cookie-changed");
+ Services.obs.removeObserver(cookieReloadDisplay, "perm-changed");
+}
+
+function PromptConfirm(title, msg, yes) {
+ var flags =
+ ((Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ Services.prompt.BUTTON_POS_1_DEFAULT)
+ return Services.prompt.confirmEx(window,
+ cookieBundle.getString(title),
+ cookieBundle.getString(msg),
+ flags,
+ cookieBundle.getString(yes),
+ null, null, null, {value:0});
+}
+
+var cookieReloadDisplay = {
+ observe: function(subject, topic, state) {
+ if (topic == gUpdatingBatch)
+ return;
+ if (topic == "cookie-changed") {
+ allCookies.length = 0;
+ loadCookies();
+ } else if (topic == "perm-changed") {
+ permissions.length = 0;
+ loadPermissions();
+ }
+ }
+}
+
+function doSelectAll() {
+ var elem = document.commandDispatcher.focusedElement;
+ if (elem && "treeBoxObject" in elem)
+ elem.view.selection.selectAll();
+}
+
+/*** =================== COOKIES CODE =================== ***/
+
+var cookiesTreeView = {
+ rowCount : 0,
+ setTree : function(tree){},
+ getImageSrc : function(row,column) {},
+ getProgressMode : function(row,column) {},
+ getCellValue : function(row,column) {},
+ getCellText : function(row,column){ return cookies[row][column.id]; },
+ isSeparator : function(index) {return false;},
+ isSorted: function() { return false; },
+ isContainer : function(index) {return false;},
+ cycleHeader : function(aCol) {},
+ getRowProperties : function(row) { return ""; },
+ getColumnProperties : function(column) { return ""; },
+ getCellProperties : function(row, column) { return ""; }
+};
+var cookiesTree;
+
+function Cookie(id, host, name, path, originAttributes, value,
+ isDomain, rawHost, isSecure, expires) {
+ this.id = id;
+ this.host = host;
+ this.name = name;
+ this.path = path;
+ this.originAttributes = originAttributes;
+ this.value = value;
+ this.isDomain = isDomain;
+ this.rawHost = rawHost;
+ this.isSecure = isSecure;
+ this.expires = GetExpiresString(expires);
+ this.expiresSortValue = expires;
+}
+
+function loadCookies() {
+ // load cookies into a table
+ var enumerator = Services.cookies.enumerator;
+ var count = 0;
+ while (enumerator.hasMoreElements()) {
+ var nextCookie = enumerator.getNext();
+ if (!nextCookie) break;
+ nextCookie = nextCookie.QueryInterface(Ci.nsICookie);
+ var host = nextCookie.host;
+ allCookies.push(
+ new Cookie(count++, host, nextCookie.name,
+ nextCookie.path, nextCookie.originAttributes,
+ nextCookie.value, nextCookie.isDomain,
+ host.charAt(0)=="." ? host.slice(1) : host,
+ nextCookie.isSecure, nextCookie.expires));
+ }
+
+ // filter, sort and display the table
+ cookiesTree.view = cookiesTreeView;
+ filter(document.getElementById("filter").value);
+}
+
+function GetExpiresString(expires) {
+ if (expires) {
+ var date = new Date(1000*expires);
+
+ // if a server manages to set a really long-lived cookie, the dateservice
+ // can't cope with it properly, so we'll just return a blank string
+ // see bug 238045 for details
+ var expiry = "";
+ try {
+ const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "full", timeStyle: "long" });
+ expiry = dateTimeFormatter.format(date);
+ } catch(ex) {
+ // do nothing
+ }
+ return expiry;
+ }
+ return cookieBundle.getString("expireAtEndOfSession");
+}
+
+function CookieSelected() {
+ var selections = GetTreeSelections(cookiesTree);
+ if (selections.length) {
+ document.getElementById("removeCookie").removeAttribute("disabled");
+ } else {
+ document.getElementById("removeCookie").setAttribute("disabled", "true");
+ ClearCookieProperties();
+ return true;
+ }
+
+ var idx = selections[0];
+ if (idx >= cookies.length) {
+ // Something got out of synch. See bug 119812 for details
+ dump("Tree and viewer state are out of sync! " +
+ "Help us figure out the problem in bug 119812");
+ return false;
+ }
+
+ var props = [
+ {id: "ifl_name", value: cookies[idx].name},
+ {id: "ifl_value", value: cookies[idx].value},
+ {id: "ifl_isDomain",
+ value: cookies[idx].isDomain ?
+ cookieBundle.getString("domainColon") : cookieBundle.getString("hostColon")},
+ {id: "ifl_host", value: cookies[idx].host},
+ {id: "ifl_path", value: cookies[idx].path},
+ {id: "ifl_isSecure",
+ value: cookies[idx].isSecure ?
+ cookieBundle.getString("forSecureOnly") :
+ cookieBundle.getString("forAnyConnection")},
+ {id: "ifl_expires", value: cookies[idx].expires}
+ ];
+
+ var value;
+ var field;
+
+ for (let lProp of props)
+ {
+ field = document.getElementById(lProp.id);
+ if ((selections.length > 1) && (lProp.id != "ifl_isDomain")) {
+ value = ""; // clear field if multiple selections
+ } else {
+ value = lProp.value;
+ }
+ field.value = value;
+ }
+ return true;
+}
+
+function ClearCookieProperties() {
+ var properties =
+ ["ifl_name","ifl_value","ifl_host","ifl_path","ifl_isSecure","ifl_expires"];
+ for (let prop of properties) {
+ document.getElementById(prop).value = "";
+ }
+}
+
+function DeleteCookie() {
+ if (cookiesTreeView.selection.count > 1 &&
+ PromptConfirm("deleteSelectedCookiesTitle",
+ "deleteSelectedCookies",
+ "deleteSelectedCookiesYes") == 1) {
+ return;
+ }
+ DeleteSelectedItemFromTree(cookiesTree, cookiesTreeView,
+ cookies, deletedCookies,
+ "removeCookie", "removeAllCookies");
+ if (document.getElementById("filter").value) {
+ // remove selected cookies from unfiltered set
+ for (let cookie of deletedCookies) {
+ allCookies.splice(allCookies.indexOf(cookie), 1);
+ }
+ }
+ if (!cookies.length) {
+ ClearCookieProperties();
+ }
+ FinalizeCookieDeletions();
+}
+
+function DeleteAllCookies() {
+ if (PromptConfirm("deleteAllCookiesTitle",
+ "deleteAllCookies",
+ "deleteAllCookiesYes") == 1) {
+ return;
+ }
+
+ ClearCookieProperties();
+ DeleteAllFromTree(cookiesTree, cookiesTreeView,
+ cookies, deletedCookies,
+ "removeCookie", "removeAllCookies");
+ allCookies.length = 0;
+ FinalizeCookieDeletions();
+}
+
+function FinalizeCookieDeletions() {
+ gUpdatingBatch = "cookie-changed";
+ for (let delCookie of deletedCookies) {
+ Services.cookies.remove(delCookie.host,
+ delCookie.name,
+ delCookie.path,
+ document.getElementById("checkbox").checked,
+ delCookie.originAttributes);
+ }
+ deletedCookies.length = 0;
+ gUpdatingBatch = "";
+}
+
+function HandleCookieKeyPress(e) {
+ if (e.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ DeleteCookie();
+ }
+}
+
+function CookieColumnSort(column, updateSelection) {
+ lastCookieSortAscending =
+ SortTree(cookiesTree, cookiesTreeView, cookies,
+ column, lastCookieSortColumn, lastCookieSortAscending,
+ updateSelection);
+ lastCookieSortColumn = column;
+
+ SetSortDirection(cookiesTree, column, lastCookieSortAscending);
+}
+
+/*** =================== PERMISSIONS CODE =================== ***/
+
+var permissionsTreeView = {
+ rowCount : 0,
+ setTree : function(tree){},
+ getImageSrc : function(row,column) {},
+ getProgressMode : function(row,column) {},
+ getCellValue : function(row,column) {},
+ getCellText : function(row,column) { return permissions[row][column.id]; },
+ isSeparator : function(index) {return false;},
+ isSorted: function() { return false; },
+ isContainer : function(index) {return false;},
+ cycleHeader : function(aCol) {},
+ getRowProperties : function(row) { return ""; },
+ getColumnProperties : function(column) { return ""; },
+ getCellProperties : function(row, column) { return ""; }
+};
+var permissionsTree;
+
+function Permission(id, principal, type, capability) {
+ this.id = id;
+ this.principal = principal;
+ this.host = principal.URI.hostPort;
+ this.scheme = principal.URI.scheme;
+ this.type = type;
+ this.capability = capability;
+}
+
+function loadPermissions() {
+ // load permissions into a table
+ var enumerator = Services.perms.enumerator;
+ var canStr = cookieBundle.getString("can");
+ var canSessionStr = cookieBundle.getString("canSession");
+ var cannotStr = cookieBundle.getString("cannot");
+ var capability;
+ var count = 0;
+ var permission;
+ while (enumerator.hasMoreElements()) {
+ permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
+ // We are only interested in cookie permissions in this code.
+ if (permission.type == "cookie") {
+ // It is currently possible to add a cookie permission for about:xxx
+ // and other internal pages. They are probably invalid and will be
+ // ignored for now.
+ // Test if the permission has a host.
+ try {
+ permission.principal.URI.host;
+ }
+ catch (e) {
+ Cu.reportError("Invalid permission found: " +
+ permission.principal.origin + " " + permission.type);
+ continue;
+ }
+
+ switch (permission.capability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ capability = canStr;
+ break;
+ case Ci.nsIPermissionManager.DENY_ACTION:
+ capability = cannotStr;
+ break;
+ case Ci.nsICookiePermission.ACCESS_SESSION:
+ capability = canSessionStr;
+ break;
+ default:
+ continue;
+ }
+ permissions.push(new Permission(count++,
+ permission.principal,
+ permission.type,
+ capability));
+ }
+ }
+ permissionsTreeView.rowCount = permissions.length;
+
+ // sort and display the table
+ permissionsTree.view = permissionsTreeView;
+ permissionsTreeView.selection.clearSelection();
+ SortTree(permissionsTree, permissionsTreeView, permissions,
+ lastPermissionSortColumn, lastPermissionSortColumn,
+ !lastPermissionSortAscending);
+
+ // disable "remove all" button if there are no cookies
+ document.getElementById("removeAllPermissions").disabled = permissions.length == 0;
+}
+
+function DeletePermission() {
+ if (permissionsTreeView.selection.count > 1 &&
+ PromptConfirm("deleteSelectedSitesTitle",
+ "deleteSelectedCookiesSites",
+ "deleteSelectedSitesYes") == 1) {
+ return;
+ }
+ DeleteSelectedItemFromTree(permissionsTree, permissionsTreeView,
+ permissions, deletedPermissions,
+ "removePermission", "removeAllPermissions");
+ FinalizePermissionDeletions();
+}
+
+function setCookiePermissions(action) {
+ var site = document.getElementById("cookie-site");
+
+ // let the backend do the validation
+ try {
+ var url = new URL(site.value);
+ } catch (e) {
+ // show an error if URL is invalid
+ window.alert(cookieBundle.getString("allowedURLSchemes"));
+ return;
+ }
+
+ try {
+ var uri = Services.io.newURI(url);
+ } catch (e) {
+ // show an error if URI can not be constructed or adding it failed
+ window.alert(cookieBundle.getString("errorAddPermission"));
+ return;
+ }
+ // only allow a few schemes here
+ // others like file:// would produce an invalid entry in the database
+ if (uri.scheme != "http" &&
+ uri.scheme != "https") {
+ // show an error if uri uses invalid scheme
+ window.alert(uri.scheme + ": " + cookieBundle.getString("allowedURLSchemes"));
+ return;
+ }
+
+ if (Services.perms.testPermission(uri, "cookie") != action)
+ Services.perms.add(uri, "cookie", action);
+
+ site.focus();
+ site.value = "";
+}
+
+function DeleteAllPermissions() {
+ if (PromptConfirm("deleteAllSitesTitle",
+ "deleteAllCookiesSites",
+ "deleteAllSitesYes") == 1) {
+ return;
+ }
+
+ DeleteAllFromTree(permissionsTree, permissionsTreeView,
+ permissions, deletedPermissions,
+ "removePermission", "removeAllPermissions");
+ FinalizePermissionDeletions();
+}
+
+function FinalizePermissionDeletions() {
+ if (!deletedPermissions.length)
+ return;
+
+ gUpdatingBatch = "perm-changed";
+ for (let permission of deletedPermissions)
+ Services.perms.removeFromPrincipal(permission.principal, permission.type);
+ deletedPermissions.length = 0;
+ gUpdatingBatch = "";
+}
+
+function HandlePermissionKeyPress(e) {
+ if (e.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ DeletePermission();
+ }
+}
+
+function PermissionColumnSort(column, updateSelection) {
+ lastPermissionSortAscending =
+ SortTree(permissionsTree, permissionsTreeView, permissions,
+ column, lastPermissionSortColumn, lastPermissionSortAscending,
+ updateSelection);
+ lastPermissionSortColumn = column;
+
+ SetSortDirection(permissionsTree, column, lastPermissionSortAscending);
+}
+
+/*** ============ CODE FOR HELP BUTTON =================== ***/
+
+function doHelpButton()
+{
+ var selTab = document.getElementById("tabbox").selectedTab;
+ var key = selTab.getAttribute("help");
+ openHelp(key, "chrome://communicator/locale/help/suitehelp.rdf");
+}
+
+/*** =================== FILTER CODE =================== ***/
+
+function filterCookies(aFilterValue)
+{
+ var filterSet = [];
+ for (let cookie of allCookies) {
+ if (cookie.rawHost.includes(aFilterValue) ||
+ cookie.name.includes(aFilterValue) ||
+ cookie.value.includes(aFilterValue))
+ filterSet.push(cookie);
+ }
+ return filterSet;
+}
+
+function filter(filter)
+{
+ // clear the display
+ var oldCount = cookiesTreeView.rowCount;
+ cookiesTreeView.rowCount = 0;
+ cookiesTree.treeBoxObject.rowCountChanged(0, -oldCount);
+
+ // set up the display
+ cookies = filter ? filterCookies(filter) : allCookies;
+ cookiesTreeView.rowCount = cookies.length;
+ cookiesTree.treeBoxObject.rowCountChanged(0, cookiesTreeView.rowCount);
+
+ // sort the tree according to the last sort parameters
+ SortTree(cookiesTree, cookiesTreeView, cookies, lastCookieSortColumn,
+ lastCookieSortColumn, !lastCookieSortAscending);
+
+ // disable Remove All Cookies button if the view is filtered or there are no cookies
+ if (filter || !cookies.length)
+ document.getElementById("removeAllCookies").setAttribute("disabled", "true");
+ else
+ document.getElementById("removeAllCookies").removeAttribute("disabled");
+
+ // if the view is filtered and not empty then select the first item
+ if (filter && cookies.length)
+ cookiesTreeView.selection.select(0);
+}
+
+function setFilter(aFilterString)
+{
+ document.getElementById("filter").value = aFilterString;
+ filter(aFilterString);
+}
+
+function focusFilterBox()
+{
+ var filterBox = document.getElementById("filter");
+ filterBox.focus();
+ filterBox.select();
+}
diff --git a/comm/suite/components/permissions/content/cookieViewer.xul b/comm/suite/components/permissions/content/cookieViewer.xul
new file mode 100644
index 0000000000..62a9db9ad3
--- /dev/null
+++ b/comm/suite/components/permissions/content/cookieViewer.xul
@@ -0,0 +1,225 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- CHANGE THIS WHEN MOVING FILES -->
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!-- CHANGE THIS WHEN MOVING FILES -->
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/permissions/cookieViewer.dtd" >
+
+<dialog id="cookieviewer"
+ buttons="help"
+ title="&windowtitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="mozilla:cookieviewer"
+ style="width: 65ch; height: 42em;"
+ onload="Startup()"
+ onunload="Shutdown()"
+ ondialoghelp="doHelpButton();"
+ persist="screenX screenY width height">
+
+ <script src="chrome://communicator/content/permissions/cookieViewer.js"/>
+ <script src="chrome://communicator/content/permissions/permissionsUtils.js"/>
+ <script src="chrome://global/content/treeUtils.js"/>
+ <script src="chrome://help/content/contextHelp.js" />
+
+ <keyset id="dialogKeys">
+ <key key="&focusSearch.key;"
+ modifiers="accel"
+ oncommand="focusFilterBox();"/>
+ <key key="&selectAll.key;"
+ modifiers="accel"
+ oncommand="doSelectAll();"/>
+ </keyset>
+ <stringbundle id="cookieBundle"
+ src="chrome://communicator/locale/permissions/cookieViewer.properties"/>
+
+ <tabbox id="tabbox" flex="1">
+ <tabs>
+ <tab id="cookiesTab" label="&tab.cookiesonsystem.label;" help="cookies_stored"/>
+ <tab id="permissionsTab" label="&tab.bannedservers.label;" help="cookie_sites"/>
+ </tabs>
+ <tabpanels id="panel" flex="1">
+ <vbox class="tabpanel" id="system" flex="1">
+ <vbox id="dummyContainer" flex="1">
+ <!-- filter -->
+ <hbox align="center">
+ <textbox id="filter"
+ flex="1"
+ type="search"
+ aria-controls="cookiesTree"
+ placeholder="&search.placeholder;"
+ oncommand="filter(this.value);"/>
+ </hbox>
+ <separator class="thin"/>
+ <label value="&div.cookiesonsystem.label;" control="cookiesTree"/>
+ <separator class="thin"/>
+ <tree id="cookiesTree" flex="1" style="height: 10em;"
+ onkeypress="HandleCookieKeyPress(event);"
+ onselect="CookieSelected();"
+ sortAscending="true"
+ sortColumn="rawHost"
+ persist="sortAscending sortColumn">
+ <treecols>
+ <treecol id="rawHost"
+ label="&treehead.cookiedomain.label;"
+ flex="5"
+ onclick="CookieColumnSort(this.id, true);"
+ sortDirection="ascending"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="name"
+ label="&treehead.cookiename.label;"
+ flex="5"
+ onclick="CookieColumnSort(this.id, true);"
+ persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="expires"
+ label="&treehead.cookieexpires.label;"
+ flex="10"
+ hidden="true"
+ onclick="CookieColumnSort(this.id, true);"
+ persist="width hidden"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <groupbox>
+ <caption label="&treehead.infoselected.label;"/>
+ <!-- labels -->
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&props.name.label;" control="ifl_name"/>
+ </hbox>
+ <textbox id="ifl_name" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&props.value.label;" control="ifl_value"/>
+ </hbox>
+ <textbox id="ifl_value" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label id="ifl_isDomain" value="&props.domain.label;" control="ifl_host"/>
+ </hbox>
+ <textbox id="ifl_host" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&props.path.label;" control="ifl_path"/>
+ </hbox>
+ <textbox id="ifl_path" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&props.secure.label;" control="ifl_isSecure"/>
+ </hbox>
+ <textbox id="ifl_isSecure" readonly="true" class="plain"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&props.expires.label;" control="ifl_expires"/>
+ </hbox>
+ <textbox id="ifl_expires" readonly="true" class="plain"/>
+ </row>
+
+ </rows>
+ </grid>
+ </groupbox>
+ <hbox>
+ <button id="removeCookie" disabled="true"
+ label="&button.removecookie.label;"
+ accesskey="&button.removecookie.accesskey;"
+ oncommand="DeleteCookie();"/>
+ <button id="removeAllCookies"
+ label="&button.removeallcookies.label;"
+ accesskey="&button.removeallcookies.accesskey;"
+ oncommand="DeleteAllCookies();"/>
+ <!-- todo: <button id="restoreCookies" class="dialog push" disabled="true" label="&button.restorecookie.label;" oncommand="RestoreCookies();"/> -->
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="start">
+ <checkbox id="checkbox" label="&futureCookies.label;" accesskey="&futureCookies.accesskey;" persist="checked"/>
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <vbox id="servers" flex="1">
+ <description id="permissionsText">&div.bannedservers.label;</description>
+ <separator class="thin"/>
+ <hbox>
+ <textbox id="cookie-site"
+ flex="1"
+ oninput="handleHostInput(this.value);"/>
+ <button id="btnBlock" label="&blockSite.label;" disabled="true"
+ accesskey="&blockSite.accesskey;"
+ oncommand="setCookiePermissions(Ci.nsIPermissionManager.DENY_ACTION);"/>
+ <button id="btnSession" label="&allowSiteSession.label;" disabled="true"
+ accesskey="&allowSiteSession.accesskey;"
+ oncommand="setCookiePermissions(Ci.nsICookiePermission.ACCESS_SESSION);"/>
+ <button id="btnAllow" label="&allowSite.label;" disabled="true"
+ accesskey="&allowSite.accesskey;"
+ oncommand="setCookiePermissions(Ci.nsIPermissionManager.ALLOW_ACTION);"/>
+ </hbox>
+ <separator class="thin"/>
+ <tree id="permissionsTree"
+ flex="1"
+ style="height: 10em;"
+ hidecolumnpicker="true"
+ onkeypress="HandlePermissionKeyPress(event);"
+ onselect="PermissionSelected(this);"
+ sortAscending="true"
+ sortColumn="host"
+ persist="sortAscending sortColumn">
+ <treecols>
+ <treecol id="host"
+ label="&treehead.sitename.label;"
+ flex="5"
+ onclick="PermissionColumnSort(this.id, true);"
+ sortDirection="ascending"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="scheme"
+ label="&treehead.scheme.label;"
+ flex="5"
+ onclick="PermissionColumnSort(this.id, true);"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="capability"
+ label="&treehead.status.label;"
+ flex="5"
+ onclick="PermissionColumnSort(this.id, true);"
+ persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox>
+ <button id="removePermission"
+ disabled="true"
+ label="&removepermission.label;"
+ accesskey="&removepermission.accesskey;"
+ oncommand="DeletePermission();"/>
+ <button id="removeAllPermissions"
+ label="&removeallpermissions.label;"
+ accesskey="&removeallpermissions.accesskey;"
+ oncommand="DeleteAllPermissions();"/>
+ </hbox>
+ </vbox>
+
+ </tabpanels>
+ </tabbox>
+</dialog>
diff --git a/comm/suite/components/permissions/content/permissionsManager.js b/comm/suite/components/permissions/content/permissionsManager.js
new file mode 100644
index 0000000000..9004dc3cda
--- /dev/null
+++ b/comm/suite/components/permissions/content/permissionsManager.js
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+var permissions = [];
+var removals = [];
+
+var sortColumn;
+var sortAscending;
+
+var permissionsTreeView = {
+ rowCount: 0,
+ setTree: function(tree) {},
+ getImageSrc: function(row, column) {},
+ getProgressMode: function(row, column) {},
+ getCellValue: function(row, column) {},
+ getCellText: function(row, column) { return permissions[row][column.id]; },
+ isSeparator: function(index) { return false; },
+ isSorted: function() { return false; },
+ isContainer: function(index) { return false; },
+ cycleHeader: function(column) {},
+ getRowProperties: function(row, column) { return ""; },
+ getColumnProperties: function(column) { return ""; },
+ getCellProperties: function(row, column) { return ""; }
+ };
+
+var permissionsTree;
+var permissionType = "popup";
+var gManageCapability;
+
+var permissionsBundle;
+
+function Startup() {
+ var introText, windowTitle;
+
+ permissionsTree = document.getElementById("permissionsTree");
+
+ permissionsBundle = document.getElementById("permissionsBundle");
+
+ sortAscending = (permissionsTree.getAttribute("sortAscending") == "true");
+ sortColumn = permissionsTree.getAttribute("sortColumn");
+
+ var params = { blockVisible : true,
+ sessionVisible : true,
+ allowVisible : true,
+ manageCapability : true
+ };
+
+ if (window.arguments && window.arguments[0]) {
+ params = window.arguments[0];
+ setHost(params.prefilledHost);
+ permissionType = params.permissionType;
+ gManageCapability = params.manageCapability;
+ introText = params.introText;
+ windowTitle = params.windowTitle;
+ }
+
+ document.getElementById("btnBlock").hidden = !params.blockVisible;
+ document.getElementById("btnSession").hidden = !params.sessionVisible;
+ document.getElementById("btnAllow").hidden = !params.allowVisible;
+
+ document.getElementById("permissionsText").textContent = introText ||
+ permissionsBundle.getString(permissionType + "permissionstext");
+
+ document.title = windowTitle ||
+ permissionsBundle.getString(permissionType + "permissionstitle");
+
+ var dialogElement = document.getElementById("permissionsManager");
+ dialogElement.setAttribute("windowtype", "permissions-" + permissionType);
+
+ var urlFieldVisible = params.blockVisible ||
+ params.sessionVisible ||
+ params.allowVisible;
+
+ document.getElementById("url").hidden = !urlFieldVisible;
+ document.getElementById("urlLabel").hidden = !urlFieldVisible;
+
+ handleHostInput(document.getElementById("url").value);
+ loadPermissions();
+}
+
+function onAccept() {
+ finalizeChanges();
+ reInitialize();
+
+ // Don't close the window.
+ return false;
+}
+
+function onCancel() {
+ reInitialize();
+
+ // Don't close the window.
+ return false;
+}
+
+function reInitialize() {
+ permissions = [];
+ removals = [];
+
+ // loadPermissions will reverse the sort direction so flip it now.
+ sortAscending = !sortAscending;
+
+ // Reload permissions tree.
+ loadPermissions();
+}
+
+function setHost(aHost) {
+ document.getElementById("url").value = aHost;
+}
+
+function Permission(id, principal, host, type, capability, perm) {
+ this.id = id;
+ this.principal = principal;
+ this.host = host;
+ this.rawHost = host.replace(/^\./, "");
+ this.type = type;
+ this.capability = capability;
+ this.perm = perm;
+}
+
+function loadPermissions() {
+ var enumerator = Services.perms.enumerator;
+ var count = 0;
+ var permission;
+
+ try {
+ while (enumerator.hasMoreElements()) {
+ permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
+ if (permission.type == permissionType &&
+ (!gManageCapability || permission.capability == gManageCapability)) {
+ permissions.push(new Permission(count++,
+ permission.principal,
+ permission.principal.URI.host,
+ permission.type,
+ capabilityString(permission.capability),
+ permission.capability));
+ }
+ }
+ } catch(ex) {
+ }
+
+ permissionsTreeView.rowCount = permissions.length;
+
+ // sort and display the table
+ permissionsTree.view = permissionsTreeView;
+ permissionColumnSort(sortColumn, false);
+
+ // disable "remove all" button if there are none
+ document.getElementById("removeAllPermissions").disabled =
+ permissions.length == 0;
+}
+
+function capabilityString(aCapability) {
+ var capability = null;
+ switch (aCapability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ capability = "can";
+ break;
+ case Ci.nsIPermissionManager.DENY_ACTION:
+ capability = "cannot";
+ break;
+ // we should only ever hit this for cookies
+ case Ci.nsICookiePermission.ACCESS_SESSION:
+ capability = "canSession";
+ break;
+ default:
+ break;
+ }
+ return permissionsBundle.getString(capability);
+}
+
+function permissionColumnSort(aColumn, aUpdateSelection) {
+ sortAscending =
+ SortTree(permissionsTree, permissionsTreeView, permissions,
+ aColumn, sortColumn, sortAscending, aUpdateSelection);
+ sortColumn = aColumn;
+
+ SetSortDirection(permissionsTree, aColumn, sortAscending);
+}
+
+function deletePermissions() {
+ DeleteSelectedItemFromTree(permissionsTree, permissionsTreeView,
+ permissions, removals,
+ "removePermission", "removeAllPermissions");
+}
+
+function deleteAllPermissions() {
+ DeleteAllFromTree(permissionsTree, permissionsTreeView, permissions,
+ removals, "removePermission", "removeAllPermissions");
+}
+
+function finalizeChanges() {
+ let p;
+
+ for (let i in permissions) {
+ p = permissions[i];
+ try {
+ // Principal is null so a permission we just added in this session.
+ if (p.principal == null) {
+ let uri = Services.io.newURI("https://" + p.host);
+ Services.perms.add(uri, p.type, p.perm);
+ }
+ } catch(ex) {
+ }
+ }
+
+ for (let i in removals) {
+ p = removals[i];
+ try {
+ // Principal is not null so not a permission we just added in this
+ // session.
+ if (p.principal) {
+ Services.perms.removeFromPrincipal(p.principal,
+ p.type);
+ }
+ } catch(ex) {
+ }
+ }
+}
+
+function handlePermissionKeyPress(e) {
+ if (e.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+ deletePermissions();
+ }
+}
+
+function addPermission(aPermission) {
+ var textbox = document.getElementById("url");
+ // trim any leading and trailing spaces and scheme
+ var host = trimSpacesAndScheme(textbox.value);
+ try {
+ let uri = Services.io.newURI("https://" + host);
+ host = uri.host;
+ } catch(ex) {
+ var message = permissionsBundle.getFormattedString("alertInvalid", [host]);
+ var title = permissionsBundle.getString("alertInvalidTitle");
+ Services.prompt.alert(window, title, message);
+ textbox.value = "";
+ textbox.focus();
+ handleHostInput("");
+ return;
+ }
+
+ // we need this whether the perm exists or not
+ var stringCapability = capabilityString(aPermission);
+
+ // check whether the permission already exists, if not, add it
+ var exists = false;
+ for (var i in permissions) {
+ if (permissions[i].rawHost == host) {
+ // Avoid calling the permission manager if the capability settings are
+ // the same. Otherwise allow the call to the permissions manager to
+ // update the listbox for us.
+ exists = permissions[i].perm == aPermission;
+ break;
+ }
+ }
+
+ if (!exists) {
+ permissions.push(new Permission(permissions.length, null, host,
+ permissionType, stringCapability,
+ aPermission));
+
+ permissionsTreeView.rowCount = permissions.length;
+ permissionsTree.treeBoxObject.rowCountChanged(permissions.length - 1, 1);
+ permissionsTree.treeBoxObject.ensureRowIsVisible(permissions.length - 1);
+ }
+ textbox.value = "";
+ textbox.focus();
+
+ // covers a case where the site exists already, so the buttons don't disable
+ handleHostInput("");
+
+ // enable "remove all" button as needed
+ document.getElementById("removeAllPermissions").disabled = permissions.length == 0;
+}
+
+function doHelpButton() {
+ openHelp(permissionsBundle.getString(permissionType + "permissionshelp"), "chrome://communicator/locale/help/suitehelp.rdf");
+ return true;
+}
diff --git a/comm/suite/components/permissions/content/permissionsManager.xul b/comm/suite/components/permissions/content/permissionsManager.xul
new file mode 100644
index 0000000000..4231fd1a96
--- /dev/null
+++ b/comm/suite/components/permissions/content/permissionsManager.xul
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/permissions/permissionsManager.dtd" >
+
+<dialog id="permissionsManager"
+ buttons="accept,cancel,help"
+ windowtype="exceptions"
+ title="&windowtitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="width:32em; height:42em;"
+ persist="width height screenX screenY"
+ onload="Startup();"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ ondialoghelp="return doHelpButton();">
+
+ <script src="chrome://communicator/content/permissions/permissionsManager.js"/>
+ <script src="chrome://communicator/content/permissions/permissionsUtils.js"/>
+ <script src="chrome://global/content/treeUtils.js"/>
+ <script src="chrome://help/content/contextHelp.js"/>
+
+ <stringbundle id="permissionsBundle"
+ src="chrome://communicator/locale/permissions/permissionsManager.properties"/>
+
+ <description id="permissionsText"/>
+ <separator class="thin"/>
+ <label id="urlLabel"
+ value="&address.label;"
+ accesskey="&address.accesskey;"
+ control="url"/>
+ <hbox align="start">
+ <textbox id="url" flex="1" oninput="handleHostInput(event.target.value);"/>
+ </hbox>
+ <hbox pack="end">
+ <button id="btnBlock" disabled="true" accesskey="&block.accesskey;"
+ label="&block.label;" oncommand="addPermission(Ci.nsIPermissionManager.DENY_ACTION);"/>
+ <button id="btnSession" disabled="true" accesskey="&session.accesskey;"
+ label="&session.label;" oncommand="addPermission(Ci.nsICookiePermission.ACCESS_SESSION);"/>
+ <button id="btnAllow" disabled="true" accesskey="&allow.accesskey;"
+ label="&allow.label;" oncommand="addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"/>
+ </hbox>
+ <separator class="thin"/>
+ <tree id="permissionsTree" flex="1" style="height: 18em;"
+ hidecolumnpicker="true"
+ onkeypress="handlePermissionKeyPress(event)"
+ onselect="PermissionSelected(this);"
+ sortAscending="false"
+ sortColumn="rawHost"
+ persist="sortAscending sortColumn">
+ <treecols>
+ <treecol id="rawHost"
+ label="&treehead.sitename.label;"
+ flex="3"
+ onclick="permissionColumnSort(this.id, true);"
+ sortDirection="descending"
+ persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="capability"
+ label="&treehead.status.label;"
+ flex="1"
+ onclick="permissionColumnSort(this.id, true);"
+ persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <separator class="thin"/>
+ <hbox>
+ <button id="removePermission" disabled="true"
+ label="&remove.label;" accesskey="&remove.accesskey;"
+ oncommand="deletePermissions();"/>
+ <button id="removeAllPermissions"
+ label="&removeall.label;" accesskey="&removeall.accesskey;"
+ oncommand="deleteAllPermissions();"/>
+ </hbox>
+</dialog>
diff --git a/comm/suite/components/permissions/content/permissionsUtils.js b/comm/suite/components/permissions/content/permissionsUtils.js
new file mode 100644
index 0000000000..4b208f87da
--- /dev/null
+++ b/comm/suite/components/permissions/content/permissionsUtils.js
@@ -0,0 +1,130 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function DeleteAllFromTree
+ (tree, view, table, deletedTable, removeButton, removeAllButton) {
+
+ gTreeUtils.deleteAll(tree, view, table, deletedTable);
+
+ // disable buttons
+ document.getElementById(removeButton).setAttribute("disabled", "true")
+ document.getElementById(removeAllButton).setAttribute("disabled","true");
+}
+
+function DeleteSelectedItemFromTree
+ (tree, view, table, deletedTable, removeButton, removeAllButton) {
+
+ gTreeUtils.deleteSelectedItems(tree, view, table, deletedTable);
+
+ // disable buttons if nothing left in the table
+ if (!table.length) {
+ document.getElementById(removeButton).setAttribute("disabled", "true")
+ document.getElementById(removeAllButton).setAttribute("disabled","true");
+ }
+}
+
+function GetTreeSelections(tree) {
+ var selections = [];
+ var select = tree.view.selection;
+ if (select) {
+ var count = select.getRangeCount();
+ var min = new Object();
+ var max = new Object();
+ for (var i=0; i<count; i++) {
+ select.getRangeAt(i, min, max);
+ for (var k=min.value; k<=max.value; k++) {
+ if (k != -1) {
+ selections[selections.length] = k;
+ }
+ }
+ }
+ }
+ return selections;
+}
+
+function SortTree(tree, view, table, column, lastSortColumn, lastSortAscending, updateSelection) {
+
+ // remember which item was selected so we can restore it after the sort
+ var selections = GetTreeSelections(tree);
+ var selectedNumber = selections.length ? table[selections[0]].id : -1;
+
+ // do the sort or re-sort
+ // this is a temporary hack for 1.7, we should implement
+ // display and sort variables here for trees in general
+ var sortColumn;
+ var comparator;
+ if (column == "expires") {
+ sortColumn = "expiresSortValue";
+ comparator = function compare(a, b) { return a - b; };
+ } else {
+ sortColumn = column;
+ comparator = function compare(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ };
+ }
+ if (lastSortColumn == "expires") {
+ lastSortColumn = "expiresSortValue";
+ }
+ var ascending = gTreeUtils.sort(tree, view, table, sortColumn, comparator,
+ lastSortColumn, lastSortAscending);
+
+ // restore the selection
+ if (selectedNumber >= 0 && updateSelection) {
+ var selectedRow = -1;
+ for (var s = 0; s < table.length; s++) {
+ if (table[s].id == selectedNumber) {
+ selectedRow = s;
+ break;
+ }
+ }
+
+ if (selectedRow > 0) {
+ // update selection and display the results
+ tree.view.selection.select(selectedRow);
+ tree.treeBoxObject.invalidate();
+ tree.treeBoxObject.ensureRowIsVisible(selectedRow);
+ }
+ }
+
+ return ascending;
+}
+
+function handleHostInput(aValue) {
+ // trim any leading and trailing spaces and scheme
+ // and set buttons appropiately
+ btnDisable(!trimSpacesAndScheme(aValue));
+}
+
+function trimSpacesAndScheme(aString) {
+ if (!aString)
+ return "";
+ return aString.trim().replace(/([-\w]*:\/+)?/, "");
+}
+
+function btnDisable(aDisabled) {
+ document.getElementById("btnSession").disabled = aDisabled;
+ document.getElementById("btnBlock").disabled = aDisabled;
+ document.getElementById("btnAllow").disabled = aDisabled;
+}
+
+function PermissionSelected(tree) {
+ var hasSelection = tree.view.selection.count > 0;
+ document.getElementById("removePermission").disabled = !hasSelection;
+}
+
+function SetSortDirection(tree, column, ascending) {
+ // first we need to get the right elements
+ for (let col of tree.getElementsByTagName("treecol")) {
+ if (col.id == column) {
+ // set the sortDirection attribute to get the styling going
+ col.setAttribute("sortDirection", ascending ? "ascending" : "descending");
+ }
+ else {
+ // clear out the sortDirection attribute on the rest of the columns
+ col.removeAttribute("sortDirection");
+ }
+ }
+}
diff --git a/comm/suite/components/permissions/jar.mn b/comm/suite/components/permissions/jar.mn
new file mode 100644
index 0000000000..15784fc80b
--- /dev/null
+++ b/comm/suite/components/permissions/jar.mn
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/permissions/cookieViewer.js (content/cookieViewer.js)
+ content/communicator/permissions/cookieViewer.xul (content/cookieViewer.xul)
+ content/communicator/permissions/permissionsManager.js (content/permissionsManager.js)
+ content/communicator/permissions/permissionsManager.xul (content/permissionsManager.xul)
+ content/communicator/permissions/permissionsUtils.js (content/permissionsUtils.js)
diff --git a/comm/suite/components/permissions/moz.build b/comm/suite/components/permissions/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/permissions/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/places/PlacesUIUtils.jsm b/comm/suite/components/places/PlacesUIUtils.jsm
new file mode 100644
index 0000000000..5440384212
--- /dev/null
+++ b/comm/suite/components/places/PlacesUIUtils.jsm
@@ -0,0 +1,1499 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["PlacesUIUtils"];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {clearTimeout, setTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+Cu.importGlobalProperties(["Element"]);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ RecentWindow: "resource:///modules/RecentWindow.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "bundle", function() {
+ return Services.strings.createBundle("chrome://communicator/locale/places/places.properties");
+});
+
+const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
+// Map from windows to arrays of data about pending favicon loads.
+var gFaviconLoadDataMap = new Map();
+
+const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10;
+
+// copied from utilityOverlay.js
+const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+
+var InternalFaviconLoader = {
+ /**
+ * This gets called for every inner window that is destroyed.
+ * In the parent process, we process the destruction ourselves. In the child process,
+ * we notify the parent which will then process it based on that message.
+ */
+ observe(subject, topic, data) {
+ let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ this.removeRequestsForInner(innerWindowID);
+ },
+
+ /**
+ * Actually cancel the request, and clear the timeout for cancelling it.
+ */
+ _cancelRequest({uri, innerWindowID, timerID, callback}, reason) {
+ // Break cycle
+ let request = callback.request;
+ delete callback.request;
+ // Ensure we don't time out.
+ clearTimeout(timerID);
+ try {
+ request.cancel();
+ } catch (ex) {
+ Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!");
+ }
+ },
+
+ /**
+ * Called for every inner that gets destroyed, only in the parent process.
+ */
+ removeRequestsForInner(innerID) {
+ for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
+ let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
+ let innerWasDestroyed = loadData.innerWindowID == innerID;
+ if (innerWasDestroyed) {
+ this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it");
+ }
+ // Keep the items whose inner is still alive.
+ return !innerWasDestroyed;
+ });
+ // Map iteration with for...of is safe against modification, so
+ // now just replace the old value:
+ gFaviconLoadDataMap.set(window, newLoadDataForWindow);
+ }
+ },
+
+ /**
+ * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
+ * avoid leaks, and cancel any remaining requests. The last part should in theory be
+ * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
+ */
+ onUnload(win) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ for (let loadData of loadDataForWindow) {
+ this._cancelRequest(loadData, "the chrome window went away");
+ }
+ }
+ gFaviconLoadDataMap.delete(win);
+ },
+
+ /**
+ * Remove a particular favicon load's loading data from our map tracking
+ * load data per chrome window.
+ *
+ * @param win
+ * the chrome window in which we should look for this load
+ * @param filterData ({innerWindowID, uri, callback})
+ * the data we should use to find this particular load to remove.
+ *
+ * @return the loadData object we removed, or null if we didn't find any.
+ */
+ _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ let itemIndex = loadDataForWindow.findIndex(loadData => {
+ return loadData.innerWindowID == innerWindowID &&
+ loadData.uri.equals(uri) &&
+ loadData.callback.request == callback.request;
+ });
+ if (itemIndex != -1) {
+ let loadData = loadDataForWindow[itemIndex];
+ loadDataForWindow.splice(itemIndex, 1);
+ return loadData;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
+ * information when the request succeeds. Note that right now there are some edge-cases,
+ * such as about: URIs with chrome:// favicons where the success callback is not invoked.
+ * This is OK: we will 'cancel' the request after the timeout (or when the window goes
+ * away) but that will be a no-op in such cases.
+ */
+ _makeCompletionCallback(win, id) {
+ return {
+ onComplete(uri) {
+ let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
+ uri,
+ innerWindowID: id,
+ callback: this,
+ });
+ if (loadData) {
+ clearTimeout(loadData.timerID);
+ }
+ delete this.request;
+ },
+ };
+ },
+
+ ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ Services.obs.addObserver(this, "inner-window-destroyed");
+ Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => {
+ this.removeRequestsForInner(msg.data);
+ });
+ },
+
+ loadFavicon(browser, principal, uri, requestContextID) {
+ this.ensureInitialized();
+ let win = browser.ownerGlobal;
+ if (!gFaviconLoadDataMap.has(win)) {
+ gFaviconLoadDataMap.set(win, []);
+ let unloadHandler = event => {
+ let doc = event.target;
+ let eventWin = doc.defaultView;
+ if (eventWin == win) {
+ win.removeEventListener("unload", unloadHandler);
+ this.onUnload(win);
+ }
+ };
+ win.addEventListener("unload", unloadHandler, true);
+ }
+
+ let {innerWindowID, currentURI} = browser;
+
+ // First we do the actual setAndFetch call:
+ let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
+ ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
+ : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
+ let callback = this._makeCompletionCallback(win, innerWindowID);
+ let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
+ loadType, callback, principal,
+ requestContextID);
+
+ // Now register the result so we can cancel it if/when necessary.
+ if (!request) {
+ // The favicon service can return with success but no-op (and leave request
+ // as null) if the icon is the same as the page (e.g. for images) or if it is
+ // the favicon for an error page. In this case, we do not need to do anything else.
+ return;
+ }
+ callback.request = request;
+ let loadData = {innerWindowID, uri, callback};
+ loadData.timerID = setTimeout(() => {
+ this._cancelRequest(loadData, "it timed out");
+ this._removeLoadDataFromWindowMap(win, loadData);
+ }, FAVICON_REQUEST_TIMEOUT);
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ loadDataForWindow.push(loadData);
+ },
+};
+
+var PlacesUIUtils = {
+ ORGANIZER_LEFTPANE_VERSION: 8,
+ ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
+ ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
+
+ LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
+ DESCRIPTION_ANNO: "bookmarkProperties/description",
+
+ /**
+ * Makes a URI from a spec, and do fixup
+ * @param aSpec
+ * The string spec of the URI
+ * @return A URI object for the spec.
+ */
+ createFixedURI: function PUIU_createFixedURI(aSpec) {
+ return Services.uriFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
+ },
+
+ getFormattedString: function PUIU_getFormattedString(key, params) {
+ return bundle.formatStringFromName(key, params, params.length);
+ },
+
+ /**
+ * Get a localized plural string for the specified key name and numeric value
+ * substituting parameters.
+ *
+ * @param aKey
+ * String, key for looking up the localized string in the bundle
+ * @param aNumber
+ * Number based on which the final localized form is looked up
+ * @param aParams
+ * Array whose items will substitute #1, #2,... #n parameters
+ * in the string.
+ *
+ * @see https://developer.mozilla.org/en/Localization_and_Plurals
+ * @return The localized plural string.
+ */
+ getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
+ let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
+
+ // Replace #1 with aParams[0], #2 with aParams[1], and so on.
+ return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) {
+ let param = aParams[parseInt(matchedNumber, 10) - 1];
+ return param !== undefined ? param : matchedId;
+ });
+ },
+
+ getString: function PUIU_getString(key) {
+ return bundle.GetStringFromName(key);
+ },
+
+ /**
+ * Shows the bookmark dialog corresponding to the specified info.
+ *
+ * @param aInfo
+ * Describes the item to be edited/added in the dialog.
+ * See documentation at the top of bookmarkProperties.js
+ * @param aWindow
+ * Owner window for the new dialog.
+ *
+ * @see documentation at the top of bookmarkProperties.js
+ * @return true if any transaction has been performed, false otherwise.
+ */
+ showBookmarkDialog(aInfo, aParentWindow) {
+ // Preserve size attributes differently based on the fact the dialog has
+ // a folder picker or not, since it needs more horizontal space than the
+ // other controls.
+ let hasFolderPicker = !("hiddenRows" in aInfo) ||
+ !aInfo.hiddenRows.includes("folderPicker");
+ // Use a different chrome url to persist different sizes.
+ let dialogURL = hasFolderPicker ?
+ "chrome://communicator/content/places/bookmarkProperties2.xul" :
+ "chrome://communicator/content/places/bookmarkProperties.xul";
+
+ let features = "centerscreen,chrome,modal,resizable=yes";
+
+ let topUndoEntry;
+ let batchBlockingDeferred;
+
+ // Set the transaction manager into batching mode.
+ topUndoEntry = PlacesTransactions.topUndoEntry;
+ batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(async () => {
+ await batchBlockingDeferred.promise;
+ });
+
+ aParentWindow.openDialog(dialogURL, "", features, aInfo);
+
+ let performed = ("performed" in aInfo && aInfo.performed);
+
+ batchBlockingDeferred.resolve();
+
+ if (!performed &&
+ topUndoEntry != PlacesTransactions.topUndoEntry) {
+ PlacesTransactions.undo().catch(Cu.reportError);
+ }
+
+ return performed;
+ },
+
+ /**
+ * set and fetch a favicon. Can only be used from the parent process.
+ * @param browser {Browser} The XUL browser element for which we're fetching a favicon.
+ * @param principal {Principal} The loading principal to use for the fetch.
+ * @param uri {URI} The URI to fetch.
+ */
+ loadFavicon(browser, principal, uri, requestContextID) {
+ if (gInContentProcess) {
+ throw new Error("Can't track loads from within the child process!");
+ }
+ InternalFaviconLoader.loadFavicon(browser, principal, uri, requestContextID);
+ },
+
+ /**
+ * Returns the closet ancestor places view for the given DOM node
+ * @param aNode
+ * a DOM node
+ * @return the closet ancestor places view if exists, null otherwsie.
+ */
+ getViewForNode: function PUIU_getViewForNode(aNode) {
+ let node = aNode;
+
+ if (node.localName == "panelview" && node._placesView) {
+ return node._placesView;
+ }
+
+ // The view for a <menu> of which its associated menupopup is a places
+ // view, is the menupopup.
+ if (node.localName == "menu" && !node._placesNode &&
+ node.lastChild._placesView)
+ return node.lastChild._placesView;
+
+ while (Element.isInstance(node)) {
+ if (node._placesView)
+ return node._placesView;
+ if (node.localName == "tree" && node.getAttribute("type") == "places")
+ return node;
+
+ node = node.parentNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the active PlacesController for a given command.
+ *
+ * @param win The window containing the affected view
+ * @param command The command
+ * @return a PlacesController
+ */
+ getControllerForCommand(win, command) {
+ // A context menu may be built for non-focusable views. Thus, we first try
+ // to look for a view associated with document.popupNode
+ let popupNode;
+ try {
+ popupNode = win.document.popupNode;
+ } catch (e) {
+ // The document went away (bug 797307).
+ return null;
+ }
+ if (popupNode) {
+ let view = this.getViewForNode(popupNode);
+ if (view && view._contextMenuShown)
+ return view.controllers.getControllerForCommand(command);
+ }
+
+ // When we're not building a context menu, only focusable views
+ // are possible. Thus, we can safely use the command dispatcher.
+ let controller = win.top.document.commandDispatcher
+ .getControllerForCommand(command);
+ return controller || null;
+ },
+
+ /**
+ * Update all the Places commands for the given window.
+ *
+ * @param win The window to update.
+ */
+ updateCommands(win) {
+ // Get the controller for one of the places commands.
+ let controller = this.getControllerForCommand(win, "placesCmd_open");
+ for (let command of [
+ "placesCmd_open",
+ "placesCmd_open:window",
+ "placesCmd_open:privatewindow",
+ "placesCmd_open:tab",
+ "placesCmd_new:folder",
+ "placesCmd_new:bookmark",
+ "placesCmd_new:separator",
+ "placesCmd_show:info",
+ "placesCmd_reload",
+ "placesCmd_sortBy:name",
+ "placesCmd_cut",
+ "placesCmd_copy",
+ "placesCmd_paste",
+ "placesCmd_delete",
+ ]) {
+ win.goSetCommandEnabled(command,
+ controller && controller.isCommandEnabled(command));
+ }
+ },
+
+ /**
+ * Executes the given command on the currently active controller.
+ *
+ * @param win The window containing the affected view
+ * @param command The command to execute
+ */
+ doCommand(win, command) {
+ let controller = this.getControllerForCommand(win, command);
+ if (controller && controller.isCommandEnabled(command))
+ controller.doCommand(command);
+ },
+
+ /**
+ * By calling this before visiting an URL, the visit will be associated to a
+ * TRANSITION_TYPED transition (if there is no a referrer).
+ * This is used when visiting pages from the history menu, history sidebar,
+ * url bar, url autocomplete results, and history searches from the places
+ * organizer. If this is not called visits will be marked as
+ * TRANSITION_LINK.
+ */
+ markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
+ PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL));
+ },
+
+ /**
+ * By calling this before visiting an URL, the visit will be associated to a
+ * TRANSITION_BOOKMARK transition.
+ * This is used when visiting pages from the bookmarks menu,
+ * personal toolbar, and bookmarks from within the places organizer.
+ * If this is not called visits will be marked as TRANSITION_LINK.
+ */
+ markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) {
+ PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL));
+ },
+
+ /**
+ * By calling this before visiting an URL, any visit in frames will be
+ * associated to a TRANSITION_FRAMED_LINK transition.
+ * This is actually used to distinguish user-initiated visits in frames
+ * so automatic visits can be correctly ignored.
+ */
+ markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) {
+ PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL));
+ },
+
+ /**
+ * Allows opening of javascript/data URI only if the given node is
+ * bookmarked (see bug 224521).
+ * @param aURINode
+ * a URI node
+ * @param aWindow
+ * a window on which a potential error alert is shown on.
+ * @return true if it's safe to open the node in the browser, false otherwise.
+ *
+ */
+ checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) {
+ if (PlacesUtils.nodeIsBookmark(aURINode))
+ return true;
+
+ var uri = Services.io.newURI(aURINode.uri);
+ if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
+ const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
+ var brandShortName = Services.strings
+ .createBundle(BRANDING_BUNDLE_URI)
+ .GetStringFromName("brandShortName");
+
+ var errorStr = this.getString("load-js-data-url-error");
+ Services.prompt.alert(aWindow, brandShortName, errorStr);
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Get the description associated with a document, as specified in a <META>
+ * element.
+ * @param doc
+ * A DOM Document to get a description for
+ * @return A description string if a META element was discovered with a
+ * "description" or "httpequiv" attribute, empty string otherwise.
+ */
+ getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
+ var metaElements = doc.getElementsByTagName("META");
+ for (var i = 0; i < metaElements.length; ++i) {
+ if (metaElements[i].name.toLowerCase() == "description" ||
+ metaElements[i].httpEquiv.toLowerCase() == "description") {
+ return metaElements[i].content;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Retrieve the description of an item
+ * @param aItemId
+ * item identifier
+ * @return the description of the given item, or an empty string if it is
+ * not set.
+ */
+ getItemDescription: function PUIU_getItemDescription(aItemId) {
+ if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
+ return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
+ return "";
+ },
+
+ /**
+ * Check whether or not the given node represents a removable entry (either in
+ * history or in bookmarks).
+ *
+ * @param aNode
+ * a node, except the root node of a query.
+ * @param aView
+ * The view originating the request.
+ * @return true if the aNode represents a removable entry, false otherwise.
+ */
+ canUserRemove(aNode, aView) {
+ let parentNode = aNode.parent;
+ if (!parentNode) {
+ // canUserRemove doesn't accept root nodes.
+ return false;
+ }
+
+ // Is it a query pointing to one of the special root folders?
+ if (PlacesUtils.nodeIsQuery(parentNode) && PlacesUtils.nodeIsFolder(aNode)) {
+ let guid = PlacesUtils.getConcreteItemGuid(aNode);
+ // If the parent folder is not a folder, it must be a query, and so this node
+ // cannot be removed.
+ if (PlacesUtils.isRootItem(guid)) {
+ return false;
+ }
+ }
+
+ // If it's not a bookmark, we can remove it unless it's a child of a
+ // livemark.
+ if (aNode.itemId == -1) {
+ // Rather than executing a db query, checking the existence of the feedURI
+ // annotation, detect livemark children by the fact that they are the only
+ // direct non-bookmark children of bookmark folders.
+ return !PlacesUtils.nodeIsFolder(parentNode);
+ }
+
+ // Generally it's always possible to remove children of a query.
+ if (PlacesUtils.nodeIsQuery(parentNode))
+ return true;
+
+ // Otherwise it has to be a child of an editable folder.
+ return !this.isFolderReadOnly(parentNode, aView);
+ },
+
+ /**
+ * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
+ * TO GUIDS IS COMPLETE (BUG 1071511).
+ *
+ * Check whether or not the given Places node points to a folder which
+ * should not be modified by the user (i.e. its children should be unremovable
+ * and unmovable, new children should be disallowed, etc).
+ * These semantics are not inherited, meaning that read-only folder may
+ * contain editable items (for instance, the places root is read-only, but all
+ * of its direct children aren't).
+ *
+ * You should only pass folder nodes.
+ *
+ * @param placesNode
+ * any folder result node.
+ * @param view
+ * The view originating the request.
+ * @throws if placesNode is not a folder result node or views is invalid.
+ * @note livemark "folders" are considered read-only (but see bug 1072833).
+ * @return true if placesNode is a read-only folder, false otherwise.
+ */
+ isFolderReadOnly(placesNode, view) {
+ if (typeof placesNode != "object" || !PlacesUtils.nodeIsFolder(placesNode)) {
+ throw new Error("invalid value for placesNode");
+ }
+ if (!view || typeof view != "object") {
+ throw new Error("invalid value for aView");
+ }
+ let itemId = PlacesUtils.getConcreteItemId(placesNode);
+ if (itemId == PlacesUtils.placesRootId ||
+ view.controller.hasCachedLivemarkInfo(placesNode))
+ return true;
+
+ // leftPaneFolderId is a lazy getter
+ // performing at least a synchronous DB query (and on its very first call
+ // in a fresh profile, it also creates the entire structure).
+ // Therefore we don't want to this function, which is called very often by
+ // isCommandEnabled, to ever be the one that invokes it first, especially
+ // because isCommandEnabled may be called way before the left pane folder is
+ // even created (for example, if the user only uses the bookmarks menu or
+ // toolbar for managing bookmarks). To do so, we avoid comparing to those
+ // special folder if the lazy getter is still in place. This is safe merely
+ // because the only way to access the left pane contents goes through
+ // "resolving" the leftPaneFolderId getter.
+ if (typeof Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").get == "function") {
+ return false;
+ }
+ return itemId == this.leftPaneFolderId;
+ },
+
+ /** aItemsToOpen needs to be an array of objects of the form:
+ * {uri: string, isBookmark: boolean}
+ */
+ _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) {
+ if (!aItemsToOpen.length)
+ return;
+
+ // Prefer the caller window if it's a browser window, otherwise use
+ // the top browser window.
+ var browserWindow = null;
+ browserWindow =
+ aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ?
+ aWindow : RecentWindow.getMostRecentBrowserWindow();
+
+ var urls = [];
+ let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow);
+ for (let item of aItemsToOpen) {
+ urls.push(item.uri);
+ if (skipMarking) {
+ continue;
+ }
+
+ if (item.isBookmark)
+ this.markPageAsFollowedBookmark(item.uri);
+ else
+ this.markPageAsTyped(item.uri);
+ }
+
+ // whereToOpenLink doesn't return "window" when there's no browser window
+ // open (Bug 630255).
+ var where = browserWindow ?
+ browserWindow.whereToOpenLink(aEvent, false, true) : "window";
+ if (where == "window") {
+ // There is no browser window open, thus open a new one.
+ var uriList = PlacesUtils.toISupportsString(urls.join("|"));
+ var args = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ args.appendElement(uriList);
+ browserWindow = Services.ww.openWindow(aWindow,
+ "chrome://navigator/content/navigator.xul",
+ null, "chrome,dialog=no,all", args);
+ return;
+ }
+
+ var loadInBackground = where == "tabshifted";
+ // For consistency, we want all the bookmarks to open in new tabs, instead
+ // of having one of them replace the currently focused tab. Hence we call
+ // loadTabs with aReplace set to false.
+ browserWindow.gBrowser.loadTabs(urls, {
+ inBackground: loadInBackground,
+ replace: false,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ openLiveMarkNodesInTabs:
+ function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) {
+ let window = aView.ownerWindow;
+
+ PlacesUtils.livemarks.getLivemark({id: aNode.itemId})
+ .then(aLivemark => {
+ let urlsToOpen = [];
+
+ let nodes = aLivemark.getNodesForContainer(aNode);
+ for (let node of nodes) {
+ urlsToOpen.push({uri: node.uri, isBookmark: false});
+ }
+
+ if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) {
+ this._openTabset(urlsToOpen, aEvent, window);
+ }
+ }, Cu.reportError);
+ },
+
+ openContainerNodeInTabs:
+ function PUIU_openContainerInTabs(aNode, aEvent, aView) {
+ let window = aView.ownerWindow;
+
+ let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
+ if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) {
+ this._openTabset(urlsToOpen, aEvent, window);
+ }
+ },
+
+ openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) {
+ let window = aView.ownerWindow;
+
+ let urlsToOpen = [];
+ for (var i = 0; i < aNodes.length; i++) {
+ // Skip over separators and folders.
+ if (PlacesUtils.nodeIsURI(aNodes[i]))
+ urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])});
+ }
+ this._openTabset(urlsToOpen, aEvent, window);
+ },
+
+ /**
+ * Loads the node's URL in the appropriate tab or window or as a web
+ * panel given the user's preference specified by modifier keys tracked by a
+ * DOM mouse/key event.
+ * @param aNode
+ * An uri result node.
+ * @param aEvent
+ * The DOM mouse/key event with modifier keys set that track the
+ * user's preferred destination window or tab.
+ * @param aExternal
+ * Called from the library window or an external application.
+ * Link handling for external applications will apply when true.
+ */
+ openNodeWithEvent:
+ function PUIU_openNodeWithEvent(aNode, aEvent, aExternal = false) {
+ let window = aEvent.target.ownerGlobal;
+ let whereTo;
+ if (aExternal) {
+ let openParms = window.whereToLoadExternalLink();
+ whereTo = openParms.where;
+ }
+ else {
+ whereTo = window.whereToOpenLink(aEvent, false, true);
+ }
+ this._openNodeIn(aNode, whereTo, window);
+ },
+
+ /**
+ * Loads the node's URL in the appropriate tab or window or as a
+ * web panel.
+ * see also openUILinkIn
+ */
+ openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
+ let window = aView.ownerWindow;
+ this._openNodeIn(aNode, aWhere, window, aPrivate);
+ },
+
+ _openNodeIn: function PUIU__openNodeIn(aNode, aWhere, aWindow, aPrivate = false) {
+ if (aNode && PlacesUtils.nodeIsURI(aNode) &&
+ this.checkURLSecurity(aNode, aWindow)) {
+ let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
+
+ if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ if (isBookmark)
+ this.markPageAsFollowedBookmark(aNode.uri);
+ else
+ this.markPageAsTyped(aNode.uri);
+ }
+
+ // Check whether the node is a bookmark which should be opened as
+ // a web panel
+ // Currently not supported in SeaMonkey. Please stay tuned.
+ // if (aWhere == "current" && isBookmark) {
+ // if (PlacesUtils.annotations
+ // .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) {
+ // let browserWin = this._getTopBrowserWin();
+ // if (browserWin) {
+ // browserWin.openWebPanel(aNode.title, aNode.uri);
+ // return;
+ // }
+ // }
+ // }
+
+ aWindow.openUILinkIn(aNode.uri, aWhere, {
+ allowPopups: aNode.uri.startsWith("javascript:"),
+ inBackground: Services.prefs.getBoolPref("browser.tabs.avoidBrowserFocus"),
+ aNoReferrer: true,
+ private: aPrivate,
+ });
+ }
+ },
+
+ /**
+ * Helper for guessing scheme from an url string.
+ * Used to avoid nsIURI overhead in frequently called UI functions.
+ *
+ * @param aUrlString the url to guess the scheme from.
+ *
+ * @return guessed scheme for this url string.
+ *
+ * @note this is not supposed be perfect, so use it only for UI purposes.
+ */
+ guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) {
+ return aUrlString.substr(0, aUrlString.indexOf(":"));
+ },
+
+ getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
+ var title;
+ if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) {
+ // if node title is empty, try to set the label using host and filename
+ // Services.io.newURI() will throw if aNode.uri is not a valid URI
+ try {
+ var uri = Services.io.newURI(aNode.uri);
+ var host = uri.host;
+ var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
+ // if fileName is empty, use path to distinguish labels
+ if (aDoNotCutTitle) {
+ title = host + uri.pathQueryRef;
+ } else {
+ title = host + (fileName ?
+ (host ? "/" + this.ellipsis + "/" : "") + fileName :
+ uri.pathQueryRef);
+ }
+ } catch (e) {
+ // Use (no title) for non-standard URIs (data:, javascript:, ...)
+ title = "";
+ }
+ } else
+ title = aNode.title;
+
+ return title || this.getString("noTitle");
+ },
+
+ get leftPaneQueries() {
+ // build the map
+ this.leftPaneFolderId;
+ return this.leftPaneQueries;
+ },
+
+ get leftPaneFolderId() {
+ delete this.leftPaneFolderId;
+ return this.leftPaneFolderId = this.maybeRebuildLeftPane();
+ },
+
+ // Get the folder id for the organizer left-pane folder.
+ maybeRebuildLeftPane() {
+ let leftPaneRoot = -1;
+
+ // Shortcuts to services.
+ let bs = PlacesUtils.bookmarks;
+ let as = PlacesUtils.annotations;
+
+ // This is the list of the left pane queries.
+ let queries = {
+ "PlacesRoot": { title: "" },
+ "History": { title: this.getString("OrganizerQueryHistory") },
+ "Tags": { title: this.getString("OrganizerQueryTags") },
+ "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") },
+ };
+ // All queries but PlacesRoot.
+ const EXPECTED_QUERY_COUNT = 3;
+
+ // Removes an item and associated annotations, ignoring eventual errors.
+ function safeRemoveItem(aItemId) {
+ try {
+ if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) &&
+ !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) {
+ // Some extension annotated their roots with our query annotation,
+ // so we should not delete them.
+ return;
+ }
+ // removeItemAnnotation does not check if item exists, nor the anno,
+ // so this is safe to do.
+ as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ // This will throw if the annotation is an orphan.
+ bs.removeItem(aItemId);
+ } catch (e) { /* orphan anno */ }
+ }
+
+ // Returns true if item really exists, false otherwise.
+ function itemExists(aItemId) {
+ try {
+ bs.getFolderIdForItem(aItemId);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // Get all items marked as being the left pane folder.
+ let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO);
+ if (items.length > 1) {
+ // Something went wrong, we cannot have more than one left pane folder,
+ // remove all left pane folders and continue. We will create a new one.
+ items.forEach(safeRemoveItem);
+ } else if (items.length == 1 && items[0] != -1) {
+ leftPaneRoot = items[0];
+ // Check that organizer left pane root is valid.
+ let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO);
+ if (version != this.ORGANIZER_LEFTPANE_VERSION ||
+ !itemExists(leftPaneRoot)) {
+ // Invalid root, we must rebuild the left pane.
+ safeRemoveItem(leftPaneRoot);
+ leftPaneRoot = -1;
+ }
+ }
+
+ if (leftPaneRoot != -1) {
+ // A valid left pane folder has been found.
+ // Build the leftPaneQueries Map. This is used to quickly access them,
+ // associating a mnemonic name to the real item ids.
+ delete this.leftPaneQueries;
+ this.leftPaneQueries = {};
+
+ let queryItems = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO);
+ // While looping through queries we will also check for their validity.
+ let queriesCount = 0;
+ let corrupt = false;
+ for (let i = 0; i < queryItems.length; i++) {
+ let queryName = as.getItemAnnotation(queryItems[i], this.ORGANIZER_QUERY_ANNO);
+
+ // Some extension did use our annotation to decorate their items
+ // with icons, so we should check only our elements, to avoid dataloss.
+ if (!(queryName in queries))
+ continue;
+
+ let query = queries[queryName];
+ query.itemId = queryItems[i];
+
+ if (!itemExists(query.itemId)) {
+ // Orphan annotation, bail out and create a new left pane root.
+ corrupt = true;
+ break;
+ }
+
+ // Check that all queries have valid parents.
+ let parentId = bs.getFolderIdForItem(query.itemId);
+ if (!queryItems.includes(parentId) && parentId != leftPaneRoot) {
+ // The parent is not part of the left pane, bail out and create a new
+ // left pane root.
+ corrupt = true;
+ break;
+ }
+
+ // Titles could have been corrupted or the user could have changed his
+ // locale. Check title and eventually fix it.
+ if (bs.getItemTitle(query.itemId) != query.title)
+ bs.setItemTitle(query.itemId, query.title);
+ if ("concreteId" in query) {
+ if (bs.getItemTitle(query.concreteId) != query.concreteTitle)
+ bs.setItemTitle(query.concreteId, query.concreteTitle);
+ }
+
+ // Add the query to our cache.
+ this.leftPaneQueries[queryName] = query.itemId;
+ queriesCount++;
+ }
+
+ // Note: it's not enough to just check for queriesCount, since we may
+ // find an invalid query just after accounting for a sufficient number of
+ // valid ones. As well as we can't just rely on corrupt since we may find
+ // less valid queries than expected.
+ if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) {
+ // Queries number is wrong, so the left pane must be corrupt.
+ // Note: we can't just remove the leftPaneRoot, because some query could
+ // have a bad parent, so we have to remove all items one by one.
+ queryItems.forEach(safeRemoveItem);
+ safeRemoveItem(leftPaneRoot);
+ } else {
+ // Everything is fine, return the current left pane folder.
+ return leftPaneRoot;
+ }
+ }
+
+ // Create a new left pane folder.
+ var callback = {
+ // Helper to create an organizer special query.
+ create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
+ let itemId = bs.insertBookmark(aParentId,
+ Services.io.newURI(aQueryUrl),
+ bs.DEFAULT_INDEX,
+ queries[aQueryName].title);
+ // Mark as special organizer query.
+ as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName,
+ 0, as.EXPIRE_NEVER);
+ // We should never backup this, since it changes between profiles.
+ as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
+ 0, as.EXPIRE_NEVER);
+ // Add to the queries map.
+ PlacesUIUtils.leftPaneQueries[aQueryName] = itemId;
+ return itemId;
+ },
+
+ // Helper to create an organizer special folder.
+ create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) {
+ // Left Pane Root Folder.
+ let folderId = bs.createFolder(aParentId,
+ queries[aFolderName].title,
+ bs.DEFAULT_INDEX);
+ // We should never backup this, since it changes between profiles.
+ as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
+ 0, as.EXPIRE_NEVER);
+
+ if (aIsRoot) {
+ // Mark as special left pane root.
+ as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
+ 0, as.EXPIRE_NEVER);
+ } else {
+ // Mark as special organizer folder.
+ as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName,
+ 0, as.EXPIRE_NEVER);
+ PlacesUIUtils.leftPaneQueries[aFolderName] = folderId;
+ }
+ return folderId;
+ },
+
+ runBatched: function CB_runBatched(aUserData) {
+ delete PlacesUIUtils.leftPaneQueries;
+ PlacesUIUtils.leftPaneQueries = { };
+
+ // Left Pane Root Folder.
+ leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true);
+
+ // History Query.
+ this.create_query("History", leftPaneRoot,
+ "place:type=" +
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
+ "&sort=" +
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
+
+ // Tags Query.
+ this.create_query("Tags", leftPaneRoot,
+ "place:type=" +
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
+ "&sort=" +
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
+
+ // All Bookmarks Folder.
+ this.create_query("AllBookmarks", leftPaneRoot,
+ "place:type=" +
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY);
+ }
+ };
+ bs.runInBatchMode(callback, null);
+
+ return leftPaneRoot;
+ },
+
+ /**
+ * If an item is a left-pane query, returns the name of the query
+ * or an empty string if not.
+ *
+ * @param aItemId id of a container
+ * @return the name of the query, or empty string if not a left-pane query
+ */
+ getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
+ var queryName = "";
+ // If the let pane hasn't been built, use the annotation service
+ // directly, to avoid building the left pane too early.
+ if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) {
+ try {
+ queryName = PlacesUtils.annotations.
+ getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO);
+ } catch (ex) {
+ // doesn't have the annotation
+ queryName = "";
+ }
+ } else {
+ // If the left pane has already been built, use the name->id map
+ // cached in PlacesUIUtils.
+ for (let [name, id] of Object.entries(this.leftPaneQueries)) {
+ if (aItemId == id)
+ queryName = name;
+ }
+ }
+ return queryName;
+ },
+
+ shouldShowTabsFromOtherComputersMenuitem() {
+ let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
+ Weave.Svc.Prefs.get("firstSync", "") != "notReady";
+ return weaveOK;
+ },
+
+ /**
+ * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
+ * FUTURE RELEASE.
+ *
+ * Checks if a place: href represents a folder shortcut.
+ *
+ * @param queryString
+ * the query string to check (a place: href)
+ * @return whether or not queryString represents a folder shortcut.
+ * @throws if queryString is malformed.
+ */
+ isFolderShortcutQueryString(queryString) {
+ // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
+
+ let queriesParam = { }, optionsParam = { };
+ PlacesUtils.history.queryStringToQueries(queryString,
+ queriesParam,
+ { },
+ optionsParam);
+ let queries = queries.value;
+ if (queries.length == 0)
+ throw new Error(`Invalid place: uri: ${queryString}`);
+ return queries.length == 1 &&
+ queries[0].folderCount == 1 &&
+ !queries[0].hasBeginTime &&
+ !queries[0].hasEndTime &&
+ !queries[0].hasDomain &&
+ !queries[0].hasURI &&
+ !queries[0].hasSearchTerms &&
+ !queries[0].tags.length == 0 &&
+ optionsParam.value.maxResults == 0;
+ },
+
+ /**
+ * @see showAddBookmarkUI
+ * This opens the dialog with only the name and folder pickers visible by
+ * default.
+ *
+ * This is to be used only outside of the SeaMonkey browser part e.g. for
+ * bookmarking in mail and news windows.
+ *
+ * You can still pass in the various paramaters as the default properties
+ * for the new bookmark.
+ *
+ * The keyword field will be visible only if the aKeyword parameter
+ * was used.
+ */
+ showMinimalAddBookmarkUI:
+ function PUIU_showMinimalAddBookmarkUI(aURI, aTitle, aDescription,
+ aDefaultInsertionPoint, aShowPicker,
+ aLoadInSidebar, aKeyword, aPostData,
+ aCharSet) {
+ var info = {
+ action: "add",
+ type: "bookmark",
+ hiddenRows: ["description"]
+ };
+ if (aURI)
+ info.uri = aURI;
+
+ // allow default empty title
+ if (typeof(aTitle) == "string")
+ info.title = aTitle;
+
+ if (aDescription)
+ info.description = aDescription;
+
+ if (aDefaultInsertionPoint) {
+ info.defaultInsertionPoint = aDefaultInsertionPoint;
+ if (!aShowPicker)
+ info.hiddenRows.push("folderPicker");
+ }
+
+ info.hiddenRows = info.hiddenRows.concat(["location"]);
+
+ if (typeof(aKeyword) == "string") {
+ info.keyword = aKeyword;
+ // Hide the Tags field if we are adding a keyword.
+ info.hiddenRows.push("tags");
+ // Keyword related params.
+ if (typeof(aPostData) == "string")
+ info.postData = aPostData;
+ if (typeof(aCharSet) == "string")
+ info.charSet = aCharSet;
+ }
+ else
+ info.hiddenRows.push("keyword");
+
+ return this.showBookmarkDialog(info,
+ focusManager.activeWindow ||
+ Services.wm.getMostRecentWindow(null));
+ },
+
+ /**
+ * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+ *
+ * Given a bookmark object for either a url bookmark or a folder, returned by
+ * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
+ * initialising the edit overlay with it.
+ *
+ * @param aFetchInfo
+ * a bookmark object returned by Bookmarks.fetch.
+ * @return a node-like object suitable for initialising editBookmarkOverlay.
+ * @throws if aFetchInfo is representing a separator.
+ */
+ async promiseNodeLikeFromFetchInfo(aFetchInfo) {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR)
+ throw new Error("promiseNodeLike doesn't support separators");
+
+ let parent = {
+ itemId: await PlacesUtils.promiseItemId(aFetchInfo.parentGuid),
+ bookmarkGuid: aFetchInfo.parentGuid,
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+ };
+
+ return Object.freeze({
+ itemId: await PlacesUtils.promiseItemId(aFetchInfo.guid),
+ bookmarkGuid: aFetchInfo.guid,
+ title: aFetchInfo.title,
+ uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
+
+ get type() {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+ if (this.uri.length == 0)
+ throw new Error("Unexpected item type");
+
+ if (/^place:/.test(this.uri)) {
+ if (this.isFolderShortcutQueryString(this.uri))
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ }
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ },
+
+ get parent() {
+ return parent;
+ }
+ });
+ },
+
+ /**
+ * This function wraps potentially large places transaction operations
+ * with batch notifications to the result node, hence switching the views
+ * to batch mode.
+ *
+ * @param {nsINavHistoryResult} resultNode The result node to turn on batching.
+ * @note If resultNode is not supplied, the function will pass-through to
+ * functionToWrap.
+ * @param {Integer} itemsBeingChanged The count of items being changed. If the
+ * count is lower than a threshold, then
+ * batching won't be set.
+ * @param {Function} functionToWrap The function to
+ */
+ async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) {
+ if (!resultNode) {
+ await functionToWrap();
+ return;
+ }
+
+ resultNode = resultNode.QueryInterface(Ci.nsINavBookmarkObserver);
+
+ if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
+ resultNode.onBeginUpdateBatch();
+ }
+
+ try {
+ await functionToWrap();
+ } finally {
+ if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
+ resultNode.onEndUpdateBatch();
+ }
+ }
+ },
+
+ /**
+ * Constructs a Places Transaction for the drop or paste of a blob of data
+ * into a container.
+ *
+ * @param aData
+ * The unwrapped data blob of dropped or pasted data.
+ * @param aNewParentGuid
+ * GUID of the container the data was dropped or pasted into.
+ * @param aIndex
+ * The index within the container the item was dropped or pasted at.
+ * @param aCopy
+ * The drag action was copy, so don't move folders or links.
+ *
+ * @return a Places Transaction that can be transacted for performing the
+ * move/insert command.
+ */
+ getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) {
+ if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+ throw new Error(`Unsupported '${aData.type}' data type`);
+
+ if ("itemGuid" in aData && "instanceId" in aData &&
+ aData.instanceId == PlacesUtils.instanceId) {
+ if (!this.PLACES_FLAVORS.includes(aData.type))
+ throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
+
+ let info = { guid: aData.itemGuid,
+ newParentGuid: aNewParentGuid,
+ newIndex: aIndex };
+ if (aCopy) {
+ info.excludingAnnotation = "Places/SmartBookmark";
+ return PlacesTransactions.Copy(info);
+ }
+ return PlacesTransactions.Move(info);
+ }
+
+ // Since it's cheap and harmless, we allow the paste of separators and
+ // bookmarks from builds that use legacy transactions (i.e. when itemGuid
+ // was not set on PLACES_FLAVORS data). Containers are a different story,
+ // and thus disallowed.
+ if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
+ throw new Error("Can't copy a container from a legacy-transactions build");
+
+ if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+ return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid,
+ index: aIndex });
+ }
+
+ let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
+ : aData.uri;
+ return PlacesTransactions.NewBookmark({ url: Services.io.newURI(aData.uri),
+ title,
+ parentGuid: aNewParentGuid,
+ index: aIndex });
+ },
+
+ /**
+ * Processes a set of transfer items that have been dropped or pasted.
+ * Batching will be applied where necessary.
+ *
+ * @param {Array} items A list of unwrapped nodes to process.
+ * @param {Object} insertionPoint The requested point for insertion.
+ * @param {Boolean} doCopy Set to true to copy the items, false will move them
+ * if possible.
+ * @paramt {Object} view The view that should be used for batching.
+ * @return {Array} Returns an empty array when the insertion point is a tag, else
+ * returns an array of copied or moved guids.
+ */
+ async handleTransferItems(items, insertionPoint, doCopy, view) {
+ let transactions;
+ let itemsCount;
+ if (insertionPoint.isTag) {
+ let urls = items.filter(item => "uri" in item).map(item => item.uri);
+ itemsCount = urls.length;
+ transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })];
+ } else {
+ let insertionIndex = await insertionPoint.getIndex();
+ itemsCount = items.length;
+ transactions = await getTransactionsForTransferItems(
+ items, insertionIndex, insertionPoint.guid, doCopy);
+ }
+
+ // Check if we actually have something to add, if we don't it probably wasn't
+ // valid, or it was moving to the same location, so just ignore it.
+ if (!transactions.length) {
+ return [];
+ }
+
+ let guidsToSelect = [];
+ let resultForBatching = getResultForBatching(view);
+
+ // If we're inserting into a tag, we don't get the guid, so we'll just
+ // pass the transactions direct to the batch function.
+ let batchingItem = transactions;
+ if (!insertionPoint.isTag) {
+ // If we're not a tag, then we need to get the ids of the items to select.
+ batchingItem = async () => {
+ for (let transaction of transactions) {
+ let guid = await transaction.transact();
+ if (guid) {
+ guidsToSelect.push(guid);
+ }
+ }
+ };
+ }
+
+ await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
+ await PlacesTransactions.batch(batchingItem);
+ });
+
+ return guidsToSelect;
+ },
+};
+
+// These are lazy getters to avoid importing PlacesUtils immediately.
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => {
+ return [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+ PlacesUtils.TYPE_X_MOZ_PLACE];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => {
+ return [PlacesUtils.TYPE_X_MOZ_URL,
+ TAB_DROP_TYPE,
+ PlacesUtils.TYPE_UNICODE];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => {
+ return [...PlacesUIUtils.PLACES_FLAVORS,
+ ...PlacesUIUtils.URI_FLAVORS];
+});
+
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
+ return Services.prefs.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
+ "@mozilla.org/focus-manager;1",
+ "nsIFocusManager");
+
+/**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param unwrappedNode
+ * A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+function canMoveUnwrappedNode(unwrappedNode) {
+ if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
+ unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
+ return false;
+ }
+
+ let parentGuid = unwrappedNode.parentGuid;
+ // If there's no parent Guid, this was likely a virtual query that returns
+ // bookmarks, such as a tags query.
+ if (!parentGuid ||
+ parentGuid == PlacesUtils.bookmarks.rootGuid) {
+ return false;
+ }
+ // leftPaneFolderId and allBookmarksFolderId are lazy getters running
+ // at least a synchronous DB query. Therefore we don't want to invoke
+ // them first, especially because isCommandEnabled may be called way
+ // before the left pane folder is even necessary.
+ if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
+ (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * This gets the most appropriate item for using for batching. In the case of multiple
+ * views being related, the method returns the most expensive result to batch.
+ * For example, if it detects the left-hand library pane, then it will look for
+ * and return the reference to the right-hand pane.
+ *
+ * @param {Object} viewOrElement The item to check.
+ * @return {Object} Will return the best result node to batch, or null
+ * if one could not be found.
+ */
+function getResultForBatching(viewOrElement) {
+ if (viewOrElement && Element.isInstance(viewOrElement) &&
+ viewOrElement.id === "placesList") {
+ // Note: fall back to the existing item if we can't find the right-hane pane.
+ viewOrElement = viewOrElement.ownerDocument.getElementById("placeContent") || viewOrElement;
+ }
+
+ if (viewOrElement && viewOrElement.result) {
+ return viewOrElement.result;
+ }
+
+ return null;
+}
+
+/**
+ * Processes a set of transfer items and returns transactions to insert or
+ * move them.
+ *
+ * @param {Array} items A list of unwrapped nodes to get transactions for.
+ * @param {Integer} insertionIndex The requested index for insertion.
+ * @param {String} insertionParentGuid The guid of the parent folder to insert
+ * or move the items to.
+ * @param {Boolean} doCopy Set to true to copy the items, false will move them
+ * if possible.
+ * @return {Array} Returns an array of created PlacesTransactions.
+ */
+async function getTransactionsForTransferItems(items, insertionIndex,
+ insertionParentGuid, doCopy) {
+ let transactions = [];
+ let index = insertionIndex;
+
+ for (let item of items) {
+ if (index != -1 && item.itemGuid) {
+ // Note: we use the parent from the existing bookmark as the sidebar
+ // gives us an unwrapped.parent that is actually a query and not the real
+ // parent.
+ let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid);
+
+ // If we're dropping on the same folder, then we may need to adjust
+ // the index to insert at the correct place.
+ if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) {
+ if (index > existingBookmark.index) {
+ // If we're dragging down, we need to go one lower to insert at
+ // the real point as moving the element changes the index of
+ // everything below by 1.
+ index--;
+ } else if (index == existingBookmark.index) {
+ // This isn't moving so we skip it.
+ continue;
+ }
+ }
+ }
+
+ // If this is not a copy, check for safety that we can move the
+ // source, otherwise report an error and fallback to a copy.
+ if (!doCopy && !canMoveUnwrappedNode(item)) {
+ Cu.reportError("Tried to move an unmovable Places " +
+ "node, reverting to a copy operation.");
+ doCopy = true;
+ }
+ transactions.push(
+ PlacesUIUtils.getTransactionForData(item,
+ insertionParentGuid,
+ index,
+ doCopy));
+
+ if (index != -1 && item.itemGuid) {
+ index++;
+ }
+ }
+ return transactions;
+}
diff --git a/comm/suite/components/places/content/bookmarkProperties.js b/comm/suite/components/places/content/bookmarkProperties.js
new file mode 100644
index 0000000000..8078a4e9a7
--- /dev/null
+++ b/comm/suite/components/places/content/bookmarkProperties.js
@@ -0,0 +1,524 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The panel is initialized based on data given in the js object passed
+ * as window.arguments[0]. The object must have the following fields set:
+ * @ action (String). Possible values:
+ * - "add" - for adding a new item.
+ * @ type (String). Possible values:
+ * - "bookmark"
+ * @ loadBookmarkInSidebar - optional, the default state for the
+ * "Load this bookmark in the sidebar" field.
+ * - "folder"
+ * @ URIList (Array of nsIURI objects) - optional, list of uris to
+ * be bookmarked under the new folder.
+ * - "livemark"
+ * @ uri (nsIURI object) - optional, the default uri for the new item.
+ * The property is not used for the "folder with items" type.
+ * @ title (String) - optional, the default title for the new item.
+ * @ description (String) - optional, the default description for the new
+ * item.
+ * @ defaultInsertionPoint (InsertionPoint JS object) - optional, the
+ * default insertion point for the new item.
+ * @ keyword (String) - optional, the default keyword for the new item.
+ * @ postData (String) - optional, POST data to accompany the keyword.
+ * @ charSet (String) - optional, character-set to accompany the keyword.
+ * Notes:
+ * 1) If |uri| is set for a bookmark/livemark item and |title| isn't,
+ * the dialog will query the history tables for the title associated
+ * with the given uri. If the dialog is set to adding a folder with
+ * bookmark items under it (see URIList), a default static title is
+ * used ("[Folder Name]").
+ * 2) The index field of the default insertion point is ignored if
+ * the folder picker is shown.
+ * - "edit" - for editing a bookmark item or a folder.
+ * @ type (String). Possible values:
+ * - "bookmark"
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * the bookmark.
+ * - "folder" (also applies to livemarks)
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * the folder.
+ * @ hiddenRows (Strings array) - optional, list of rows to be hidden
+ * regardless of the item edited or added by the dialog.
+ * Possible values:
+ * - "title"
+ * - "location"
+ * - "description"
+ * - "keyword"
+ * - "tags"
+ * - "loadInSidebar"
+ * - "folderPicker" - hides both the tree and the menu.
+ *
+ * window.arguments[0].performed is set to true if any transaction has
+ * been performed by the dialog.
+ */
+
+/* import-globals-from editBookmarkOverlay.js */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const BOOKMARK_ITEM = 0;
+const BOOKMARK_FOLDER = 1;
+const LIVEMARK_CONTAINER = 2;
+
+const ACTION_EDIT = 0;
+const ACTION_ADD = 1;
+
+var elementsHeight = new Map();
+
+var BookmarkPropertiesPanel = {
+
+ /** UI Text Strings */
+ __strings: null,
+ get _strings() {
+ if (!this.__strings) {
+ this.__strings = document.getElementById("stringBundle");
+ }
+ return this.__strings;
+ },
+
+ _action: null,
+ _itemType: null,
+ _itemId: -1,
+ _uri: null,
+ _loadInSidebar: false,
+ _title: "",
+ _description: "",
+ _URIs: [],
+ _keyword: "",
+ _postData: null,
+ _charSet: "",
+ _feedURI: null,
+ _siteURI: null,
+
+ _defaultInsertionPoint: null,
+ _hiddenRows: [],
+
+ /**
+ * This method returns the correct label for the dialog's "accept"
+ * button based on the variant of the dialog.
+ */
+ _getAcceptLabel: function BPP__getAcceptLabel() {
+ if (this._action == ACTION_ADD) {
+ if (this._URIs.length)
+ return this._strings.getString("dialogAcceptLabelAddMulti");
+
+ if (this._itemType == LIVEMARK_CONTAINER)
+ return this._strings.getString("dialogAcceptLabelAddLivemark");
+
+ if (this._dummyItem || this._loadInSidebar)
+ return this._strings.getString("dialogAcceptLabelAddItem");
+
+ return this._strings.getString("dialogAcceptLabelSaveItem");
+ }
+ return this._strings.getString("dialogAcceptLabelEdit");
+ },
+
+ /**
+ * This method returns the correct title for the current variant
+ * of this dialog.
+ */
+ _getDialogTitle: function BPP__getDialogTitle() {
+ if (this._action == ACTION_ADD) {
+ if (this._itemType == BOOKMARK_ITEM)
+ return this._strings.getString("dialogTitleAddBookmark");
+ if (this._itemType == LIVEMARK_CONTAINER)
+ return this._strings.getString("dialogTitleAddLivemark");
+
+ // add folder
+ if (this._itemType != BOOKMARK_FOLDER)
+ throw new Error("Unknown item type");
+ if (this._URIs.length)
+ return this._strings.getString("dialogTitleAddMulti");
+
+ return this._strings.getString("dialogTitleAddFolder");
+ }
+ if (this._action == ACTION_EDIT) {
+ return this._strings.getFormattedString("dialogTitleEdit", [this._title]);
+ }
+ return "";
+ },
+
+ /**
+ * Determines the initial data for the item edited or added by this dialog
+ */
+ async _determineItemInfo() {
+ let dialogInfo = window.arguments[0];
+ this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
+ this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
+ if (this._action == ACTION_ADD) {
+ if (!("type" in dialogInfo))
+ throw new Error("missing type property for add action");
+
+ if ("title" in dialogInfo)
+ this._title = dialogInfo.title;
+
+ if ("defaultInsertionPoint" in dialogInfo) {
+ this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
+ } else {
+ this._defaultInsertionPoint =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.bookmarksMenuFolderId,
+ parentGuid: PlacesUtils.bookmarks.menuGuid
+ });
+ }
+
+ switch (dialogInfo.type) {
+ case "bookmark":
+ this._itemType = BOOKMARK_ITEM;
+ if ("uri" in dialogInfo) {
+ if (!(dialogInfo.uri instanceof Ci.nsIURI))
+ throw new Error("uri property should be a uri object");
+ this._uri = dialogInfo.uri;
+ if (typeof(this._title) != "string") {
+ this._title = await PlacesUtils.history.fetch(this._uri) ||
+ this._uri.spec;
+ }
+ } else {
+ this._uri = Services.io.newURI("about:blank");
+ this._title = this._strings.getString("newBookmarkDefault");
+ this._dummyItem = true;
+ }
+
+ if ("loadBookmarkInSidebar" in dialogInfo)
+ this._loadInSidebar = dialogInfo.loadBookmarkInSidebar;
+
+ if ("keyword" in dialogInfo) {
+ this._keyword = dialogInfo.keyword;
+ this._isAddKeywordDialog = true;
+ if ("postData" in dialogInfo)
+ this._postData = dialogInfo.postData;
+ if ("charSet" in dialogInfo)
+ this._charSet = dialogInfo.charSet;
+ }
+ break;
+
+ case "folder":
+ this._itemType = BOOKMARK_FOLDER;
+ if (!this._title) {
+ if ("URIList" in dialogInfo) {
+ this._title = this._strings.getString("bookmarkAllTabsDefault");
+ this._URIs = dialogInfo.URIList;
+ } else
+ this._title = this._strings.getString("newFolderDefault");
+ this._dummyItem = true;
+ }
+ break;
+
+ case "livemark":
+ this._itemType = LIVEMARK_CONTAINER;
+ if ("feedURI" in dialogInfo)
+ this._feedURI = dialogInfo.feedURI;
+ if ("siteURI" in dialogInfo)
+ this._siteURI = dialogInfo.siteURI;
+
+ if (!this._title) {
+ if (this._feedURI) {
+ this._title = await PlacesUtils.history.fetch(this._feedURI) ||
+ this._feedURI.spec;
+ } else
+ this._title = this._strings.getString("newLivemarkDefault");
+ }
+ }
+
+ if ("description" in dialogInfo)
+ this._description = dialogInfo.description;
+ } else { // edit
+ this._node = dialogInfo.node;
+ this._title = this._node.title;
+ if (PlacesUtils.nodeIsFolder(this._node))
+ this._itemType = BOOKMARK_FOLDER;
+ else if (PlacesUtils.nodeIsURI(this._node))
+ this._itemType = BOOKMARK_ITEM;
+ }
+ },
+
+ /**
+ * This method should be called by the onload of the Bookmark Properties
+ * dialog to initialize the state of the panel.
+ */
+ async onDialogLoad() {
+ await this._determineItemInfo();
+
+ document.title = this._getDialogTitle();
+
+ // Disable the buttons until we have all the information required.
+ let acceptButton = document.documentElement.getButton("accept");
+ acceptButton.disabled = true;
+
+ // Allow initialization to complete in a truely async manner so that we're
+ // not blocking the main thread.
+ this._initDialog().catch(ex => {
+ Cu.reportError(`Failed to initialize dialog: ${ex}`);
+ });
+ },
+
+ /**
+ * Initializes the dialog, gathering the required bookmark data. This function
+ * will enable the accept button (if appropraite) when it is complete.
+ */
+ async _initDialog() {
+ let acceptButton = document.documentElement.getButton("accept");
+ acceptButton.label = this._getAcceptLabel();
+ let acceptButtonDisabled = false;
+
+ // Do not use sizeToContent, otherwise, due to bug 90276, the dialog will
+ // grow at every opening.
+ // Since elements can be uncollapsed asynchronously, we must observe their
+ // mutations and resize the dialog using a cached element size.
+ this._height = window.outerHeight;
+ this._mutationObserver = new MutationObserver(mutations => {
+ for (let mutation of mutations) {
+ let target = mutation.target;
+ let id = target.id;
+ if (!/^editBMPanel_.*(Row|Checkbox)$/.test(id))
+ continue;
+
+ let collapsed = target.getAttribute("collapsed") === "true";
+ let wasCollapsed = mutation.oldValue === "true";
+ if (collapsed == wasCollapsed)
+ continue;
+
+ if (collapsed) {
+ this._height -= elementsHeight.get(id);
+ elementsHeight.delete(id);
+ } else {
+ elementsHeight.set(id, target.boxObject.height);
+ this._height += elementsHeight.get(id);
+ }
+ window.resizeTo(window.outerWidth, this._height);
+ }
+ });
+
+ this._mutationObserver.observe(document,
+ { subtree: true,
+ attributeOldValue: true,
+ attributeFilter: ["collapsed"] });
+
+ // Some controls are flexible and we want to update their cached size when
+ // the dialog is resized.
+ window.addEventListener("resize", this);
+
+ switch (this._action) {
+ case ACTION_EDIT:
+ gEditItemOverlay.initPanel({ node: this._node,
+ hiddenRows: this._hiddenRows,
+ focusedElement: "first" });
+ acceptButtonDisabled = gEditItemOverlay.readOnly;
+ break;
+ case ACTION_ADD:
+ this._node = await this._promiseNewItem();
+ // Edit the new item
+ gEditItemOverlay.initPanel({ node: this._node,
+ hiddenRows: this._hiddenRows,
+ postData: this._postData,
+ focusedElement: "first" });
+
+ // Empty location field if the uri is about:blank, this way inserting a new
+ // url will be easier for the user, Accept button will be automatically
+ // disabled by the input listener until the user fills the field.
+ let locationField = this._element("locationField");
+ if (locationField.value == "about:blank")
+ locationField.value = "";
+
+ // if this is an uri related dialog disable accept button until
+ // the user fills an uri value.
+ if (this._itemType == BOOKMARK_ITEM)
+ acceptButtonDisabled = !this._inputIsValid();
+ break;
+ }
+
+ if (!gEditItemOverlay.readOnly) {
+ // Listen on uri fields to enable accept button if input is valid
+ if (this._itemType == BOOKMARK_ITEM) {
+ this._element("locationField")
+ .addEventListener("input", this);
+ if (this._isAddKeywordDialog) {
+ this._element("keywordField")
+ .addEventListener("input", this);
+ }
+ }
+ }
+ // Only enable the accept button once we've finished everything.
+ acceptButton.disabled = acceptButtonDisabled;
+ },
+
+ // nsIDOMEventListener
+ handleEvent: function BPP_handleEvent(aEvent) {
+ var target = aEvent.target;
+ switch (aEvent.type) {
+ case "input":
+ if (target.id == "editBMPanel_locationField" ||
+ target.id == "editBMPanel_keywordField") {
+ // Check uri fields to enable accept button if input is valid
+ document.documentElement
+ .getButton("accept").disabled = !this._inputIsValid();
+ }
+ break;
+ case "resize":
+ for (let [id, oldHeight] of elementsHeight) {
+ let newHeight = document.getElementById(id).boxObject.height;
+ this._height += -oldHeight + newHeight;
+ elementsHeight.set(id, newHeight);
+ }
+ break;
+ }
+ },
+
+ // nsISupports
+ QueryInterface: function BPP_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ _element: function BPP__element(aID) {
+ return document.getElementById("editBMPanel_" + aID);
+ },
+
+ onDialogUnload() {
+ // gEditItemOverlay does not exist anymore here, so don't rely on it.
+ this._mutationObserver.disconnect();
+ delete this._mutationObserver;
+
+ window.removeEventListener("resize", this);
+
+ // Calling removeEventListener with arguments which do not identify any
+ // currently registered EventListener on the EventTarget has no effect.
+ this._element("locationField")
+ .removeEventListener("input", this);
+ },
+
+ onDialogAccept() {
+ // We must blur current focused element to save its changes correctly
+ document.commandDispatcher.focusedElement.blur();
+ // We have to uninit the panel first, otherwise late changes could force it
+ // to commit more transactions.
+ gEditItemOverlay.uninitPanel(true);
+ window.arguments[0].performed = true;
+ },
+
+ onDialogCancel() {
+ // We have to uninit the panel first, otherwise late changes could force it
+ // to commit more transactions.
+ gEditItemOverlay.uninitPanel(true);
+ window.arguments[0].performed = false;
+ },
+
+ /**
+ * This method checks to see if the input fields are in a valid state.
+ *
+ * @returns true if the input is valid, false otherwise
+ */
+ _inputIsValid: function BPP__inputIsValid() {
+ if (this._itemType == BOOKMARK_ITEM &&
+ !this._containsValidURI("locationField"))
+ return false;
+ if (this._isAddKeywordDialog && !this._element("keywordField").value.length)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Determines whether the XUL textbox with the given ID contains a
+ * string that can be converted into an nsIURI.
+ *
+ * @param aTextboxID
+ * the ID of the textbox element whose contents we'll test
+ *
+ * @returns true if the textbox contains a valid URI string, false otherwise
+ */
+ _containsValidURI: function BPP__containsValidURI(aTextboxID) {
+ try {
+ var value = this._element(aTextboxID).value;
+ if (value) {
+ PlacesUIUtils.createFixedURI(value);
+ return true;
+ }
+ } catch (e) { }
+ return false;
+ },
+
+ /**
+ * [New Item Mode] Get the insertion point details for the new item, given
+ * dialog state and opening arguments.
+ *
+ * The container-identifier and insertion-index are returned separately in
+ * the form of [containerIdentifier, insertionIndex]
+ */
+ async _getInsertionPointDetails() {
+ return [
+ this._defaultInsertionPoint.itemId,
+ await this._defaultInsertionPoint.getIndex(),
+ this._defaultInsertionPoint.guid,
+ ];
+ },
+
+ async _promiseNewItem() {
+ let [containerId, index, parentGuid] = await this._getInsertionPointDetails();
+ let annotations = [];
+ if (this._description) {
+ annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO,
+ value: this._description });
+ }
+ if (this._loadInSidebar) {
+ annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
+ value: true });
+ }
+
+ let itemGuid;
+ let info = { parentGuid, index, title: this._title, annotations };
+ if (this._itemType == BOOKMARK_ITEM) {
+ info.url = this._uri;
+ if (this._keyword)
+ info.keyword = this._keyword;
+ if (this._postData)
+ info.postData = this._postData;
+
+ if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUtils.setCharsetForURI(this._uri, this._charSet);
+
+ itemGuid = await PlacesTransactions.NewBookmark(info).transact();
+ } else if (this._itemType == LIVEMARK_CONTAINER) {
+ info.feedUrl = this._feedURI;
+ if (this._siteURI)
+ info.siteUrl = this._siteURI;
+
+ itemGuid = await PlacesTransactions.NewLivemark(info).transact();
+ } else if (this._itemType == BOOKMARK_FOLDER) {
+ // NewFolder requires a url rather than uri.
+ info.children = this._URIs.map(item => {
+ return { url: item.uri, title: item.title };
+ });
+ itemGuid = await PlacesTransactions.NewFolder(info).transact();
+ } else {
+ throw new Error(`unexpected value for _itemType: ${this._itemType}`);
+ }
+
+ this._itemGuid = itemGuid;
+ this._itemId = await PlacesUtils.promiseItemId(itemGuid);
+ return Object.freeze({
+ itemId: this._itemId,
+ bookmarkGuid: this._itemGuid,
+ title: this._title,
+ uri: this._uri ? this._uri.spec : "",
+ type: this._itemType == BOOKMARK_ITEM ?
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ parent: {
+ itemId: containerId,
+ bookmarkGuid: parentGuid,
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+ }
+ });
+ }
+};
diff --git a/comm/suite/components/places/content/bookmarkProperties.xul b/comm/suite/components/places/content/bookmarkProperties.xul
new file mode 100644
index 0000000000..739d21c879
--- /dev/null
+++ b/comm/suite/components/places/content/bookmarkProperties.xul
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/"?>
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<dialog id="bookmarkproperties"
+ buttons="accept, cancel"
+ buttoniconaccept="save"
+ ondialogaccept="BookmarkPropertiesPanel.onDialogAccept();"
+ ondialogcancel="BookmarkPropertiesPanel.onDialogCancel();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="BookmarkPropertiesPanel.onDialogLoad();"
+ onunload="BookmarkPropertiesPanel.onDialogUnload();"
+ style="min-width: 30em;"
+ persist="screenX screenY width">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="stringBundle"
+ src="chrome://communicator/locale/places/bookmarkProperties.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://communicator/content/places/editBookmarkOverlay.js"/>
+ <script src="chrome://communicator/content/places/bookmarkProperties.js"/>
+
+<vbox id="editBookmarkPanelContent"/>
+
+</dialog>
diff --git a/comm/suite/components/places/content/bookmarksPanel.js b/comm/suite/components/places/content/bookmarksPanel.js
new file mode 100644
index 0000000000..e0c79eb742
--- /dev/null
+++ b/comm/suite/components/places/content/bookmarksPanel.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function init() {
+ document.getElementById("bookmarks-view").place =
+ "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
+}
+
+function searchBookmarks(aSearchString) {
+ var tree = document.getElementById("bookmarks-view");
+ if (!aSearchString)
+ tree.place = tree.place;
+ else
+ tree.applyFilter(aSearchString,
+ [PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId,
+ PlacesUtils.mobileFolderId]);
+}
+
+window.addEventListener("SidebarFocused",
+ () => document.getElementById("search-box").focus());
diff --git a/comm/suite/components/places/content/bookmarksPanel.xul b/comm/suite/components/places/content/bookmarksPanel.xul
new file mode 100644
index 0000000000..59b9957a5a
--- /dev/null
+++ b/comm/suite/components/places/content/bookmarksPanel.xul
@@ -0,0 +1,54 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebarListView.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE page SYSTEM "chrome://communicator/locale/places/places.dtd">
+
+<page id="bookmarksPanel"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();"
+ onunload="SidebarUtils.setMouseoverURL('');">
+
+ <script src="chrome://communicator/content/bookmarks/sidebarUtils.js"/>
+ <script src="chrome://communicator/content/bookmarks/bookmarksPanel.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <commandset id="placesCommands"/>
+ <menupopup id="placesContext"/>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip"/>
+
+ <hbox id="sidebar-search-container" align="center">
+ <textbox id="search-box" flex="1" type="search"
+ placeholder="&search.placeholder;"
+ aria-controls="bookmarks-view"
+ oncommand="searchBookmarks(this.value);"/>
+ </hbox>
+
+ <tree id="bookmarks-view" class="sidebar-placesTree" type="places"
+ flex="1"
+ hidecolumnpicker="true"
+ treelines="true"
+ context="placesContext"
+ onkeypress="SidebarUtils.handleTreeKeyPress(event);"
+ onclick="SidebarUtils.handleTreeClick(this, event, true);"
+ onmousemove="SidebarUtils.handleTreeMouseMove(event);"
+ onmouseout="SidebarUtils.setMouseoverURL('');">
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren id="bookmarks-view-children" view="bookmarks-view"
+ class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/>
+ </tree>
+</page>
diff --git a/comm/suite/components/places/content/browserPlacesViews.js b/comm/suite/components/places/content/browserPlacesViews.js
new file mode 100644
index 0000000000..4b3e4c24c2
--- /dev/null
+++ b/comm/suite/components/places/content/browserPlacesViews.js
@@ -0,0 +1,2287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/browser-window */
+
+/**
+ * The base view implements everything that's common to the toolbar and
+ * menu views.
+ */
+function PlacesViewBase(aPlace, aOptions = {}) {
+ if ("rootElt" in aOptions)
+ this._rootElt = aOptions.rootElt;
+ if ("viewElt" in aOptions)
+ this._viewElt = aOptions.viewElt;
+ this.options = aOptions;
+ this._controller = new PlacesController(this);
+ this.place = aPlace;
+ this._viewElt.controllers.appendController(this._controller);
+}
+
+PlacesViewBase.prototype = {
+ // The xul element that holds the entire view.
+ _viewElt: null,
+ get viewElt() {
+ return this._viewElt;
+ },
+
+ get associatedElement() {
+ return this._viewElt;
+ },
+
+ get controllers() {
+ return this._viewElt.controllers;
+ },
+
+ // The xul element that represents the root container.
+ _rootElt: null,
+
+ // Set to true for views that are represented by native widgets (i.e.
+ // the native mac menu).
+ _nativeView: false,
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsINavHistoryResultObserver,
+ Ci.nsISupportsWeakReference]),
+
+ _place: "",
+ get place() {
+ return this._place;
+ },
+ set place(val) {
+ this._place = val;
+
+ let history = PlacesUtils.history;
+ let queries = { }, options = { };
+ history.queryStringToQueries(val, queries, { }, options);
+ if (!queries.value.length)
+ queries.value = [history.getNewQuery()];
+
+ let result = history.executeQueries(queries.value, queries.value.length,
+ options.value);
+ result.addObserver(this);
+ return val;
+ },
+
+ _result: null,
+ get result() {
+ return this._result;
+ },
+ set result(val) {
+ if (this._result == val)
+ return val;
+
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ }
+
+ if (this._rootElt.localName == "menupopup")
+ this._rootElt._built = false;
+
+ this._result = val;
+ if (val) {
+ this._resultNode = val.root;
+ this._rootElt._placesNode = this._resultNode;
+ this._domNodes = new Map();
+ this._domNodes.set(this._resultNode, this._rootElt);
+
+ // This calls _rebuild through invalidateContainer.
+ this._resultNode.containerOpen = true;
+ } else {
+ this._resultNode = null;
+ delete this._domNodes;
+ }
+
+ return val;
+ },
+
+ _options: null,
+ get options() {
+ return this._options;
+ },
+ set options(val) {
+ if (!val)
+ val = {};
+
+ if (!("extraClasses" in val))
+ val.extraClasses = {};
+ this._options = val;
+
+ return val;
+ },
+
+ /**
+ * Gets the DOM node used for the given places node.
+ *
+ * @param aPlacesNode
+ * a places result node.
+ * @param aAllowMissing
+ * whether the node may be missing
+ * @throws if there is no DOM node set for aPlacesNode.
+ */
+ _getDOMNodeForPlacesNode:
+ function PVB__getDOMNodeForPlacesNode(aPlacesNode, aAllowMissing = false) {
+ let node = this._domNodes.get(aPlacesNode, null);
+ if (!node && !aAllowMissing) {
+ throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
+ aPlacesNode.type + ". node.parent: " + aPlacesNode);
+ }
+ return node;
+ },
+
+ get controller() {
+ return this._controller;
+ },
+
+ get selType() {
+ return "single";
+ },
+ selectItems() { },
+ selectAll() { },
+
+ get selectedNode() {
+ if (this._contextMenuShown) {
+ let anchor = this._contextMenuShown.triggerNode;
+ if (!anchor)
+ return null;
+
+ if (anchor._placesNode)
+ return this._rootElt == anchor ? null : anchor._placesNode;
+
+ anchor = anchor.parentNode;
+ return this._rootElt == anchor ? null : (anchor._placesNode || null);
+ }
+ return null;
+ },
+
+ get hasSelection() {
+ return this.selectedNode != null;
+ },
+
+ get selectedNodes() {
+ let selectedNode = this.selectedNode;
+ return selectedNode ? [selectedNode] : [];
+ },
+
+ get removableSelectionRanges() {
+ // On static content the current selectedNode would be the selection's
+ // parent node. We don't want to allow removing a node when the
+ // selection is not explicit.
+ if (document.popupNode &&
+ (document.popupNode == "menupopup" || !document.popupNode._placesNode))
+ return [];
+
+ return [this.selectedNodes];
+ },
+
+ get draggableSelection() {
+ return [this._draggedElt];
+ },
+
+ get insertionPoint() {
+ // There is no insertion point for history queries, so bail out now and
+ // save a lot of work when updating commands.
+ let resultNode = this._resultNode;
+ if (PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
+ return null;
+
+ // By default, the insertion point is at the top level, at the end.
+ let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ let container = this._resultNode;
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+ let tagName = null;
+
+ let selectedNode = this.selectedNode;
+ if (selectedNode) {
+ let popup = document.popupNode;
+ if (!popup._placesNode || popup._placesNode == this._resultNode ||
+ popup._placesNode.itemId == -1 || !selectedNode.parent) {
+ // If a static menuitem is selected, or if the root node is selected,
+ // the insertion point is inside the folder, at the end.
+ container = selectedNode;
+ orientation = Ci.nsITreeView.DROP_ON;
+ } else {
+ // In all other cases the insertion point is before that node.
+ container = selectedNode.parent;
+ index = container.getChildIndex(selectedNode);
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ if (!tagName)
+ return null;
+ }
+ }
+ }
+
+ if (this.controller.disallowInsertion(container))
+ return null;
+
+ return new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(container),
+ parentGuid: PlacesUtils.getConcreteItemGuid(container),
+ index, orientation, tagName
+ });
+ },
+
+ buildContextMenu: function PVB_buildContextMenu(aPopup) {
+ this._contextMenuShown = aPopup;
+ window.updateCommands("places");
+ return this.controller.buildContextMenu(aPopup);
+ },
+
+ destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
+ this._contextMenuShown = null;
+ },
+
+ clearAllContents(aPopup) {
+ let kid = aPopup.firstChild;
+ while (kid) {
+ let next = kid.nextSibling;
+ if (!kid.classList.contains("panel-header")) {
+ kid.remove();
+ }
+ kid = next;
+ }
+ aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null;
+ },
+
+ _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+ // Ensure markers are here when `invalidateContainer` is called before the
+ // popup is shown, which may the case for panelviews, for example.
+ this._ensureMarkers(aPopup);
+ // Remove Places nodes from the popup.
+ let child = aPopup._startMarker;
+ while (child.nextSibling != aPopup._endMarker) {
+ let sibling = child.nextSibling;
+ if (sibling._placesNode && !aDelay) {
+ aPopup.removeChild(sibling);
+ } else if (sibling._placesNode && aDelay) {
+ // HACK (bug 733419): the popups originating from the OS X native
+ // menubar don't live-update while open, thus we don't clean it
+ // until the next popupshowing, to avoid zombie menuitems.
+ if (!aPopup._delayedRemovals)
+ aPopup._delayedRemovals = [];
+ aPopup._delayedRemovals.push(sibling);
+ child = child.nextSibling;
+ } else {
+ child = child.nextSibling;
+ }
+ }
+ },
+
+ _rebuildPopup: function PVB__rebuildPopup(aPopup) {
+ let resultNode = aPopup._placesNode;
+ if (!resultNode.containerOpen)
+ return;
+
+ if (this.controller.hasCachedLivemarkInfo(resultNode)) {
+ this._setEmptyPopupStatus(aPopup, false);
+ aPopup._built = true;
+ this._populateLivemarkPopup(aPopup);
+ return;
+ }
+
+ this._cleanPopup(aPopup);
+
+ let cc = resultNode.childCount;
+ if (cc > 0) {
+ this._setEmptyPopupStatus(aPopup, false);
+ let fragment = document.createDocumentFragment();
+ for (let i = 0; i < cc; ++i) {
+ let child = resultNode.getChild(i);
+ this._insertNewItemToPopup(child, fragment);
+ }
+ aPopup.insertBefore(fragment, aPopup._endMarker);
+ } else {
+ this._setEmptyPopupStatus(aPopup, true);
+ }
+ aPopup._built = true;
+ },
+
+ _removeChild: function PVB__removeChild(aChild) {
+ // If document.popupNode pointed to this child, null it out,
+ // otherwise controller's command-updating may rely on the removed
+ // item still being "selected".
+ if (document.popupNode == aChild)
+ document.popupNode = null;
+
+ aChild.remove();
+ },
+
+ _setEmptyPopupStatus:
+ function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
+ if (!aPopup._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ aPopup._emptyMenuitem = document.createElement("menuitem");
+ aPopup._emptyMenuitem.setAttribute("label", label);
+ aPopup._emptyMenuitem.setAttribute("disabled", true);
+ aPopup._emptyMenuitem.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string")
+ aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+
+ if (aEmpty) {
+ aPopup.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ if (!aPopup._startMarker.previousSibling &&
+ !aPopup._endMarker.nextSibling)
+ aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
+ } else {
+ aPopup.removeAttribute("emptyplacesresult");
+ try {
+ aPopup.removeChild(aPopup._emptyMenuitem);
+ } catch (ex) {}
+ }
+ },
+
+ _createDOMNodeForPlacesNode:
+ function PVB__createDOMNodeForPlacesNode(aPlacesNode) {
+ this._domNodes.delete(aPlacesNode);
+
+ let element;
+ let type = aPlacesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createElement("menuseparator");
+ element.setAttribute("class", "small-separator");
+ } else {
+ let itemId = aPlacesNode.itemId;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
+ element = document.createElement("menuitem");
+ element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
+ element.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
+ } else if (PlacesUtils.containerTypes.includes(type)) {
+ element = document.createElement("menu");
+ element.setAttribute("container", "true");
+
+ if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
+ element.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
+ element.setAttribute("tagContainer", "true");
+ else if (PlacesUtils.nodeIsDay(aPlacesNode))
+ element.setAttribute("dayContainer", "true");
+ else if (PlacesUtils.nodeIsHost(aPlacesNode))
+ element.setAttribute("hostContainer", "true");
+ } else if (itemId != -1) {
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ element.setAttribute("livemark", "true");
+ if (AppConstants.platform === "macosx") {
+ // OS X native menubar doesn't track list-style-images since
+ // it doesn't have a frame (bug 733415). Thus enforce updating.
+ element.setAttribute("image", "");
+ element.removeAttribute("image");
+ }
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ }, () => undefined);
+ }
+
+ let popup = document.createElement("menupopup");
+ popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
+
+ if (!this._nativeView) {
+ popup.setAttribute("placespopup", "true");
+ }
+
+ element.appendChild(popup);
+ element.className = "menu-iconic bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string") {
+ element.classList.add(this.options.extraClasses.entry);
+ }
+
+ this._domNodes.set(aPlacesNode, popup);
+ } else
+ throw "Unexpected node";
+
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+
+ let icon = aPlacesNode.icon;
+ if (icon)
+ element.setAttribute("image", icon);
+ }
+
+ element._placesNode = aPlacesNode;
+ if (!this._domNodes.has(aPlacesNode))
+ this._domNodes.set(aPlacesNode, element);
+
+ return element;
+ },
+
+ _insertNewItemToPopup:
+ function PVB__insertNewItemToPopup(aNewChild, aInsertionNode, aBefore = null) {
+ let element = this._createDOMNodeForPlacesNode(aNewChild);
+
+ if (element.localName == "menuitem" || element.localName == "menu") {
+ if (typeof this.options.extraClasses.entry == "string")
+ element.classList.add(this.options.extraClasses.entry);
+ }
+
+ aInsertionNode.insertBefore(element, aBefore);
+ return element;
+ },
+
+ _setLivemarkSiteURIMenuItem:
+ function PVB__setLivemarkSiteURIMenuItem(aPopup) {
+ let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
+ let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
+ livemarkInfo.siteURI.spec : null;
+ if (!siteUrl && aPopup._siteURIMenuitem) {
+ aPopup.removeChild(aPopup._siteURIMenuitem);
+ aPopup._siteURIMenuitem = null;
+ aPopup.removeChild(aPopup._siteURIMenuseparator);
+ aPopup._siteURIMenuseparator = null;
+ } else if (siteUrl && !aPopup._siteURIMenuitem) {
+ // Add "Open (Feed Name)" menuitem.
+ aPopup._siteURIMenuitem = document.createElement("menuitem");
+ aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
+ if (typeof this.options.extraClasses.entry == "string") {
+ aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
+ aPopup._siteURIMenuitem.setAttribute("oncommand",
+ "openUILink(this.getAttribute('targetURI'), event);");
+
+ // If a user middle-clicks this item we serve the oncommand event.
+ // We are using checkForMiddleClick because of Bug 246720.
+ // Note: stopPropagation is needed to avoid serving middle-click
+ // with BT_onClick that would open all items in tabs.
+ aPopup._siteURIMenuitem.setAttribute("onclick",
+ "checkForMiddleClick(this, event); event.stopPropagation();");
+ let label =
+ PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
+ [aPopup.parentNode.getAttribute("label")]);
+ aPopup._siteURIMenuitem.setAttribute("label", label);
+ aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
+
+ aPopup._siteURIMenuseparator = document.createElement("menuseparator");
+ aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
+ }
+ },
+
+ /**
+ * Add, update or remove the livemark status menuitem.
+ * @param aPopup
+ * The livemark container popup
+ * @param aStatus
+ * The livemark status
+ */
+ _setLivemarkStatusMenuItem:
+ function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
+ let statusMenuitem = aPopup._statusMenuitem;
+ if (!statusMenuitem) {
+ // Create the status menuitem and cache it in the popup object.
+ statusMenuitem = document.createElement("menuitem");
+ statusMenuitem.className = "livemarkstatus-menuitem";
+ if (typeof this.options.extraClasses.entry == "string") {
+ statusMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ statusMenuitem.setAttribute("disabled", true);
+ aPopup._statusMenuitem = statusMenuitem;
+ }
+
+ if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
+ aStatus == Ci.mozILivemark.STATUS_FAILED) {
+ // Status has changed, update the cached status menuitem.
+ let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
+ "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
+ statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
+ if (aPopup._startMarker.nextSibling != statusMenuitem)
+ aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
+ } else if (aPopup._statusMenuitem.parentNode == aPopup) {
+ // The livemark has finished loading.
+ aPopup.removeChild(aPopup._statusMenuitem);
+ }
+ },
+
+ toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // We may get the popup for menus, but we need the menu itself.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+ if (aValue)
+ elt.setAttribute("cutting", "true");
+ else
+ elt.removeAttribute("cutting");
+ },
+
+ nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
+ },
+
+ nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's nothing to
+ // be done when the icon changes.
+ if (elt == this._rootElt)
+ return;
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+ // We must remove and reset the attribute to force an update.
+ elt.removeAttribute("image");
+ elt.setAttribute("image", aPlacesNode.icon);
+ },
+
+ nodeAnnotationChanged:
+ function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // All livemarks have a feedURI, so use it as our indicator of a livemark
+ // being modified.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ let menu = elt.parentNode;
+ if (!menu.hasAttribute("livemark")) {
+ menu.setAttribute("livemark", "true");
+ if (AppConstants.platform === "macosx") {
+ // OS X native menubar doesn't track list-style-images since
+ // it doesn't have a frame (bug 733415). Thus enforce updating.
+ menu.setAttribute("image", "");
+ menu.removeAttribute("image");
+ }
+ }
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ // Controller will use this to build the meta data for the node.
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, () => undefined);
+ }
+ },
+
+ nodeTitleChanged:
+ function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's
+ // nothing to be done when the title changes.
+ if (elt == this._rootElt)
+ return;
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (!aNewTitle && elt.localName != "toolbarbutton") {
+ // Many users consider toolbars as shortcuts containers, so explicitly
+ // allow empty labels on toolbarbuttons. For any other element try to be
+ // smarter, guessing a title from the uri.
+ elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+ } else {
+ elt.setAttribute("label", aNewTitle);
+ }
+ },
+
+ nodeRemoved:
+ function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (parentElt._built) {
+ parentElt.removeChild(elt);
+
+ // Figure out if we need to show the "<Empty>" menu-item.
+ // TODO Bug 517701: This doesn't seem to handle the case of an empty
+ // root.
+ if (parentElt._startMarker.nextSibling == parentElt._endMarker)
+ this._setEmptyPopupStatus(parentElt, true);
+ }
+ },
+
+ nodeHistoryDetailsChanged:
+ function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
+ if (aPlacesNode.parent &&
+ this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
+ // Find the node in the parent.
+ let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
+ for (let child = popup._startMarker.nextSibling;
+ child != popup._endMarker;
+ child = child.nextSibling) {
+ if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
+ if (aPlacesNode.accessCount)
+ child.setAttribute("visited", "true");
+ else
+ child.removeAttribute("visited");
+ break;
+ }
+ }
+ }
+ },
+
+ nodeTagsChanged() { },
+ nodeDateAddedChanged() { },
+ nodeLastModifiedChanged() { },
+ nodeKeywordChanged() { },
+ sortingChanged() { },
+ batching() { },
+
+ nodeInserted:
+ function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (!parentElt._built)
+ return;
+
+ let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+ aIndex + 1;
+ this._insertNewItemToPopup(aPlacesNode, parentElt,
+ parentElt.childNodes[index] || parentElt._endMarker);
+ this._setEmptyPopupStatus(parentElt, false);
+ },
+
+ nodeMoved:
+ function PBV_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ // Note: the current implementation of moveItem does not actually
+ // use this notification when the item in question is moved from one
+ // folder to another. Instead, it calls nodeRemoved and nodeInserted
+ // for the two folders. Thus, we can assume old-parent == new-parent.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ // If our root node is a folder, it might be moved. There's nothing
+ // we need to do in that case.
+ if (elt == this._rootElt)
+ return;
+
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt._built) {
+ // Move the node.
+ parentElt.removeChild(elt);
+ let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+ aNewIndex + 1;
+ parentElt.insertBefore(elt, parentElt.childNodes[index]);
+ }
+ },
+
+ containerStateChanged:
+ function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
+ aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
+ this.invalidateContainer(aPlacesNode);
+
+ if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (queryOptions.excludeItems) {
+ return;
+ }
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ let shouldInvalidate =
+ !this.controller.hasCachedLivemarkInfo(aPlacesNode);
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ aLivemark.registerForUpdates(aPlacesNode, this);
+ // Prioritize the current livemark.
+ aLivemark.reload();
+ PlacesUtils.livemarks.reloadLivemarks();
+ if (shouldInvalidate)
+ this.invalidateContainer(aPlacesNode);
+ } else {
+ aLivemark.unregisterForUpdates(aPlacesNode);
+ }
+ }, () => undefined);
+ }
+ }
+ },
+
+ _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup) {
+ this._setLivemarkSiteURIMenuItem(aPopup);
+ // Show the loading status only if there are no entries yet.
+ if (aPopup._startMarker.nextSibling == aPopup._endMarker)
+ this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
+ .then(aLivemark => {
+ let placesNode = aPopup._placesNode;
+ if (!placesNode.containerOpen)
+ return;
+
+ if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
+ this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
+ this._cleanPopup(aPopup,
+ this._nativeView && aPopup.parentNode.hasAttribute("open"));
+
+ let children = aLivemark.getNodesForContainer(placesNode);
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ this.nodeInserted(placesNode, child, i);
+ if (child.accessCount)
+ this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
+ else
+ this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
+ }
+ }, Cu.reportError);
+ },
+
+ /**
+ * Checks whether the popup associated with the provided element is open.
+ * This method may be overridden by classes that extend this base class.
+ *
+ * @param {Element} elt
+ * @return {Boolean}
+ */
+ _isPopupOpen(elt) {
+ return !!elt.parentNode.open;
+ },
+
+ invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ elt._built = false;
+
+ // If the menupopup is open we should live-update it.
+ if (this._isPopupOpen(elt))
+ this._rebuildPopup(elt);
+ },
+
+ uninit: function PVB_uninit() {
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ this._resultNode = null;
+ this._result = null;
+ }
+
+ if (this._controller) {
+ this._controller.terminate();
+ // Removing the controller will fail if it is already no longer there.
+ // This can happen if the view element was removed/reinserted without
+ // our knowledge. There is no way to check for that having happened
+ // without the possibility of an exception. :-(
+ try {
+ this._viewElt.controllers.removeController(this._controller);
+ } catch (ex) {
+ } finally {
+ this._controller = null;
+ }
+ }
+
+ delete this._viewElt._placesView;
+ },
+
+ get isRTL() {
+ if ("_isRTL" in this)
+ return this._isRTL;
+
+ return this._isRTL = document.defaultView
+ .getComputedStyle(this.viewElt)
+ .direction == "rtl";
+ },
+
+ get ownerWindow() {
+ return window;
+ },
+
+ /**
+ * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
+ * @param aPopup
+ * a Places popup.
+ */
+ _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
+ // The command items are never added to the root popup.
+ if (aPopup == this._rootElt)
+ return;
+
+ let hasMultipleURIs = false;
+
+ // Check if the popup contains at least 2 menuitems with places nodes.
+ // We don't currently support opening multiple uri nodes when they are not
+ // populated by the result.
+ if (aPopup._placesNode.childCount > 0) {
+ let currentChild = aPopup.firstChild;
+ let numURINodes = 0;
+ while (currentChild) {
+ if (currentChild.localName == "menuitem" && currentChild._placesNode) {
+ if (++numURINodes == 2)
+ break;
+ }
+ currentChild = currentChild.nextSibling;
+ }
+ hasMultipleURIs = numURINodes > 1;
+ }
+
+ let isLiveMark = false;
+ if (this.controller.hasCachedLivemarkInfo(aPopup._placesNode)) {
+ hasMultipleURIs = true;
+ isLiveMark = true;
+ }
+
+ if (!hasMultipleURIs) {
+ aPopup.setAttribute("singleitempopup", "true");
+ } else {
+ aPopup.removeAttribute("singleitempopup");
+ }
+
+ if (!hasMultipleURIs) {
+ // We don't have to show any option.
+ if (aPopup._endOptOpenAllInTabs) {
+ aPopup.removeChild(aPopup._endOptOpenAllInTabs);
+ aPopup._endOptOpenAllInTabs = null;
+
+ aPopup.removeChild(aPopup._endOptSeparator);
+ aPopup._endOptSeparator = null;
+ }
+ } else if (!aPopup._endOptOpenAllInTabs) {
+ // Create a separator before options.
+ aPopup._endOptSeparator = document.createElement("menuseparator");
+ aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
+ aPopup.appendChild(aPopup._endOptSeparator);
+
+ // Add the "Open All in Tabs" menuitem.
+ aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
+ aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
+
+ if (typeof this.options.extraClasses.entry == "string")
+ aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
+ if (typeof this.options.extraClasses.footer == "string")
+ aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
+
+ if (isLiveMark) {
+ aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+ "PlacesUIUtils.openLiveMarkNodesInTabs(this.parentNode._placesNode, event, " +
+ "PlacesUIUtils.getViewForNode(this));");
+ } else {
+ aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+ "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
+ "PlacesUIUtils.getViewForNode(this));");
+ }
+ aPopup._endOptOpenAllInTabs.setAttribute("onclick",
+ "checkForMiddleClick(this, event); event.stopPropagation();");
+ aPopup._endOptOpenAllInTabs.setAttribute("label",
+ gNavigatorBundle.getString("menuOpenAllInTabs.label"));
+ aPopup.appendChild(aPopup._endOptOpenAllInTabs);
+ }
+ },
+
+ _ensureMarkers: function PVB__ensureMarkers(aPopup) {
+ if (aPopup._startMarker)
+ return;
+
+ // _startMarker is an hidden menuseparator that lives before places nodes.
+ aPopup._startMarker = document.createElement("menuseparator");
+ aPopup._startMarker.hidden = true;
+ aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
+
+ // _endMarker is a DOM node that lives after places nodes, specified with
+ // the 'insertionPoint' option or will be a hidden menuseparator.
+ let node = this.options.insertionPoint ?
+ aPopup.querySelector(this.options.insertionPoint) : null;
+ if (node) {
+ aPopup._endMarker = node;
+ } else {
+ aPopup._endMarker = document.createElement("menuseparator");
+ aPopup._endMarker.hidden = true;
+ }
+ aPopup.appendChild(aPopup._endMarker);
+
+ // Move the markers to the right position.
+ let firstNonStaticNodeFound = false;
+ for (let i = 0; i < aPopup.childNodes.length; i++) {
+ let child = aPopup.childNodes[i];
+ // Menus that have static content at the end, but are initially empty,
+ // use a special "builder" attribute to figure out where to start
+ // inserting places nodes.
+ if (child.getAttribute("builder") == "end") {
+ aPopup.insertBefore(aPopup._endMarker, child);
+ break;
+ }
+
+ if (child._placesNode && !firstNonStaticNodeFound) {
+ firstNonStaticNodeFound = true;
+ aPopup.insertBefore(aPopup._startMarker, child);
+ }
+ }
+ if (!firstNonStaticNodeFound) {
+ aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
+ }
+ },
+
+ _onPopupShowing: function PVB__onPopupShowing(aEvent) {
+ // Avoid handling popupshowing of inner views.
+ let popup = aEvent.originalTarget;
+
+ this._ensureMarkers(popup);
+
+ // Remove any delayed element, see _cleanPopup for details.
+ if ("_delayedRemovals" in popup) {
+ while (popup._delayedRemovals.length > 0) {
+ popup.removeChild(popup._delayedRemovals.shift());
+ }
+ }
+
+ if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
+ if (!popup._placesNode.containerOpen)
+ popup._placesNode.containerOpen = true;
+ if (!popup._built)
+ this._rebuildPopup(popup);
+
+ this._mayAddCommandsItems(popup);
+ }
+ },
+
+ _addEventListeners:
+ function PVB__addEventListeners(aObject, aEventNames, aCapturing = false) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.addEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+
+ _removeEventListeners:
+ function PVB__removeEventListeners(aObject, aEventNames, aCapturing = false) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.removeEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+};
+
+function PlacesToolbar(aPlace) {
+ let startTime = Date.now();
+ // Add some smart getters for our elements.
+ let thisView = this;
+ [
+ ["_viewElt", "PlacesToolbar"],
+ ["_rootElt", "PlacesToolbarItems"],
+ ["_dropIndicator", "PlacesToolbarDropIndicator"],
+ ["_chevron", "PlacesChevron"],
+ ["_chevronPopup", "PlacesChevronPopup"]
+ ].forEach(function(elementGlobal) {
+ let [name, id] = elementGlobal;
+ thisView.__defineGetter__(name, function() {
+ let element = document.getElementById(id);
+ if (!element)
+ return null;
+
+ delete thisView[name];
+ return thisView[name] = element;
+ });
+ });
+
+ this._viewElt._placesView = this;
+
+ this._addEventListeners(this._viewElt, this._cbEvents, false);
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._addEventListeners(window, ["resize", "unload"], false);
+
+ // If personal-bookmarks has been dragged to the tabs toolbar,
+ // we have to track addition and removals of tabs, to properly
+ // recalculate the available space for bookmarks.
+ // TODO (bug 734730): Use a performant mutation listener when available.
+ if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
+ this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+ }
+
+ PlacesViewBase.call(this, aPlace);
+}
+
+PlacesToolbar.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
+ "mousemove", "mouseover", "mouseout"],
+
+ QueryInterface: function PT_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener) ||
+ aIID.equals(Ci.nsITimerCallback))
+ return this;
+
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ uninit: function PT_uninit() {
+ this._removeEventListeners(this._viewElt, this._cbEvents, false);
+ this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
+ true);
+ this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._removeEventListeners(window, ["resize", "unload"], false);
+ this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+
+ if (this._chevron._placesView) {
+ this._chevron._placesView.uninit();
+ }
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _openedMenuButton: null,
+ _allowPopupShowing: true,
+
+ _rebuild: function PT__rebuild() {
+ // Clear out references to existing nodes, since they will be removed
+ // and re-added.
+ if (this._overFolder.elt)
+ this._clearOverFolder();
+
+ this._openedMenuButton = null;
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.firstChild.remove();
+ }
+
+ let fragment = document.createDocumentFragment();
+ let cc = this._resultNode.childCount;
+ if (cc > 0) {
+ // There could be a lot of nodes, but we only want to build the ones that
+ // are likely to be shown, not all of them. Then we'll lazily create the
+ // missing nodes when needed.
+ // We don't want to cause reflows at every node insertion to calculate
+ // a precise size, thus we guess a size from the first node.
+ let button = this._insertNewItem(this._resultNode.getChild(0),
+ this._rootElt);
+ requestAnimationFrame(() => {
+ // May have been destroyed in the meanwhile.
+ if (!this._resultNode || !this._rootElt)
+ return;
+ // We assume a button with just the icon will be more or less a square,
+ // then compensate the measurement error by considering a larger screen
+ // width. Moreover the window could be bigger than the screen.
+ let size = button.clientHeight;
+ let limit = Math.min(cc, parseInt((window.screen.width * 1.5) / size));
+ for (let i = 1; i < limit; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), fragment);
+ }
+ this._rootElt.appendChild(fragment);
+
+ this.updateNodesVisibility();
+ });
+ }
+
+ if (this._chevronPopup.hasAttribute("type")) {
+ // Chevron has already been initialized, but since we are forcing
+ // a rebuild of the toolbar, it has to be rebuilt.
+ // Otherwise, it will be initialized when the toolbar overflows.
+ this._chevronPopup.place = this.place;
+ }
+ },
+
+ _insertNewItem:
+ function PT__insertNewItem(aChild, aInsertionNode, aBefore = null) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createElement("toolbarseparator");
+ } else {
+ button = document.createElement("toolbarbutton");
+ button.className = "bookmark-item";
+ button.setAttribute("label", aChild.title || "");
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("type", "menu");
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild))
+ button.setAttribute("tagContainer", "true");
+ } else if (PlacesUtils.nodeIsFolder(aChild)) {
+ PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+ .then(aLivemark => {
+ button.setAttribute("livemark", "true");
+ this.controller.cacheLivemarkInfo(aChild, aLivemark);
+ }, () => undefined);
+ }
+
+ let popup = document.createElement("menupopup");
+ popup.setAttribute("placespopup", "true");
+ button.appendChild(popup);
+ popup._placesNode = PlacesUtils.asContainer(aChild);
+ popup.setAttribute("context", "placesContext");
+
+ this._domNodes.set(aChild, popup);
+ } else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
+ }
+ }
+
+ button._placesNode = aChild;
+ if (!this._domNodes.has(aChild))
+ this._domNodes.set(aChild, button);
+
+ if (aBefore)
+ aInsertionNode.insertBefore(button, aBefore);
+ else
+ aInsertionNode.appendChild(button);
+ return button;
+ },
+
+ _updateChevronPopupNodesVisibility:
+ function PT__updateChevronPopupNodesVisibility() {
+ // Note the toolbar by default builds less nodes than the chevron popup.
+ for (let toolbarNode = this._rootElt.firstChild,
+ node = this._chevronPopup._startMarker.nextSibling;
+ toolbarNode && node;
+ toolbarNode = toolbarNode.nextSibling, node = node.nextSibling) {
+ node.hidden = toolbarNode.style.visibility != "hidden";
+ }
+ },
+
+ _onChevronPopupShowing:
+ function PT__onChevronPopupShowing(aEvent) {
+ // Handle popupshowing only for the chevron popup, not for nested ones.
+ if (aEvent.target != this._chevronPopup)
+ return;
+
+ if (!this._chevron._placesView)
+ this._chevron._placesView = new PlacesMenu(aEvent, this.place);
+
+ this._updateChevronPopupNodesVisibility();
+ },
+
+ handleEvent: function PT_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "resize":
+ // This handler updates nodes visibility in both the toolbar
+ // and the chevron popup when a window resize does not change
+ // the overflow status of the toolbar.
+ if (aEvent.target == aEvent.currentTarget) {
+ this.updateNodesVisibility();
+ }
+ break;
+ case "overflow":
+ if (!this._isOverflowStateEventRelevant(aEvent))
+ return;
+ this._onOverflow();
+ break;
+ case "underflow":
+ if (!this._isOverflowStateEventRelevant(aEvent))
+ return;
+ this._onUnderflow();
+ break;
+ case "TabOpen":
+ case "TabClose":
+ this.updateNodesVisibility();
+ break;
+ case "dragstart":
+ this._onDragStart(aEvent);
+ break;
+ case "dragover":
+ this._onDragOver(aEvent);
+ break;
+ case "dragexit":
+ this._onDragExit(aEvent);
+ break;
+ case "dragend":
+ this._onDragEnd(aEvent);
+ break;
+ case "drop":
+ this._onDrop(aEvent);
+ break;
+ case "mouseover":
+ this._onMouseOver(aEvent);
+ break;
+ case "mousemove":
+ this._onMouseMove(aEvent);
+ break;
+ case "mouseout":
+ this._onMouseOut(aEvent);
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ default:
+ throw "Trying to handle unexpected event.";
+ }
+ },
+
+ _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
+ // Ignore events not aimed at ourselves, as well as purely vertical ones:
+ return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
+ },
+
+ _onOverflow: function PT_onOverflow() {
+ // Attach the popup binding to the chevron popup if it has not yet
+ // been initialized.
+ if (!this._chevronPopup.hasAttribute("type")) {
+ this._chevronPopup.setAttribute("place", this.place);
+ this._chevronPopup.setAttribute("type", "places");
+ }
+ this._chevron.collapsed = false;
+ this.updateNodesVisibility();
+ },
+
+ _onUnderflow: function PT_onUnderflow() {
+ this.updateNodesVisibility();
+ this._chevron.collapsed = true;
+ },
+
+ updateNodesVisibility: function PT_updateNodesVisibility() {
+ // Update the chevron on a timer. This will avoid repeated work when
+ // lot of changes happen in a small timeframe.
+ if (this._updateNodesVisibilityTimer)
+ this._updateNodesVisibilityTimer.cancel();
+
+ this._updateNodesVisibilityTimer = this._setTimer(100);
+ },
+
+ _updateNodesVisibilityTimerCallback: function PT__updateNodesVisibilityTimerCallback() {
+ let scrollRect = this._rootElt.getBoundingClientRect();
+ let childOverflowed = false;
+ for (let child of this._rootElt.childNodes) {
+ // Once a child overflows, all the next ones will.
+ if (!childOverflowed) {
+ let childRect = child.getBoundingClientRect();
+ childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
+ : (childRect.right > scrollRect.right);
+ }
+
+ if (childOverflowed) {
+ child.removeAttribute("image");
+ child.style.visibility = "hidden";
+ } else {
+ let icon = child._placesNode.icon;
+ if (icon)
+ child.setAttribute("image", icon);
+ child.style.visibility = "visible";
+ }
+ }
+
+ // We rebuild the chevron on popupShowing, so if it is open
+ // we must update it.
+ if (!this._chevron.collapsed && this._chevron.open)
+ this._updateChevronPopupNodesVisibility();
+ let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", {bubbles: true});
+ this._viewElt.dispatchEvent(event);
+ },
+
+ nodeInserted:
+ function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt == this._rootElt) { // Node is on the toolbar.
+ let children = this._rootElt.childNodes;
+ // Nothing to do if it's a never-visible node, but note it's possible
+ // we are appending.
+ if (aIndex > children.length)
+ return;
+
+ // Note that childCount is already accounting for the node being added,
+ // thus we must subtract one node from it.
+ if (this._resultNode.childCount - 1 > children.length) {
+ if (aIndex == children.length) {
+ // If we didn't build all the nodes and new node is being appended,
+ // we can skip it as well.
+ return;
+ }
+ // Keep the number of built nodes consistent.
+ this._rootElt.removeChild(this._rootElt.lastChild);
+ }
+
+ let button = this._insertNewItem(aPlacesNode, this._rootElt,
+ children[aIndex] || null);
+ let prevSiblingOverflowed = aIndex > 0 && aIndex <= children.length &&
+ children[aIndex - 1].style.visibility == "hidden";
+ if (prevSiblingOverflowed) {
+ button.style.visibility = "hidden";
+ } else {
+ let icon = aPlacesNode.icon;
+ if (icon)
+ button.setAttribute("image", icon);
+ this.updateNodesVisibility();
+ }
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
+ },
+
+ nodeRemoved:
+ function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt == this._rootElt) { // Node is on the toolbar.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ // Nothing to do if it's a never-visible node.
+ if (!elt)
+ return;
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ let overflowed = elt.style.visibility == "hidden";
+ this._removeChild(elt);
+ if (this._resultNode.childCount > this._rootElt.childNodes.length) {
+ // A new node should be built to keep a coherent number of children.
+ this._insertNewItem(this._resultNode.getChild(this._rootElt.childNodes.length),
+ this._rootElt);
+ }
+ if (!overflowed)
+ this.updateNodesVisibility();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
+ },
+
+ nodeMoved:
+ function PT_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt == this._rootElt) { // Node is on the toolbar.
+ // Do nothing if the node will never be visible.
+ let lastBuiltIndex = this._rootElt.childNodes.length - 1;
+ if (aOldIndex > lastBuiltIndex && aNewIndex > lastBuiltIndex + 1)
+ return;
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ if (elt) {
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+ this._removeChild(elt);
+ }
+
+ if (aNewIndex > lastBuiltIndex + 1) {
+ if (this._resultNode.childCount > this._rootElt.childNodes.length) {
+ // If the element was built and becomes non built, another node should
+ // be built to keep a coherent number of children.
+ this._insertNewItem(this._resultNode.getChild(this._rootElt.childNodes.length),
+ this._rootElt);
+ }
+ return;
+ }
+
+ if (!elt) {
+ // The node has not been inserted yet, so we must create it.
+ elt = this._insertNewItem(aPlacesNode, this._rootElt, this._rootElt.childNodes[aNewIndex]);
+ let icon = aPlacesNode.icon;
+ if (icon)
+ elt.setAttribute("image", icon);
+ } else {
+ this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
+ }
+
+ // The chevron view may get nodeMoved after the toolbar. In such a case,
+ // we should ensure (by manually swapping menuitems) that the actual nodes
+ // are in the final position before updateNodesVisibility tries to update
+ // their visibility, or the chevron may go out of sync.
+ // Luckily updateNodesVisibility runs on a timer, so, by the time it updates
+ // nodes, the menu has already handled the notification.
+
+ this.updateNodesVisibility();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
+ },
+
+ nodeAnnotationChanged:
+ function PT_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ // Nothing to do if it's a never-visible node.
+ if (!elt || elt == this._rootElt)
+ return;
+
+ // We're notified for the menupopup, not the containing toolbarbutton.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (elt.parentNode == this._rootElt) { // Node is on the toolbar.
+ // All livemarks have a feedURI, so use it as our indicator.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ elt.setAttribute("livemark", true);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, Cu.reportError);
+ }
+ } else {
+ // Node is in a submenu.
+ PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
+ }
+ },
+
+ nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+
+ // Nothing to do if it's a never-visible node.
+ if (!elt || elt == this._rootElt)
+ return;
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (elt.parentNode == this._rootElt) { // Node is on the toolbar.
+ if (elt.style.visibility != "hidden")
+ this.updateNodesVisibility();
+ }
+ },
+
+ invalidateContainer: function PT_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ // Nothing to do if it's a never-visible node.
+ if (!elt)
+ return;
+
+ if (elt == this._rootElt) {
+ // Container is the toolbar itself.
+ this._rebuild();
+ return;
+ }
+
+ PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
+ },
+
+ _overFolder: { elt: null,
+ openTimer: null,
+ hoverTime: 350,
+ closeTimer: null },
+
+ _clearOverFolder: function PT__clearOverFolder() {
+ // The mouse is no longer dragging over the stored menubutton.
+ // Close the menubutton, clear out drag styles, and clear all
+ // timers for opening/closing it.
+ if (this._overFolder.elt && this._overFolder.elt.lastChild) {
+ if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) {
+ this._overFolder.elt.lastChild.hidePopup();
+ }
+ this._overFolder.elt.removeAttribute("dragover");
+ this._overFolder.elt = null;
+ }
+ if (this._overFolder.openTimer) {
+ this._overFolder.openTimer.cancel();
+ this._overFolder.openTimer = null;
+ }
+ if (this._overFolder.closeTimer) {
+ this._overFolder.closeTimer.cancel();
+ this._overFolder.closeTimer = null;
+ }
+ },
+
+ /**
+ * This function returns information about where to drop when dragging over
+ * the toolbar. The returned object has the following properties:
+ * - ip: the insertion point for the bookmarks service.
+ * - beforeIndex: child index to drop before, for the drop indicator.
+ * - folderElt: the folder to drop into, if applicable.
+ */
+ _getDropPoint: function PT__getDropPoint(aEvent) {
+ if (!PlacesUtils.nodeIsFolder(this._resultNode))
+ return null;
+
+ let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
+ let elt = aEvent.target;
+ if (elt._placesNode && elt != this._rootElt &&
+ elt.localName != "menupopup") {
+ let eltRect = elt.getBoundingClientRect();
+ let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
+ if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
+ !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this)) {
+ // This is a folder.
+ // If we are in the middle of it, drop inside it.
+ // Otherwise, drop before it, with regards to RTL mode.
+ let threshold = eltRect.width * 0.25;
+ if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
+ : (aEvent.clientX < eltRect.left + threshold)) {
+ // Drop before this folder.
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: eltIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE
+ });
+ dropPoint.beforeIndex = eltIndex;
+ } else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+ : (aEvent.clientX < eltRect.right - threshold)) {
+ // Drop inside this folder.
+ let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+ elt._placesNode.title : null;
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(elt._placesNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
+ tagName
+ });
+ dropPoint.beforeIndex = eltIndex;
+ dropPoint.folderElt = elt;
+ } else {
+ // Drop after this folder.
+ let beforeIndex =
+ (eltIndex == this._rootElt.childNodes.length - 1) ?
+ -1 : eltIndex + 1;
+
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: beforeIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE
+ });
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ } else {
+ // This is a non-folder node or a read-only folder.
+ // Drop before it with regards to RTL mode.
+ let threshold = eltRect.width * 0.5;
+ if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+ : (aEvent.clientX < eltRect.left + threshold)) {
+ // Drop before this bookmark.
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: eltIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE
+ });
+ dropPoint.beforeIndex = eltIndex;
+ } else {
+ // Drop after this bookmark.
+ let beforeIndex =
+ eltIndex == this._rootElt.childNodes.length - 1 ?
+ -1 : eltIndex + 1;
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: beforeIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE
+ });
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ }
+ } else {
+ // We are most likely dragging on the empty area of the
+ // toolbar, we should drop after the last node.
+ dropPoint.ip =
+ new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ orientation: Ci.nsITreeView.DROP_BEFORE
+ });
+ dropPoint.beforeIndex = -1;
+ }
+
+ return dropPoint;
+ },
+
+ _setTimer: function PT_setTimer(aTime) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ notify: function PT_notify(aTimer) {
+ if (aTimer == this._updateNodesVisibilityTimer) {
+ this._updateNodesVisibilityTimer = null;
+ // Bug 1440070: This should use promiseDocumentFlushed, so that
+ // _updateNodesVisibilityTimerCallback can use getBoundsWithoutFlush.
+ window.requestAnimationFrame(this._updateNodesVisibilityTimerCallback.bind(this));
+ } else if (aTimer == this._ibTimer) {
+ // * Timer to turn off indicator bar.
+ this._dropIndicator.collapsed = true;
+ this._ibTimer = null;
+ } else if (aTimer == this._overFolder.openTimer) {
+ // * Timer to open a menubutton that's being dragged over.
+ // Set the autoopen attribute on the folder's menupopup so that
+ // the menu will automatically close when the mouse drags off of it.
+ this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
+ this._overFolder.elt.open = true;
+ this._overFolder.openTimer = null;
+ } else if (aTimer == this._overFolder.closeTimer) {
+ // * Timer to close a menubutton that's been dragged off of.
+ // Close the menubutton if we are not dragging over it or one of
+ // its children. The autoopened attribute will let the menu know to
+ // close later if the menu is still being dragged over.
+ let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (currentPlacesNode) {
+ if (currentPlacesNode == this._rootElt) {
+ inHierarchy = true;
+ break;
+ }
+ currentPlacesNode = currentPlacesNode.parentNode;
+ }
+ // The _clearOverFolder() function will close the menu for
+ // _overFolder.elt. So null it out if we don't want to close it.
+ if (inHierarchy)
+ this._overFolder.elt = null;
+
+ // Clear out the folder and all associated timers.
+ this._clearOverFolder();
+ }
+ },
+
+ _onMouseOver: function PT__onMouseOver(aEvent) {
+ let button = aEvent.target;
+ if (button.parentNode == this._rootElt && button._placesNode &&
+ PlacesUtils.nodeIsURI(button._placesNode))
+ window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null);
+ },
+
+ _onMouseOut: function PT__onMouseOut(aEvent) {
+ window.XULBrowserWindow.setOverLink("", null);
+ },
+
+ _cleanupDragDetails: function PT__cleanupDragDetails() {
+ // Called on dragend and drop.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this._draggedElt = null;
+ if (this._ibTimer)
+ this._ibTimer.cancel();
+
+ this._dropIndicator.collapsed = true;
+ },
+
+ _onDragStart: function PT__onDragStart(aEvent) {
+ // Sub menus have their own d&d handlers.
+ let draggedElt = aEvent.target;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+ return;
+
+ if (draggedElt.localName == "toolbarbutton" &&
+ draggedElt.getAttribute("type") == "menu") {
+ // If the drag gesture on a container is toward down we open instead
+ // of dragging.
+ let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
+ let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
+ if ((translateY) >= Math.abs(translateX / 2)) {
+ // Don't start the drag.
+ aEvent.preventDefault();
+ // Open the menu.
+ draggedElt.open = true;
+ return;
+ }
+
+ // If the menu is open, close it.
+ if (draggedElt.open) {
+ draggedElt.lastChild.hidePopup();
+ draggedElt.open = false;
+ }
+ }
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(aEvent);
+ aEvent.stopPropagation();
+ },
+
+ _onDragOver: function PT__onDragOver(aEvent) {
+ // Cache the dataTransfer
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+ let dt = aEvent.dataTransfer;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (!dropPoint || !dropPoint.ip ||
+ !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
+ this._dropIndicator.collapsed = true;
+ aEvent.stopPropagation();
+ return;
+ }
+
+ if (this._ibTimer) {
+ this._ibTimer.cancel();
+ this._ibTimer = null;
+ }
+
+ if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
+ // Dropping over a menubutton or chevron button.
+ // Set styles and timer to open relative menupopup.
+ let overElt = dropPoint.folderElt || this._chevron;
+ if (this._overFolder.elt != overElt) {
+ this._clearOverFolder();
+ this._overFolder.elt = overElt;
+ this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
+ }
+ if (!this._overFolder.elt.hasAttribute("dragover"))
+ this._overFolder.elt.setAttribute("dragover", "true");
+
+ this._dropIndicator.collapsed = true;
+ } else {
+ // Dragging over a normal toolbarbutton,
+ // show indicator bar and move it to the appropriate drop point.
+ let ind = this._dropIndicator;
+ ind.parentNode.collapsed = false;
+ let halfInd = ind.clientWidth / 2;
+ let translateX;
+ if (this.isRTL) {
+ halfInd = Math.ceil(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
+ if (this._rootElt.firstChild) {
+ if (dropPoint.beforeIndex == -1)
+ translateX += this._rootElt.lastChild.getBoundingClientRect().left;
+ else {
+ translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
+ .getBoundingClientRect().right;
+ }
+ }
+ } else {
+ halfInd = Math.floor(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().left +
+ halfInd;
+ if (this._rootElt.firstChild) {
+ if (dropPoint.beforeIndex == -1)
+ translateX += this._rootElt.lastChild.getBoundingClientRect().right;
+ else {
+ translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
+ .getBoundingClientRect().left;
+ }
+ }
+ }
+
+ ind.style.transform = "translate(" + Math.round(translateX) + "px)";
+ ind.style.marginInlineStart = (-ind.clientWidth) + "px";
+ ind.collapsed = false;
+
+ // Clear out old folder information.
+ this._clearOverFolder();
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ _onDrop: function PT__onDrop(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (dropPoint && dropPoint.ip) {
+ PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
+ .catch(Cu.reportError);
+ aEvent.preventDefault();
+ }
+
+ this._cleanupDragDetails();
+ aEvent.stopPropagation();
+ },
+
+ _onDragExit: function PT__onDragExit(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = null;
+
+ // Set timer to turn off indicator bar (if we turn it off
+ // here, dragenter might be called immediately after, creating
+ // flicker).
+ if (this._ibTimer)
+ this._ibTimer.cancel();
+ this._ibTimer = this._setTimer(10);
+
+ // If we hovered over a folder, close it now.
+ if (this._overFolder.elt)
+ this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
+ },
+
+ _onDragEnd: function PT_onDragEnd(aEvent) {
+ this._cleanupDragDetails();
+ },
+
+ _onPopupShowing: function PT__onPopupShowing(aEvent) {
+ if (!this._allowPopupShowing) {
+ this._allowPopupShowing = true;
+ aEvent.preventDefault();
+ return;
+ }
+
+ let parent = aEvent.target.parentNode;
+ if (parent.localName == "toolbarbutton")
+ this._openedMenuButton = parent;
+
+ PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
+ },
+
+ _onPopupHidden: function PT__onPopupHidden(aEvent) {
+ let popup = aEvent.target;
+ let placesNode = popup._placesNode;
+ // Avoid handling popuphidden of inner views
+ if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode)) {
+ placesNode.containerOpen = false;
+ }
+ }
+
+ let parent = popup.parentNode;
+ if (parent.localName == "toolbarbutton") {
+ this._openedMenuButton = null;
+ // Clear the dragover attribute if present, if we are dragging into a
+ // folder in the hierachy of current opened popup we don't clear
+ // this attribute on clearOverFolder. See Notify for closeTimer.
+ if (parent.hasAttribute("dragover"))
+ parent.removeAttribute("dragover");
+ }
+ },
+
+ _onMouseMove: function PT__onMouseMove(aEvent) {
+ // Used in dragStart to prevent dragging folders when dragging down.
+ this._cachedMouseMoveEvent = aEvent;
+
+ if (this._openedMenuButton == null ||
+ PlacesControllerDragHelper.getSession())
+ return;
+
+ let target = aEvent.originalTarget;
+ if (this._openedMenuButton != target &&
+ target.localName == "toolbarbutton" &&
+ target.type == "menu") {
+ this._openedMenuButton.open = false;
+ target.open = true;
+ }
+ }
+};
+
+/**
+ * View for Places menus. This object should be created during the first
+ * popupshowing that's dispatched on the menu.
+ */
+function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
+ this._rootElt = aPopupShowingEvent.target; // <menupopup>
+ this._viewElt = this._rootElt.parentNode; // <menu>
+ this._viewElt._placesView = this;
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(window, ["unload"], false);
+
+ if (AppConstants.platform === "macosx") {
+ // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
+ for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
+ if (elt.localName == "menubar") {
+ this._nativeView = true;
+ break;
+ }
+ }
+ }
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+ this._onPopupShowing(aPopupShowingEvent);
+}
+
+PlacesMenu.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ QueryInterface: function PM_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener))
+ return this;
+
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ _removeChild: function PM_removeChild(aChild) {
+ PlacesViewBase.prototype._removeChild.apply(this, arguments);
+ },
+
+ uninit: function PM_uninit() {
+ this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
+ true);
+ this._removeEventListeners(window, ["unload"], false);
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ handleEvent: function PM_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ }
+ },
+
+ _onPopupHidden: function PM__onPopupHidden(aEvent) {
+ // Avoid handling popuphidden of inner views.
+ let popup = aEvent.originalTarget;
+ let placesNode = popup._placesNode;
+ if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
+ return;
+
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode))
+ placesNode.containerOpen = false;
+
+ // The autoopened attribute is set for folders which have been
+ // automatically opened when dragged over. Turn off this attribute
+ // when the folder closes because it is no longer applicable.
+ popup.removeAttribute("autoopened");
+ popup.removeAttribute("dragstart");
+ }
+};
+
+function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
+ this._viewElt = document.getElementById(aViewId);
+ this._rootElt = document.getElementById(aRootId);
+ this._viewElt._placesView = this;
+ this.options = aOptions;
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+}
+
+PlacesPanelMenuView.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ QueryInterface: function PAMV_QueryInterface(aIID) {
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ uninit: function PAMV_uninit() {
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _insertNewItem:
+ function PAMV__insertNewItem(aChild, aInsertionNode, aBefore = null) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createElement("toolbarseparator");
+ button.setAttribute("class", "small-separator");
+ } else {
+ button = document.createElement("toolbarbutton");
+ button.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string")
+ button.classList.add(this.options.extraClasses.entry);
+ button.setAttribute("label", aChild.title || "");
+ let icon = aChild.icon;
+ if (icon)
+ button.setAttribute("image", icon);
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild))
+ button.setAttribute("tagContainer", "true");
+ } else if (PlacesUtils.nodeIsFolder(aChild)) {
+ PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+ .then(aLivemark => {
+ button.setAttribute("livemark", "true");
+ this.controller.cacheLivemarkInfo(aChild, aLivemark);
+ }, () => undefined);
+ }
+ } else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
+ }
+ }
+
+ button._placesNode = aChild;
+ if (!this._domNodes.has(aChild))
+ this._domNodes.set(aChild, button);
+
+ aInsertionNode.insertBefore(button, aBefore);
+ return button;
+ },
+
+ nodeInserted:
+ function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let children = this._rootElt.childNodes;
+ this._insertNewItem(aPlacesNode, this._rootElt,
+ aIndex < children.length ? children[aIndex] : null);
+ },
+
+ nodeRemoved:
+ function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ },
+
+ nodeMoved:
+ function PAMV_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
+ },
+
+ nodeAnnotationChanged:
+ function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ // There's no UI representation for the root node.
+ if (elt == this._rootElt)
+ return;
+
+ if (elt.parentNode != this._rootElt)
+ return;
+
+ // All livemarks have a feedURI, so use it as our indicator.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ elt.setAttribute("livemark", true);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, Cu.reportError);
+ }
+ },
+
+ nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node.
+ if (elt == this._rootElt)
+ return;
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+ },
+
+ invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ if (elt != this._rootElt)
+ return;
+
+ // Container is the toolbar itself.
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.firstChild.remove();
+ }
+
+ let fragment = document.createDocumentFragment();
+ for (let i = 0; i < this._resultNode.childCount; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), fragment);
+ }
+ this._rootElt.appendChild(fragment);
+ }
+};
+
+this.PlacesPanelview = class extends PlacesViewBase {
+ constructor(container, panelview, place, options = {}) {
+ options.rootElt = container;
+ options.viewElt = panelview;
+ super(place, options);
+ this._viewElt._placesView = this;
+ // We're simulating a popup show, because a panelview may only be shown when
+ // its containing popup is already shown.
+ this._onPopupShowing({ originalTarget: this._rootElt });
+ this._addEventListeners(window, ["unload"]);
+ this._rootElt.setAttribute("context", "placesContext");
+ }
+
+ get events() {
+ if (this._events)
+ return this._events;
+ return this._events = ["click", "command", "dragend", "dragstart", "ViewHiding", "ViewShown"];
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click":
+ // For middle clicks, fall through to the command handler.
+ if (event.button != 1) {
+ break;
+ }
+ case "command":
+ this._onCommand(event);
+ break;
+ case "dragend":
+ this._onDragEnd(event);
+ break;
+ case "dragstart":
+ this._onDragStart(event);
+ break;
+ case "unload":
+ this.uninit(event);
+ break;
+ case "ViewHiding":
+ this._onPopupHidden(event);
+ break;
+ case "ViewShown":
+ this._onViewShown(event);
+ break;
+ }
+ }
+
+ _onCommand(event) {
+ let button = event.originalTarget;
+ if (!button._placesNode)
+ return;
+
+ let modifKey = AppConstants.platform === "macosx" ? event.metaKey
+ : event.ctrlKey;
+ if (!PlacesUIUtils.openInTabClosesMenu && modifKey) {
+ // If 'Recent Bookmarks' in Bookmarks Panel.
+ if (button.parentNode.id == "panelMenu_bookmarksMenu") {
+ button.setAttribute("closemenu", "none");
+ }
+ } else {
+ button.removeAttribute("closemenu");
+ }
+ PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+ // Unlike left-click, middle-click requires manual menu closing.
+ if (button.parentNode.id != "panelMenu_bookmarksMenu" ||
+ (event.type == "click" && event.button == 1 && PlacesUIUtils.openInTabClosesMenu)) {
+ this.panelMultiView.closest("panel").hidePopup();
+ }
+ }
+
+ _onDragEnd() {
+ this._draggedElt = null;
+ }
+
+ _onDragStart(event) {
+ let draggedElt = event.originalTarget;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+ return;
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ }
+
+ uninit(event) {
+ this._removeEventListeners(this.panelMultiView, this.events);
+ this._removeEventListeners(window, ["unload"]);
+ delete this.panelMultiView;
+ super.uninit(event);
+ }
+
+ _createDOMNodeForPlacesNode(placesNode) {
+ this._domNodes.delete(placesNode);
+
+ let element;
+ let type = placesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createElement("toolbarseparator");
+ } else {
+ if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI)
+ throw "Unexpected node";
+
+ element = document.createElement("toolbarbutton");
+ element.classList.add("subviewbutton", "subviewbutton-iconic", "bookmark-item");
+ element.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri));
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
+
+ let icon = placesNode.icon;
+ if (icon)
+ element.setAttribute("image", icon);
+ }
+
+ element._placesNode = placesNode;
+ if (!this._domNodes.has(placesNode))
+ this._domNodes.set(placesNode, element);
+
+ return element;
+ }
+
+ _setEmptyPopupStatus(panelview, empty = false) {
+ if (!panelview._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ panelview._emptyMenuitem = document.createElement("toolbarbutton");
+ panelview._emptyMenuitem.setAttribute("label", label);
+ panelview._emptyMenuitem.setAttribute("disabled", true);
+ panelview._emptyMenuitem.className = "subviewbutton";
+ if (typeof this.options.extraClasses.entry == "string")
+ panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+
+ if (empty) {
+ panelview.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ // We also support external usage for custom crafted panels - which'll have
+ // no markers present.
+ if (!panelview._startMarker ||
+ (!panelview._startMarker.previousSibling && !panelview._endMarker.nextSibling)) {
+ panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+ }
+ } else {
+ panelview.removeAttribute("emptyplacesresult");
+ try {
+ panelview.removeChild(panelview._emptyMenuitem);
+ } catch (ex) {}
+ }
+ }
+
+ _isPopupOpen() {
+ return PanelView.forNode(this._viewElt).active;
+ }
+
+ _onPopupHidden(event) {
+ let panelview = event.originalTarget;
+ let placesNode = panelview._placesNode;
+ // Avoid handling ViewHiding of inner views
+ if (placesNode && PlacesUIUtils.getViewForNode(panelview) == this) {
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode)) {
+ placesNode.containerOpen = false;
+ }
+ }
+ }
+
+ _onPopupShowing(event) {
+ // If the event came from the root element, this is the first time
+ // we ever get here.
+ if (event.originalTarget == this._rootElt) {
+ // Start listening for events from all panels inside the panelmultiview.
+ this.panelMultiView = this._viewElt.panelMultiView;
+ this._addEventListeners(this.panelMultiView, this.events);
+ }
+ super._onPopupShowing(event);
+ }
+
+ _onViewShown(event) {
+ if (event.originalTarget != this._viewElt)
+ return;
+
+ // Because PanelMultiView reparents the panelview internally, the controller
+ // may get lost. In that case we'll append it again, because we certainly
+ // need it later!
+ if (!this.controllers.getControllerCount() && this._controller)
+ this.controllers.appendController(this._controller);
+ }
+};
diff --git a/comm/suite/components/places/content/controller.js b/comm/suite/components/places/content/controller.js
new file mode 100644
index 0000000000..1bf51db463
--- /dev/null
+++ b/comm/suite/components/places/content/controller.js
@@ -0,0 +1,1442 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Represents an insertion point within a container where we can insert
+ * items.
+ * @param {object} an object containing the following properties:
+ * - parentId
+ * The identifier of the parent container
+ * - parentGuid
+ * The unique identifier of the parent container
+ * - index
+ * The index within the container where to insert, defaults to appending
+ * - orientation
+ * The orientation of the insertion. NOTE: the adjustments to the
+ * insertion point to accommodate the orientation should be done by
+ * the person who constructs the IP, not the user. The orientation
+ * is provided for informational purposes only! Defaults to DROP_ON.
+ * - tagName
+ * The tag name if this IP is set to a tag, null otherwise.
+ * - dropNearNode
+ * When defined index will be calculated based on this node
+ */
+function PlacesInsertionPoint({ parentId, parentGuid,
+ index = PlacesUtils.bookmarks.DEFAULT_INDEX,
+ orientation = Ci.nsITreeView.DROP_ON,
+ tagName = null,
+ dropNearNode = null }) {
+ this.itemId = parentId;
+ this.guid = parentGuid;
+ this._index = index;
+ this.orientation = orientation;
+ this.tagName = tagName;
+ this.dropNearNode = dropNearNode;
+}
+
+PlacesInsertionPoint.prototype = {
+ set index(val) {
+ return this._index = val;
+ },
+
+ async getIndex() {
+ if (this.dropNearNode) {
+ // If dropNearNode is set up we must calculate the index of the item near
+ // which we will drop.
+ let index = (await PlacesUtils.bookmarks.fetch(this.dropNearNode.bookmarkGuid)).index;
+ return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
+ }
+ return this._index;
+ },
+
+ get isTag() {
+ return typeof(this.tagName) == "string";
+ }
+};
+
+/**
+ * Places Controller
+ */
+
+function PlacesController(aView) {
+ this._view = aView;
+ XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
+ "@mozilla.org/widget/clipboard;1",
+ "nsIClipboard");
+ XPCOMUtils.defineLazyGetter(this, "profileName", function() {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
+ });
+
+ this._cachedLivemarkInfoObjects = new Map();
+}
+
+PlacesController.prototype = {
+ /**
+ * The places view.
+ */
+ _view: null,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIClipboardOwner
+ ]),
+
+ // nsIClipboardOwner
+ LosingOwnership: function PC_LosingOwnership(aXferable) {
+ this.cutNodes = [];
+ },
+
+ terminate: function PC_terminate() {
+ this._releaseClipboardOwnership();
+ },
+
+ supportsCommand: function PC_supportsCommand(aCommand) {
+ // Non-Places specific commands that we also support
+ switch (aCommand) {
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_cut":
+ case "cmd_copy":
+ case "cmd_paste":
+ case "cmd_delete":
+ case "cmd_selectAll":
+ return true;
+ }
+
+ // All other Places Commands are prefixed with "placesCmd_" ... this
+ // filters out other commands that we do _not_ support (see 329587).
+ const CMD_PREFIX = "placesCmd_";
+ return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
+ },
+
+ isCommandEnabled: function PC_isCommandEnabled(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ return PlacesTransactions.topUndoEntry != null;
+ case "cmd_redo":
+ return PlacesTransactions.topRedoEntry != null;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ for (let node of this._view.selectedNodes) {
+ // If selection includes history nodes or tags-as-bookmark, disallow
+ // cutting.
+ if (node.itemId == -1 ||
+ (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
+ return false;
+ }
+ }
+ // Otherwise fall through the cmd_delete check.
+ case "cmd_delete":
+ case "placesCmd_delete":
+ case "placesCmd_deleteDataHost":
+ return this._hasRemovableSelection();
+ case "cmd_copy":
+ case "placesCmd_copy":
+ return this._view.hasSelection;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ return this._canInsert(true) && this._isClipboardDataPasteable();
+ case "cmd_selectAll":
+ if (this._view.selType != "single") {
+ let rootNode = this._view.result.root;
+ if (rootNode.containerOpen && rootNode.childCount > 0)
+ return true;
+ }
+ return false;
+ case "placesCmd_open":
+ case "placesCmd_open:window":
+ case "placesCmd_open:privatewindow":
+ case "placesCmd_open:tab": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
+ }
+ case "placesCmd_new:folder":
+ return this._canInsert();
+ case "placesCmd_new:bookmark":
+ return this._canInsert();
+ case "placesCmd_new:separator":
+ return this._canInsert() &&
+ !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ case "placesCmd_show:info": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1;
+ }
+ case "placesCmd_reload": {
+ // Livemark containers
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
+ }
+ case "placesCmd_sortBy:name": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode &&
+ PlacesUtils.nodeIsFolder(selectedNode) &&
+ !PlacesUIUtils.isFolderReadOnly(selectedNode, this._view) &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ case "placesCmd_createBookmark":
+ var node = this._view.selectedNode;
+ return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function PC_doCommand(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ PlacesTransactions.undo().catch(Cu.reportError);
+ break;
+ case "cmd_redo":
+ PlacesTransactions.redo().catch(Cu.reportError);
+ break;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ this.cut();
+ break;
+ case "cmd_copy":
+ case "placesCmd_copy":
+ this.copy();
+ break;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ this.paste().catch(Cu.reportError);
+ break;
+ case "cmd_delete":
+ case "placesCmd_delete":
+ this.remove("Remove Selection").catch(Cu.reportError);
+ break;
+ case "placesCmd_deleteDataHost":
+ var host;
+ if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
+ var queries = this._view.selectedNode.getQueries();
+ host = queries[0].domain;
+ } else
+ host = Services.io.newURI(this._view.selectedNode.uri).host;
+ ForgetAboutSite.removeDataFromDomain(host)
+ .catch(Cu.reportError);
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ case "placesCmd_open":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
+ break;
+ case "placesCmd_open:window":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
+ break;
+ case "placesCmd_open:privatewindow":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
+ break;
+ case "placesCmd_open:tab":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
+ break;
+ case "placesCmd_new:folder":
+ this.newItem("folder").catch(Cu.reportError);
+ break;
+ case "placesCmd_new:bookmark":
+ this.newItem("bookmark").catch(Cu.reportError);
+ break;
+ case "placesCmd_new:separator":
+ this.newSeparator().catch(Cu.reportError);
+ break;
+ case "placesCmd_show:info":
+ this.showBookmarkPropertiesForSelection();
+ break;
+ case "placesCmd_reload":
+ this.reloadSelectedLivemark();
+ break;
+ case "placesCmd_sortBy:name":
+ this.sortFolderByName().catch(Cu.reportError);
+ break;
+ case "placesCmd_createBookmark":
+ let node = this._view.selectedNode;
+ PlacesUIUtils.showBookmarkDialog({ action: "add",
+ type: "bookmark",
+ hiddenRows: [ "description",
+ "keyword",
+ "location",
+ "loadInSidebar" ],
+ uri: Services.io.newURI(node.uri),
+ title: node.title
+ }, window.top);
+ break;
+ }
+ },
+
+ onEvent: function PC_onEvent(eventName) { },
+
+
+ /**
+ * Determine whether or not the selection can be removed, either by the
+ * delete or cut operations based on whether or not any of its contents
+ * are non-removable. We don't need to worry about recursion here since it
+ * is a policy decision that a removable item not be placed inside a non-
+ * removable item.
+ *
+ * @return true if all nodes in the selection can be removed,
+ * false otherwise.
+ */
+ _hasRemovableSelection() {
+ var ranges = this._view.removableSelectionRanges;
+ if (!ranges.length)
+ return false;
+
+ var root = this._view.result.root;
+
+ for (var j = 0; j < ranges.length; j++) {
+ var nodes = ranges[j];
+ for (var i = 0; i < nodes.length; ++i) {
+ // Disallow removing the view's root node
+ if (nodes[i] == root)
+ return false;
+
+ if (!PlacesUIUtils.canUserRemove(nodes[i], this._view))
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Determines whether or not nodes can be inserted relative to the selection.
+ */
+ _canInsert: function PC__canInsert(isPaste) {
+ var ip = this._view.insertionPoint;
+ return ip != null && (isPaste || ip.isTag != true);
+ },
+
+ /**
+ * Looks at the data on the clipboard to see if it is paste-able.
+ * Paste-able data is:
+ * - in a format that the view can receive
+ * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
+ * - clipboard data is of type TEXT_UNICODE and
+ * is a valid URI.
+ */
+ _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
+ // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
+ // pasteable, with no need to unwrap all the nodes.
+
+ var flavors = PlacesUIUtils.PLACES_FLAVORS;
+ var clipboard = this.clipboard;
+ var hasPlacesData =
+ clipboard.hasDataMatchingFlavors(flavors,
+ Ci.nsIClipboard.kGlobalClipboard);
+ if (hasPlacesData)
+ return this._view.insertionPoint != null;
+
+ // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
+ // pasting of valid "text/unicode" and "text/x-moz-url" data
+ var xferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xferable.init(null);
+
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
+ xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
+ clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ try {
+ // getAnyTransferData will throw if no data is available.
+ var data = { }, type = { };
+ xferable.getAnyTransferData(type, data, { });
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
+ type.value != PlacesUtils.TYPE_UNICODE)
+ return false;
+
+ // unwrapNodes() will throw if the data blob is malformed.
+ PlacesUtils.unwrapNodes(data, type.value);
+ return this._view.insertionPoint != null;
+ } catch (e) {
+ // getAnyTransferData or unwrapNodes failed
+ return false;
+ }
+ },
+
+ /**
+ * Gathers information about the selected nodes according to the following
+ * rules:
+ * "link" node is a URI
+ * "bookmark" node is a bookmark
+ * "livemarkChild" node is a child of a livemark
+ * "tagChild" node is a child of a tag
+ * "folder" node is a folder
+ * "query" node is a query
+ * "separator" node is a separator line
+ * "host" node is a host
+ *
+ * @return an array of objects corresponding the selected nodes. Each
+ * object has each of the properties above set if its corresponding
+ * node matches the rule. In addition, the annotations names for each
+ * node are set on its corresponding object as properties.
+ * Notes:
+ * 1) This can be slow, so don't call it anywhere performance critical!
+ */
+ _buildSelectionMetadata: function PC__buildSelectionMetadata() {
+ var metadata = [];
+ var nodes = this._view.selectedNodes;
+
+ for (var i = 0; i < nodes.length; i++) {
+ var nodeData = {};
+ var node = nodes[i];
+ var nodeType = node.type;
+ var uri = null;
+
+ // We don't use the nodeIs* methods here to avoid going through the type
+ // property way too often
+ switch (nodeType) {
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
+ nodeData.query = true;
+ if (node.parent) {
+ switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
+ nodeData.host = true;
+ break;
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
+ nodeData.day = true;
+ break;
+ }
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
+ nodeData.folder = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ nodeData.separator = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
+ nodeData.link = true;
+ uri = Services.io.newURI(node.uri);
+ if (PlacesUtils.nodeIsBookmark(node)) {
+ nodeData.bookmark = true;
+ var parentNode = node.parent;
+ if (parentNode) {
+ if (PlacesUtils.nodeIsTagQuery(parentNode))
+ nodeData.tagChild = true;
+ else if (this.hasCachedLivemarkInfo(parentNode))
+ nodeData.livemarkChild = true;
+ }
+ }
+ break;
+ }
+
+ // annotations
+ if (uri) {
+ let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+
+ // For items also include the item-specific annotations
+ if (node.itemId != -1) {
+ let names = PlacesUtils.annotations
+ .getItemAnnotationNames(node.itemId);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+ metadata.push(nodeData);
+ }
+
+ return metadata;
+ },
+
+ /**
+ * Determines if a context-menu item should be shown
+ * @param aMenuItem
+ * the context menu item
+ * @param aMetaData
+ * meta data about the selection
+ * @return true if the conditions (see buildContextMenu) are satisfied
+ * and the item can be displayed, false otherwise.
+ */
+ _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
+ var selectiontype = aMenuItem.getAttribute("selectiontype");
+ if (!selectiontype) {
+ selectiontype = "single|multiple";
+ }
+ var selectionTypes = selectiontype.split("|");
+ if (selectionTypes.includes("any")) {
+ return true;
+ }
+ var count = aMetaData.length;
+ if (count > 1 && !selectionTypes.includes("multiple"))
+ return false;
+ if (count == 1 && !selectionTypes.includes("single"))
+ return false;
+ // NB: if there is no selection, we show the item if and only if
+ // the selectiontype includes 'none' - the metadata list will be
+ // empty so none of the other criteria will apply anyway.
+ if (count == 0)
+ return selectionTypes.includes("none");
+
+ var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
+ if (forceHideAttr) {
+ var forceHideRules = forceHideAttr.split("|");
+ for (let i = 0; i < aMetaData.length; ++i) {
+ for (let j = 0; j < forceHideRules.length; ++j) {
+ if (forceHideRules[j] in aMetaData[i])
+ return false;
+ }
+ }
+ }
+
+ var selectionAttr = aMenuItem.getAttribute("selection");
+ if (!selectionAttr) {
+ return !aMenuItem.hidden;
+ }
+
+ if (selectionAttr == "any")
+ return true;
+
+ var showRules = selectionAttr.split("|");
+ var anyMatched = false;
+ function metaDataNodeMatches(metaDataNode, rules) {
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i] in metaDataNode)
+ return true;
+ }
+ return false;
+ }
+
+ for (var i = 0; i < aMetaData.length; ++i) {
+ if (metaDataNodeMatches(aMetaData[i], showRules))
+ anyMatched = true;
+ else
+ return false;
+ }
+ return anyMatched;
+ },
+
+ /**
+ * Detects information (meta-data rules) about the current selection in the
+ * view (see _buildSelectionMetadata) and sets the visibility state for each
+ * of the menu-items in the given popup with the following rules applied:
+ * 0) The "ignoreitem" attribute may be set to "true" for this code not to
+ * handle that menuitem.
+ * 1) The "selectiontype" attribute may be set on a menu-item to "single"
+ * if the menu-item should be visible only if there is a single node
+ * selected, or to "multiple" if the menu-item should be visible only if
+ * multiple nodes are selected, or to "none" if the menuitems should be
+ * visible for if there are no selected nodes, or to a |-separated
+ * combination of these.
+ * If the attribute is not set or set to an invalid value, the menu-item
+ * may be visible irrespective of the selection.
+ * 2) The "selection" attribute may be set on a menu-item to the various
+ * meta-data rules for which it may be visible. The rules should be
+ * separated with the | character.
+ * 3) A menu-item may be visible only if at least one of the rules set in
+ * its selection attribute apply to each of the selected nodes in the
+ * view.
+ * 4) The "forcehideselection" attribute may be set on a menu-item to rules
+ * for which it should be hidden. This attribute takes priority over the
+ * selection attribute. A menu-item would be hidden if at least one of the
+ * given rules apply to one of the selected nodes. The rules should be
+ * separated with the | character.
+ * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
+ * true if it should be hidden when there's no insertion point
+ * 6) The visibility state of a menu-item is unchanged if none of these
+ * attribute are set.
+ * 7) These attributes should not be set on separators for which the
+ * visibility state is "auto-detected."
+ * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
+ * true if it should be hidden inside the private browsing mode
+ * @param aPopup
+ * The menupopup to build children into.
+ * @return true if at least one item is visible, false otherwise.
+ */
+ buildContextMenu: function PC_buildContextMenu(aPopup) {
+ var metadata = this._buildSelectionMetadata();
+ var ip = this._view.insertionPoint;
+ var noIp = !ip || ip.isTag;
+
+ var separator = null;
+ var visibleItemsBeforeSep = false;
+ var usableItemCount = 0;
+ for (var i = 0; i < aPopup.childNodes.length; ++i) {
+ var item = aPopup.childNodes[i];
+ if (item.getAttribute("ignoreitem") == "true") {
+ continue;
+ }
+ if (item.localName != "menuseparator") {
+ // We allow pasting into tag containers, so special case that.
+ var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
+ noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
+ var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
+ PrivateBrowsingUtils.isWindowPrivate(window);
+ var shouldHideItem = hideIfNoIP || hideIfPrivate ||
+ !this._shouldShowMenuItem(item, metadata);
+ item.hidden = item.disabled = shouldHideItem;
+
+ if (!item.hidden) {
+ visibleItemsBeforeSep = true;
+ usableItemCount++;
+
+ // Show the separator above the menu-item if any
+ if (separator) {
+ separator.hidden = false;
+ separator = null;
+ }
+ }
+ } else { // menuseparator
+ // Initially hide it. It will be unhidden if there will be at least one
+ // visible menu-item above and below it.
+ item.hidden = true;
+
+ // We won't show the separator at all if no items are visible above it
+ if (visibleItemsBeforeSep)
+ separator = item;
+
+ // New separator, count again:
+ visibleItemsBeforeSep = false;
+ }
+ }
+
+ // Set Open Folder/Links In Tabs items enabled state if they're visible
+ if (usableItemCount > 0) {
+ var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
+ if (!openContainerInTabsItem.hidden) {
+ var containerToUse = this._view.selectedNode || this._view.result.root;
+ if (PlacesUtils.nodeIsContainer(containerToUse)) {
+ if (!PlacesUtils.hasChildURIs(containerToUse)) {
+ openContainerInTabsItem.disabled = true;
+ // Ensure that we don't display the menu if nothing is enabled:
+ usableItemCount--;
+ }
+ }
+ }
+ }
+
+ // Make sure to display the correct string when multiple pages are selected.
+ let stringId = metadata.length === 1 ? "SinglePage" : "MultiplePages";
+
+ let deleteHistoryItem = document.getElementById("placesContext_delete_history");
+ deleteHistoryItem.label = PlacesUIUtils.getString(`cmd.delete${stringId}.label`);
+ deleteHistoryItem.accessKey = PlacesUIUtils.getString(`cmd.delete${stringId}.accesskey`);
+
+ let createBookmarkItem = document.getElementById("placesContext_createBookmark");
+ createBookmarkItem.label = PlacesUIUtils.getString(`cmd.bookmark${stringId}.label`);
+ createBookmarkItem.accessKey = PlacesUIUtils.getString(`cmd.bookmark${stringId}.accesskey`);
+
+ return usableItemCount > 0;
+ },
+
+ /**
+ * Select all links in the current view.
+ */
+ selectAll: function PC_selectAll() {
+ this._view.selectAll();
+ },
+
+ /**
+ * Opens the bookmark properties for the selected URI Node.
+ */
+ showBookmarkPropertiesForSelection() {
+ let node = this._view.selectedNode;
+ if (!node)
+ return;
+
+ PlacesUIUtils.showBookmarkDialog({ action: "edit",
+ node,
+ hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ },
+
+ /**
+ * Reloads the selected livemark if any.
+ */
+ reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
+ var selectedNode = this._view.selectedNode;
+ if (selectedNode) {
+ let itemId = selectedNode.itemId;
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ aLivemark.reload(true);
+ }, Cu.reportError);
+ }
+ },
+
+ /**
+ * Opens the links in the selected folder, or the selected links in new tabs.
+ */
+ openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
+ var node = this._view.selectedNode;
+ var nodes = this._view.selectedNodes;
+ // In the case of no selection, open the root node:
+ if (!node && !nodes.length) {
+ node = this._view.result.root;
+ }
+ if (node && PlacesUtils.nodeIsContainer(node))
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
+ else
+ PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
+ },
+
+ /**
+ * Shows the Add Bookmark UI for the current insertion point.
+ *
+ * @param aType
+ * the type of the new item (bookmark/livemark/folder)
+ */
+ async newItem(aType) {
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let performed =
+ PlacesUIUtils.showBookmarkDialog({ action: "add",
+ type: aType,
+ defaultInsertionPoint: ip,
+ hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ if (performed) {
+ // Select the new item.
+ // TODO (Bug 1425555): When we remove places transactions, we might be
+ // able to improve showBookmarkDialog to return the guid direct, and
+ // avoid the fetch.
+ let insertedNode = await PlacesUtils.bookmarks.fetch({
+ parentGuid: ip.guid,
+ index: await ip.getIndex()
+ });
+
+ this._view.selectItems([insertedNode.guid], false);
+ }
+ },
+
+ /**
+ * Create a new Bookmark separator somewhere.
+ */
+ async newSeparator() {
+ var ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let index = await ip.getIndex();
+ let txn = PlacesTransactions.NewSeparator({ parentGuid: ip.guid, index });
+ let guid = await txn.transact();
+ // Select the new item.
+ this._view.selectItems([guid], false);
+ },
+
+ /**
+ * Sort the selected folder by name
+ */
+ async sortFolderByName() {
+ let guid = PlacesUtils.getConcreteItemGuid(this._view.selectedNode);
+ await PlacesTransactions.SortByName(guid).transact();
+ },
+
+ /**
+ * Walk the list of folders we're removing in this delete operation, and
+ * see if the selected node specified is already implicitly being removed
+ * because it is a child of that folder.
+ * @param node
+ * Node to check for containment.
+ * @param pastFolders
+ * List of folders the calling function has already traversed
+ * @return true if the node should be skipped, false otherwise.
+ */
+ _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
+ /**
+ * Determines if a node is contained by another node within a resultset.
+ * @param node
+ * The node to check for containment for
+ * @param parent
+ * The parent container to check for containment in
+ * @return true if node is a member of parent's children, false otherwise.
+ */
+ function isNodeContainedBy(parent) {
+ var cursor = node.parent;
+ while (cursor) {
+ if (cursor == parent)
+ return true;
+ cursor = cursor.parent;
+ }
+ return false;
+ }
+
+ for (var j = 0; j < pastFolders.length; ++j) {
+ if (isNodeContainedBy(pastFolders[j]))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Creates a set of transactions for the removal of a range of items.
+ * A range is an array of adjacent nodes in a view.
+ * @param [in] range
+ * An array of nodes to remove. Should all be adjacent.
+ * @param [out] transactions
+ * An array of transactions.
+ * @param [optional] removedFolders
+ * An array of folder nodes that have already been removed.
+ * @return {Integer} The total number of items affected.
+ */
+ async _removeRange(range, transactions, removedFolders) {
+ if (!(transactions instanceof Array))
+ throw new Error("Must pass a transactions array");
+ if (!removedFolders)
+ removedFolders = [];
+
+ let bmGuidsToRemove = [];
+ let totalItems = 0;
+
+ for (var i = 0; i < range.length; ++i) {
+ var node = range[i];
+ if (this._shouldSkipNode(node, removedFolders))
+ continue;
+
+ totalItems++;
+
+ if (PlacesUtils.nodeIsTagQuery(node.parent)) {
+ // This is a uri node inside a tag container. It needs a special
+ // untag transaction.
+ let tag = node.parent.title;
+ if (!tag) {
+ // TODO: Bug 1432405 Try using getConcreteItemGuid.
+ let tagItemId = PlacesUtils.getConcreteItemId(node.parent);
+ let tagGuid = await PlacesUtils.promiseItemGuid(tagItemId);
+ tag = (await PlacesUtils.bookmarks.fetch(tagGuid)).title;
+ }
+ transactions.push(PlacesTransactions.Untag({ urls: [node.uri], tag }));
+ } else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
+ // This is a tag container.
+ // Untag all URIs tagged with this tag only if the tag container is
+ // child of the "Tags" query in the library, in all other places we
+ // must only remove the query node.
+ let tag = node.title;
+ let URIs = PlacesUtils.tagging.getURIsForTag(tag);
+ transactions.push(PlacesTransactions.Untag({ tag, urls: URIs }));
+ } else if (PlacesUtils.nodeIsURI(node) &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a uri node inside an history query.
+ PlacesUtils.history.remove(node.uri).catch(Cu.reportError);
+ // History deletes are not undoable, so we don't have a transaction.
+ } else if (node.itemId == -1 &&
+ PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a dynamically generated history query, like queries
+ // grouped by site, time or both. Dynamically generated queries don't
+ // have an itemId even if they are descendants of a bookmark.
+ this._removeHistoryContainer(node);
+ // History deletes are not undoable, so we don't have a transaction.
+ } else {
+ // This is a common bookmark item.
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // If this is a folder we add it to our array of folders, used
+ // to skip nodes that are children of an already removed folder.
+ removedFolders.push(node);
+ }
+ bmGuidsToRemove.push(node.bookmarkGuid);
+ }
+ }
+ if (bmGuidsToRemove.length) {
+ transactions.push(PlacesTransactions.Remove({ guids: bmGuidsToRemove }));
+ }
+ return totalItems;
+ },
+
+ async _removeRowsFromBookmarks() {
+ let ranges = this._view.removableSelectionRanges;
+ let transactions = [];
+ let removedFolders = [];
+ let totalItems = 0;
+
+ for (let range of ranges) {
+ totalItems += await this._removeRange(range, transactions, removedFolders);
+ }
+
+ if (transactions.length > 0) {
+ await PlacesUIUtils.batchUpdatesForNode(this._view.result, totalItems, async () => {
+ await PlacesTransactions.batch(transactions);
+ });
+ }
+ },
+
+ /**
+ * Removes the set of selected ranges from history, asynchronously.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeRowsFromHistory: function PC__removeRowsFromHistory() {
+ let nodes = this._view.selectedNodes;
+ let URIs = new Set();
+ for (let i = 0; i < nodes.length; ++i) {
+ let node = nodes[i];
+ if (PlacesUtils.nodeIsURI(node)) {
+ URIs.add(node.uri);
+ } else if (PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeHistoryContainer(node);
+ }
+ }
+
+ PlacesUtils.history.remove([...URIs]).catch(Cu.reportError);
+ },
+
+ /**
+ * Removes history visits for an history container node.
+ * @param [in] aContainerNode
+ * The container node to remove.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
+ if (PlacesUtils.nodeIsHost(aContainerNode)) {
+ // Site container.
+ PlacesUtils.history.removePagesFromHost(aContainerNode.title, true);
+ } else if (PlacesUtils.nodeIsDay(aContainerNode)) {
+ // Day container.
+ let query = aContainerNode.getQueries()[0];
+ let beginTime = query.beginTime;
+ let endTime = query.endTime;
+ if (!query || !beginTime || !endTime)
+ throw new Error("A valid date container query should exist!");
+ // We want to exclude beginTime from the removal because
+ // removePagesByTimeframe includes both extremes, while date containers
+ // exclude the lower extreme. So, if we would not exclude it, we would
+ // end up removing more history than requested.
+ PlacesUtils.history.removePagesByTimeframe(beginTime + 1, endTime);
+ }
+ },
+
+ /**
+ * Removes the selection
+ */
+ async remove() {
+ if (!this._hasRemovableSelection())
+ return;
+
+ var root = this._view.result.root;
+
+ if (PlacesUtils.nodeIsFolder(root)) {
+ await this._removeRowsFromBookmarks();
+ } else if (PlacesUtils.nodeIsQuery(root)) {
+ var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
+ if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
+ await this._removeRowsFromBookmarks();
+ } else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeRowsFromHistory();
+ } else {
+ throw new Error("implement support for QUERY_TYPE_UNIFIED");
+ }
+ } else
+ throw new Error("unexpected root");
+ },
+
+ /**
+ * Fills a DataTransfer object with the content of the selection that can be
+ * dropped elsewhere.
+ * @param aEvent
+ * The dragstart event.
+ */
+ setDataTransfer: function PC_setDataTransfer(aEvent) {
+ let dt = aEvent.dataTransfer;
+
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ function addData(type, index, feedURI) {
+ let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
+ dt.mozSetDataAt(type, wrapNode, index);
+ }
+
+ function addURIData(index, feedURI) {
+ addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
+ addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
+ addData(PlacesUtils.TYPE_HTML, index, feedURI);
+ }
+
+ try {
+ let nodes = this._view.draggableSelection;
+ for (let i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+
+ // This order is _important_! It controls how this and other
+ // applications select data to be inserted based on type.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
+
+ // Drop the feed uri for livemark containers
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ if (livemarkInfo) {
+ addURIData(i, livemarkInfo.feedURI.spec);
+ } else if (node.uri) {
+ addURIData(i);
+ }
+ }
+ } finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ get clipboardAction() {
+ let action = {};
+ let actionOwner;
+ try {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION);
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+ xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
+ [action, actionOwner] =
+ action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
+ } catch (ex) {
+ // Paste from external sources don't have any associated action, just
+ // fallback to a copy action.
+ return "copy";
+ }
+ // For cuts also check who inited the action, since cuts across different
+ // instances should instead be handled as copies (The sources are not
+ // available for this instance).
+ if (action == "cut" && actionOwner != this.profileName)
+ action = "copy";
+
+ return action;
+ },
+
+ _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
+ if (this.cutNodes.length > 0) {
+ // This clears the logical clipboard, doesn't remove data.
+ this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _clearClipboard: function PC__clearClipboard() {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // Empty transferables may cause crashes, so just add an unknown type.
+ const TYPE = "text/x-moz-place-empty";
+ xferable.addDataFlavor(TYPE);
+ xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
+ this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+ },
+
+ _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
+ // This order is _important_! It controls how this and other applications
+ // select data to be inserted based on type.
+ let contents = [
+ { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
+ { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
+ { type: PlacesUtils.TYPE_HTML, entries: [] },
+ { type: PlacesUtils.TYPE_UNICODE, entries: [] },
+ ];
+
+ // Avoid handling descendants of a copied node, the transactions take care
+ // of them automatically.
+ let copiedFolders = [];
+ aNodes.forEach(function(node) {
+ if (this._shouldSkipNode(node, copiedFolders))
+ return;
+ if (PlacesUtils.nodeIsFolder(node))
+ copiedFolders.push(node);
+
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
+
+ contents.forEach(function(content) {
+ content.entries.push(
+ PlacesUtils.wrapNode(node, content.type, feedURI)
+ );
+ });
+ }, this);
+
+ function addData(type, data) {
+ xferable.addDataFlavor(type);
+ xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
+ data.length * 2);
+ }
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ let hasData = false;
+ // This order matters here! It controls how this and other applications
+ // select data to be inserted based on type.
+ contents.forEach(function(content) {
+ if (content.entries.length > 0) {
+ hasData = true;
+ let glue =
+ content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
+ addData(content.type, content.entries.join(glue));
+ }
+ });
+
+ // Track the exected action in the xferable. This must be the last flavor
+ // since it's the least preferred one.
+ // Enqueue a unique instance identifier to distinguish operations across
+ // concurrent instances of the application.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
+
+ if (hasData) {
+ this.clipboard.setData(xferable,
+ this.cutNodes.length > 0 ? this : null,
+ Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _cutNodes: [],
+ get cutNodes() {
+ return this._cutNodes;
+ },
+ set cutNodes(aNodes) {
+ let self = this;
+ function updateCutNodes(aValue) {
+ self._cutNodes.forEach(function(aNode) {
+ self._view.toggleCutNode(aNode, aValue);
+ });
+ }
+
+ updateCutNodes(false);
+ this._cutNodes = aNodes;
+ updateCutNodes(true);
+ return aNodes;
+ },
+
+ /**
+ * Copy Bookmarks and Folders to the clipboard
+ */
+ copy: function PC_copy() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "copy");
+ } finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Cut Bookmarks and Folders to the clipboard
+ */
+ cut: function PC_cut() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "cut");
+ this.cutNodes = this._view.selectedNodes;
+ } finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Paste Bookmarks and Folders from the clipboard
+ */
+ async paste() {
+ // No reason to proceed if there isn't a valid insertion point.
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let action = this.clipboardAction;
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // This order matters here! It controls the preferred flavors for this
+ // paste operation.
+ [ PlacesUtils.TYPE_X_MOZ_PLACE,
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.TYPE_UNICODE,
+ ].forEach(type => xferable.addDataFlavor(type));
+
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ // Now get the clipboard contents, in the best available flavor.
+ let data = {}, type = {}, items = [];
+ try {
+ xferable.getAnyTransferData(type, data, {});
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ type = type.value;
+ items = PlacesUtils.unwrapNodes(data, type);
+ } catch (ex) {
+ // No supported data exists or nodes unwrap failed, just bail out.
+ return;
+ }
+
+ let doCopy = action == "copy";
+ let itemsToSelect = await PlacesUIUtils.handleTransferItems(items, ip, doCopy, this._view);
+
+ // Cut/past operations are not repeatable, so clear the clipboard.
+ if (action == "cut") {
+ this._clearClipboard();
+ }
+
+ if (itemsToSelect.length > 0)
+ this._view.selectItems(itemsToSelect, false);
+ },
+
+ /**
+ * Cache the livemark info for a node. This allows the controller and the
+ * views to treat the given node as a livemark.
+ * @param aNode
+ * a places result node.
+ * @param aLivemarkInfo
+ * a mozILivemarkInfo object.
+ */
+ cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) {
+ this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
+ },
+
+ /**
+ * Returns whether or not there's cached mozILivemarkInfo object for a node.
+ * @param aNode
+ * a places result node.
+ * @return true if there's a cached mozILivemarkInfo object for
+ * aNode, false otherwise.
+ */
+ hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.has(aNode);
+ },
+
+ /**
+ * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
+ * null otherwise.
+ * @param aNode
+ * a places result node.
+ * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
+ */
+ getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.get(aNode, null);
+ },
+
+ /**
+ * Checks if we can insert into a container.
+ * @param container
+ * The container were we are want to drop
+ */
+ disallowInsertion(container) {
+ if (!container)
+ throw new Error("empty container");
+ // Allow dropping into Tag containers and editable folders.
+ return !PlacesUtils.nodeIsTagQuery(container) &&
+ (!PlacesUtils.nodeIsFolder(container) ||
+ PlacesUIUtils.isFolderReadOnly(container, this._view));
+ },
+
+ /**
+ * Determines if a node can be moved.
+ *
+ * @param aNode
+ * A nsINavHistoryResultNode node.
+ * @return True if the node can be moved, false otherwise.
+ */
+ canMoveNode(node) {
+ // Only bookmark items are movable.
+ if (node.itemId == -1)
+ return false;
+
+ // Once tags and bookmarked are divorced, the tag-query check should be
+ // removed.
+ let parentNode = node.parent;
+ return parentNode != null &&
+ PlacesUtils.nodeIsFolder(parentNode) &&
+ !PlacesUIUtils.isFolderReadOnly(parentNode, this._view) &&
+ !PlacesUtils.nodeIsTagQuery(parentNode);
+ },
+};
+
+/**
+ * Handles drag and drop operations for views. Note that this is view agnostic!
+ * You should not use PlacesController._view within these methods, since
+ * the view that the item(s) have been dropped on was not necessarily active.
+ * Drop functions are passed the view that is being dropped on.
+ */
+var PlacesControllerDragHelper = {
+ /**
+ * DOM Element currently being dragged over
+ */
+ currentDropTarget: null,
+
+ /**
+ * Determines if the mouse is currently being dragged over a child node of
+ * this menu. This is necessary so that the menu doesn't close while the
+ * mouse is dragging over one of its submenus
+ * @param node
+ * The container node
+ * @return true if the user is dragging over a node within the hierarchy of
+ * the container, false otherwise.
+ */
+ draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
+ let currentNode = this.currentDropTarget;
+ while (currentNode) {
+ if (currentNode == node)
+ return true;
+ currentNode = currentNode.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * @return The current active drag session. Returns null if there is none.
+ */
+ getSession: function PCDH__getSession() {
+ return this.dragService.getCurrentSession();
+ },
+
+ /**
+ * Extract the first accepted flavor from a list of flavors.
+ * @param aFlavors
+ * The flavors list of type DOMStringList.
+ */
+ getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
+ for (let i = 0; i < aFlavors.length; i++) {
+ if (PlacesUIUtils.SUPPORTED_FLAVORS.includes(aFlavors[i]))
+ return aFlavors[i];
+ }
+
+ // If no supported flavor is found, check if data includes text/plain
+ // contents. If so, request them as text/unicode, a conversion will happen
+ // automatically.
+ if (aFlavors.contains("text/plain")) {
+ return PlacesUtils.TYPE_UNICODE;
+ }
+
+ return null;
+ },
+
+ /**
+ * Determines whether or not the data currently being dragged can be dropped
+ * on a places view.
+ * @param ip
+ * The insertion point where the items should be dropped.
+ */
+ canDrop: function PCDH_canDrop(ip, dt) {
+ let dropCount = dt.mozItemCount;
+
+ // Check every dragged item.
+ for (let i = 0; i < dropCount; i++) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return false;
+
+ // Urls can be dropped on any insertionpoint.
+ // XXXmano: remember that this method is called for each dragover event!
+ // Thus we shouldn't use unwrapNodes here at all if possible.
+ // I think it would be OK to accept bogus data here (e.g. text which was
+ // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
+ // will just case the actual drop to be a no-op), and only rule out valid
+ // expected cases, which are either unsupported flavors, or items which
+ // cannot be dropped in the current insertionpoint. The last case will
+ // likely force us to use unwrapNodes for the private data types of
+ // places.
+ if (flavor == TAB_DROP_TYPE)
+ continue;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ let nodes;
+ try {
+ nodes = PlacesUtils.unwrapNodes(data, flavor);
+ } catch (e) {
+ return false;
+ }
+
+ for (let dragged of nodes) {
+ // Only bookmarks and urls can be dropped into tag containers.
+ if (ip.isTag &&
+ dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
+ (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ))
+ return false;
+
+ // The following loop disallows the dropping of a folder on itself or
+ // on any of its descendants.
+ if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ) {
+ let parentId = ip.itemId;
+ while (parentId != PlacesUtils.placesRootId) {
+ if (dragged.concreteId == parentId || dragged.id == parentId)
+ return false;
+ parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+ }
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Handles the drop of one or more items onto a view.
+ *
+ * @param {Object} insertionPoint The insertion point where the items should
+ * be dropped.
+ * @param {Object} dt The dataTransfer information for the drop.
+ * @param {Object} view The view or the tree element. This allows
+ * batching to take place.
+ */
+ async onDrop(insertionPoint, dt, view) {
+ let doCopy = ["copy", "link"].includes(dt.dropEffect);
+
+ let dropCount = dt.mozItemCount;
+
+ // Following flavors may contain duplicated data.
+ let duplicable = new Map();
+ duplicable.set(PlacesUtils.TYPE_UNICODE, new Set());
+ duplicable.set(PlacesUtils.TYPE_X_MOZ_URL, new Set());
+
+ // Collect all data from the DataTransfer before processing it, as the
+ // DataTransfer is only valid during the synchronous handling of the `drop`
+ // event handler callback.
+ let nodes = [];
+ for (let i = 0; i < dropCount; ++i) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ if (duplicable.has(flavor)) {
+ let handled = duplicable.get(flavor);
+ if (handled.has(data))
+ continue;
+ handled.add(data);
+ }
+
+ if (flavor != TAB_DROP_TYPE) {
+ nodes = [...nodes, ...PlacesUtils.unwrapNodes(data, flavor)];
+ } else if (data instanceof XULElement && data.localName == "tab" &&
+ data.ownerGlobal.isChromeWindow) {
+ let uri = data.linkedBrowser.currentURI;
+ let spec = uri ? uri.spec : "about:blank";
+ nodes.push({
+ uri: spec,
+ title: data.label,
+ type: PlacesUtils.TYPE_X_MOZ_URL
+ });
+ } else {
+ throw new Error("bogus data was passed as a tab");
+ }
+ }
+
+ await PlacesUIUtils.handleTransferItems(nodes, insertionPoint, doCopy, view);
+ },
+
+XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
+ "@mozilla.org/widget/dragservice;1",
+ "nsIDragService");
diff --git a/comm/suite/components/places/content/editBookmarkOverlay.js b/comm/suite/components/places/content/editBookmarkOverlay.js
new file mode 100644
index 0000000000..5a4f9c23c3
--- /dev/null
+++ b/comm/suite/components/places/content/editBookmarkOverlay.js
@@ -0,0 +1,1129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
+const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
+
+var gEditItemOverlay = {
+ _observersAdded: false,
+ _staticFoldersListBuilt: false,
+
+ _paneInfo: null,
+ _setPaneInfo(aInitInfo) {
+ if (!aInitInfo)
+ return this._paneInfo = null;
+
+ if ("uris" in aInitInfo && "node" in aInitInfo)
+ throw new Error("ambiguous pane info");
+ if (!("uris" in aInitInfo) && !("node" in aInitInfo))
+ throw new Error("Neither node nor uris set for pane info");
+
+ // We either pass a node or uris.
+ let node = "node" in aInitInfo ? aInitInfo.node : null;
+
+ // Since there's no true UI for folder shortcuts (they show up just as their target
+ // folders), when the pane shows for them it's opened in read-only mode, showing the
+ // properties of the target folder.
+ let itemId = node ? node.itemId : -1;
+ let itemGuid = node ? PlacesUtils.getConcreteItemGuid(node) : null;
+ let isItem = itemId != -1;
+ let isFolderShortcut = isItem &&
+ node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+ let isTag = node && PlacesUtils.nodeIsTagQuery(node);
+ if (isTag) {
+ itemId = PlacesUtils.getConcreteItemId(node);
+ // For now we don't have access to the item guid synchronously for tags,
+ // so we'll need to fetch it later.
+ }
+ let isURI = node && PlacesUtils.nodeIsURI(node);
+ let uri = isURI ? Services.io.newURI(node.uri) : null;
+ let title = node ? node.title : null;
+ let isBookmark = isItem && isURI;
+ let bulkTagging = !node;
+ let uris = bulkTagging ? aInitInfo.uris : null;
+ let visibleRows = new Set();
+ let isParentReadOnly = false;
+ let postData = aInitInfo.postData;
+ let parentId = -1;
+ let parentGuid = null;
+
+ if (node && isItem) {
+ if (!node.parent || (node.parent.itemId > 0 && !node.parent.bookmarkGuid)) {
+ throw new Error("Cannot use an incomplete node to initialize the edit bookmark panel");
+ }
+ let parent = node.parent;
+ isParentReadOnly = !PlacesUtils.nodeIsFolder(parent);
+ if (!isParentReadOnly) {
+ let folderId = PlacesUtils.getConcreteItemId(parent);
+ isParentReadOnly = folderId == PlacesUtils.placesRootId ||
+ (!("get" in Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId")) &&
+ (folderId == PlacesUIUtils.leftPaneFolderId));
+ }
+ parentId = parent.itemId;
+ parentGuid = parent.bookmarkGuid;
+ }
+
+ let focusedElement = aInitInfo.focusedElement;
+ let onPanelReady = aInitInfo.onPanelReady;
+
+ return this._paneInfo = { itemId, itemGuid, parentId, parentGuid, isItem,
+ isURI, uri, title,
+ isBookmark, isFolderShortcut, isParentReadOnly,
+ bulkTagging, uris,
+ visibleRows, postData, isTag, focusedElement,
+ onPanelReady };
+ },
+
+ get initialized() {
+ return this._paneInfo != null;
+ },
+
+ // Backwards-compatibility getters
+ get itemId() {
+ if (!this.initialized || this._paneInfo.bulkTagging)
+ return -1;
+ return this._paneInfo.itemId;
+ },
+
+ get uri() {
+ if (!this.initialized)
+ return null;
+ if (this._paneInfo.bulkTagging)
+ return this._paneInfo.uris[0];
+ return this._paneInfo.uri;
+ },
+
+ get multiEdit() {
+ return this.initialized && this._paneInfo.bulkTagging;
+ },
+
+ // Check if the pane is initialized to show only read-only fields.
+ get readOnly() {
+ // TODO (Bug 1120314): Folder shortcuts are currently read-only due to some
+ // quirky implementation details (the most important being the "smart"
+ // semantics of node.title that makes hard to edit the right entry).
+ // This pane is read-only if:
+ // * the panel is not initialized
+ // * the node is a folder shortcut
+ // * the node is not bookmarked and not a tag container
+ // * the node is child of a read-only container and is not a bookmarked
+ // URI nor a tag container
+ return !this.initialized ||
+ this._paneInfo.isFolderShortcut ||
+ (!this._paneInfo.isItem && !this._paneInfo.isTag) ||
+ (this._paneInfo.isParentReadOnly && !this._paneInfo.isBookmark && !this._paneInfo.isTag);
+ },
+
+ // the first field which was edited after this panel was initialized for
+ // a certain item
+ _firstEditedField: "",
+
+ _initNamePicker() {
+ if (this._paneInfo.bulkTagging)
+ throw new Error("_initNamePicker called unexpectedly");
+
+ // title may by null, which, for us, is the same as an empty string.
+ this._initTextField(this._namePicker, this._paneInfo.title || "");
+ },
+
+ _initLocationField() {
+ if (!this._paneInfo.isURI)
+ throw new Error("_initLocationField called unexpectedly");
+ this._initTextField(this._locationField, this._paneInfo.uri.spec);
+ },
+
+ _initDescriptionField() {
+ if (!this._paneInfo.isItem)
+ throw new Error("_initDescriptionField called unexpectedly");
+
+ this._initTextField(this._descriptionField,
+ PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
+ },
+
+ async _initKeywordField(newKeyword = "") {
+ if (!this._paneInfo.isBookmark) {
+ throw new Error("_initKeywordField called unexpectedly");
+ }
+
+ // Reset the field status synchronously now, eventually we'll reinit it
+ // later if we find an existing keyword. This way we can ensure to be in a
+ // consistent status when reusing the panel across different bookmarks.
+ this._keyword = newKeyword;
+ this._initTextField(this._keywordField, newKeyword);
+
+ if (!newKeyword) {
+ let entries = [];
+ await PlacesUtils.keywords.fetch({ url: this._paneInfo.uri.spec },
+ e => entries.push(e));
+ if (entries.length > 0) {
+ // We show an existing keyword if either POST data was not provided, or
+ // if the POST data is the same.
+ let existingKeyword = entries[0].keyword;
+ let postData = this._paneInfo.postData;
+ if (postData) {
+ let sameEntry = entries.find(e => e.postData === postData);
+ existingKeyword = sameEntry ? sameEntry.keyword : "";
+ }
+ if (existingKeyword) {
+ this._keyword = existingKeyword;
+ // Update the text field to the existing keyword.
+ this._initTextField(this._keywordField, this._keyword);
+ }
+ }
+ }
+ },
+
+ _initLoadInSidebar() {
+ if (!this._paneInfo.isBookmark)
+ throw new Error("_initLoadInSidebar called unexpectedly");
+
+ this._loadInSidebarCheckbox.checked =
+ PlacesUtils.annotations.itemHasAnnotation(
+ this._paneInfo.itemId, PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+ },
+
+ /**
+ * Initialize the panel.
+ *
+ * @param aInfo
+ * An object having:
+ * 1. one of the following properties:
+ * - node: either a result node or a node-like object representing the
+ * item to be edited. A node-like object must have the following
+ * properties (with values that match exactly those a result node
+ * would have): itemId, bookmarkGuid, uri, title, type.
+ * - uris: an array of uris for bulk tagging.
+ *
+ * 2. any of the following optional properties:
+ * - hiddenRows (Strings array): list of rows to be hidden regardless
+ * of the item edited. Possible values: "title", "location",
+ * "description", "keyword", "loadInSidebar", "feedLocation",
+ * "siteLocation", folderPicker"
+ */
+ initPanel(aInfo) {
+ if (typeof(aInfo) != "object" || aInfo === null)
+ throw new Error("aInfo must be an object.");
+ if ("node" in aInfo) {
+ try {
+ aInfo.node.type;
+ } catch (e) {
+ // If the lazy loader for |type| generates an exception, it means that
+ // this bookmark could not be loaded. This sometimes happens when tests
+ // create a bookmark by clicking the bookmark star, then try to cleanup
+ // before the bookmark panel has finished opening. Either way, if we
+ // cannot retrieve the bookmark information, we cannot open the panel.
+ return;
+ }
+ }
+
+ // For sanity ensure that the implementer has uninited the panel before
+ // trying to init it again, or we could end up leaking due to observers.
+ if (this.initialized)
+ this.uninitPanel(false);
+
+ let { parentId, isItem, isURI,
+ isBookmark, bulkTagging, uris,
+ visibleRows, focusedElement,
+ onPanelReady } = this._setPaneInfo(aInfo);
+
+ let showOrCollapse =
+ (rowId, isAppropriateForInput, nameInHiddenRows = null) => {
+ let visible = isAppropriateForInput;
+ if (visible && "hiddenRows" in aInfo && nameInHiddenRows)
+ visible &= !aInfo.hiddenRows.includes(nameInHiddenRows);
+ if (visible)
+ visibleRows.add(rowId);
+ return !(this._element(rowId).collapsed = !visible);
+ };
+
+ if (showOrCollapse("nameRow", !bulkTagging, "name")) {
+ this._initNamePicker();
+ this._namePicker.readOnly = this.readOnly;
+ }
+
+ // In some cases we want to hide the location field, since it's not
+ // human-readable, but we still want to initialize it.
+ showOrCollapse("locationRow", isURI, "location");
+ if (isURI) {
+ this._initLocationField();
+ this._locationField.readOnly = this.readOnly;
+ }
+
+ // hide the description field for
+ if (showOrCollapse("descriptionRow", isItem && !this.readOnly,
+ "description")) {
+ this._initDescriptionField();
+ this._descriptionField.readOnly = this.readOnly;
+ }
+
+ if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
+ this._initKeywordField().catch(Cu.reportError);
+ this._keywordField.readOnly = this.readOnly;
+ }
+
+ // Collapse the tag selector if the item does not accept tags.
+ if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
+ this._initTagsField();
+ else if (!this._element("tagsSelectorRow").collapsed)
+ this.toggleTagsSelector();
+
+ // Load in sidebar.
+ if (showOrCollapse("loadInSidebarCheckbox", isBookmark, "loadInSidebar")) {
+ this._initLoadInSidebar();
+ }
+
+ // Folder picker.
+ // Technically we should check that the item is not moveable, but that's
+ // not cheap (we don't always have the parent), and there's no use case for
+ // this (it's only the Star UI that shows the folderPicker)
+ if (showOrCollapse("folderRow", isItem, "folderPicker")) {
+ this._initFolderMenuList(parentId).catch(Cu.reportError);
+ }
+
+ // Selection count.
+ if (showOrCollapse("selectionCount", bulkTagging)) {
+ this._element("itemsCountText").value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ uris.length,
+ [uris.length]);
+ }
+
+ // Observe changes.
+ if (!this._observersAdded) {
+ PlacesUtils.bookmarks.addObserver(this);
+ window.addEventListener("unload", this);
+ this._observersAdded = true;
+ }
+
+ let focusElement = () => {
+ // The focusedElement possible values are:
+ // * preferred: focus the field that the user touched first the last
+ // time the pane was shown (either namePicker or tagsField)
+ // * first: focus the first non collapsed textbox
+ // Note: since all controls are collapsed by default, we don't get the
+ // default XUL dialog behavior, that selects the first control, so we set
+ // the focus explicitly.
+ let elt;
+ if (focusedElement === "preferred") {
+ /* eslint-disable no-undef */
+ elt = this._element(Services.prefs.getCharPref("browser.bookmarks.editDialog.firstEditField"));
+ /* eslint-enable no-undef */
+ } else if (focusedElement === "first") {
+ elt = document.querySelector("textbox:not([collapsed=true])");
+ }
+ if (elt) {
+ elt.focus();
+ elt.select();
+ }
+ };
+
+ if (onPanelReady) {
+ onPanelReady(focusElement);
+ } else {
+ focusElement();
+ }
+ },
+
+ /**
+ * Finds tags that are in common among this._currentInfo.uris;
+ */
+ _getCommonTags() {
+ if ("_cachedCommonTags" in this._paneInfo)
+ return this._paneInfo._cachedCommonTags;
+
+ let uris = [...this._paneInfo.uris];
+ let firstURI = uris.shift();
+ let commonTags = new Set(PlacesUtils.tagging.getTagsForURI(firstURI));
+ if (commonTags.size == 0)
+ return this._cachedCommonTags = [];
+
+ for (let uri of uris) {
+ let curentURITags = PlacesUtils.tagging.getTagsForURI(uri);
+ for (let tag of commonTags) {
+ if (!curentURITags.includes(tag)) {
+ commonTags.delete(tag);
+ if (commonTags.size == 0)
+ return this._paneInfo.cachedCommonTags = [];
+ }
+ }
+ }
+ return this._paneInfo._cachedCommonTags = [...commonTags];
+ },
+
+ _initTextField(aElement, aValue) {
+ if (aElement.value != aValue) {
+ aElement.value = aValue;
+
+ // Clear the editor's undo stack, but note that editor may be null here.
+ aElement.editor?.clearUndoRedo();
+ }
+ },
+
+ /**
+ * Appends a menu-item representing a bookmarks folder to a menu-popup.
+ * @param aMenupopup
+ * The popup to which the menu-item should be added.
+ * @param aFolderId
+ * The identifier of the bookmarks folder.
+ * @param aTitle
+ * The title to use as a label.
+ * @return the new menu item.
+ */
+ _appendFolderItemToMenupopup(aMenupopup, aFolderId, aTitle) {
+ // First make sure the folders-separator is visible
+ this._element("foldersSeparator").hidden = false;
+
+ var folderMenuItem = document.createElement("menuitem");
+ var folderTitle = aTitle;
+ folderMenuItem.folderId = aFolderId;
+ folderMenuItem.setAttribute("label", folderTitle);
+ folderMenuItem.className = "menuitem-iconic folder-icon";
+ aMenupopup.appendChild(folderMenuItem);
+ return folderMenuItem;
+ },
+
+ async _initFolderMenuList(aSelectedFolder) {
+ // clean up first
+ var menupopup = this._folderMenuList.menupopup;
+ while (menupopup.childNodes.length > 6)
+ menupopup.removeChild(menupopup.lastChild);
+
+ // Build the static list
+ if (!this._staticFoldersListBuilt) {
+ let unfiledItem = this._element("unfiledRootItem");
+ unfiledItem.label = PlacesUtils.getString("OtherBookmarksFolderTitle");
+ unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
+ let bmMenuItem = this._element("bmRootItem");
+ bmMenuItem.label = PlacesUtils.getString("BookmarksMenuFolderTitle");
+ bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
+ let toolbarItem = this._element("toolbarFolderItem");
+ toolbarItem.label = PlacesUtils.getString("BookmarksToolbarFolderTitle");
+ toolbarItem.folderId = PlacesUtils.toolbarFolderId;
+ this._staticFoldersListBuilt = true;
+ }
+
+ // List of recently used folders:
+ var folderIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(LAST_USED_ANNO);
+
+ /**
+ * The value of the LAST_USED_ANNO annotation is the time (in the form of
+ * Date.getTime) at which the folder has been last used.
+ *
+ * First we build the annotated folders array, each item has both the
+ * folder identifier and the time at which it was last-used by this dialog
+ * set. Then we sort it descendingly based on the time field.
+ */
+ this._recentFolders = [];
+ for (let folderId of folderIds) {
+ var lastUsed =
+ PlacesUtils.annotations.getItemAnnotation(folderId, LAST_USED_ANNO);
+ let guid = await PlacesUtils.promiseItemGuid(folderId);
+ let bm = await PlacesUtils.bookmarks.fetch(guid);
+ // Since this could be a root mobile folder, we should get the proper
+ // title.
+ let title = PlacesUtils.bookmarks.getLocalizedTitle(bm);
+ this._recentFolders.push({ folderId, guid, title, lastUsed });
+ }
+ this._recentFolders.sort(function(a, b) {
+ if (b.lastUsed < a.lastUsed)
+ return -1;
+ if (b.lastUsed > a.lastUsed)
+ return 1;
+ return 0;
+ });
+
+ var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
+ this._recentFolders.length);
+ for (let i = 0; i < numberOfItems; i++) {
+ await this._appendFolderItemToMenupopup(menupopup,
+ this._recentFolders[i].folderId,
+ this._recentFolders[i].title);
+ }
+
+ let selectedFolderGuid = await PlacesUtils.promiseItemGuid(aSelectedFolder);
+ let title = (await PlacesUtils.bookmarks.fetch(selectedFolderGuid)).title;
+ var defaultItem = this._getFolderMenuItem(aSelectedFolder, title);
+ this._folderMenuList.selectedItem = defaultItem;
+
+ // Set a selectedIndex attribute to show special icons
+ this._folderMenuList.setAttribute("selectedIndex",
+ this._folderMenuList.selectedIndex);
+
+ // Hide the folders-separator if no folder is annotated as recently-used
+ this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
+ this._folderMenuList.disabled = this.readOnly;
+ },
+
+ QueryInterface:
+ XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsINavBookmarkObserver]),
+
+ _element(aID) {
+ return document.getElementById("editBMPanel_" + aID);
+ },
+
+ uninitPanel(aHideCollapsibleElements) {
+ if (aHideCollapsibleElements) {
+ // Hide the folder tree if it was previously visible.
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed)
+ this.toggleFolderTreeVisibility();
+
+ // Hide the tag selector if it was previously visible.
+ var tagsSelectorRow = this._element("tagsSelectorRow");
+ if (!tagsSelectorRow.collapsed)
+ this.toggleTagsSelector();
+ }
+
+ if (this._observersAdded) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ this._observersAdded = false;
+ }
+
+ this._setPaneInfo(null);
+ this._firstEditedField = "";
+ },
+
+ onTagsFieldChange() {
+ // Check for _paneInfo existing as the dialog may be closing but receiving
+ // async updates from unresolved promises.
+ if (this._paneInfo &&
+ (this._paneInfo.isURI || this._paneInfo.bulkTagging)) {
+ this._updateTags().then(
+ anyChanges => {
+ // Check _paneInfo here as we might be closing the dialog.
+ if (anyChanges && this._paneInfo)
+ this._mayUpdateFirstEditField("tagsField");
+ }, Cu.reportError);
+ }
+ },
+
+ /**
+ * For a given array of currently-set tags and the tags-input-field
+ * value, returns which tags should be removed and which should be added in
+ * the form of { removedTags: [...], newTags: [...] }.
+ */
+ _getTagsChanges(aCurrentTags) {
+ let inputTags = this._getTagsArrayFromTagsInputField();
+
+ // Optimize the trivial cases (which are actually the most common).
+ if (inputTags.length == 0 && aCurrentTags.length == 0)
+ return { newTags: [], removedTags: [] };
+ if (inputTags.length == 0)
+ return { newTags: [], removedTags: aCurrentTags };
+ if (aCurrentTags.length == 0)
+ return { newTags: inputTags, removedTags: [] };
+
+ // Do not remove tags that may be reinserted with a different
+ // case, since the tagging service may handle those more efficiently.
+ let lcInputTags = inputTags.map(t => t.toLowerCase());
+ let removedTags = aCurrentTags.filter(t => !lcInputTags.includes(t.toLowerCase()));
+ let newTags = inputTags.filter(t => !aCurrentTags.includes(t));
+ return { removedTags, newTags };
+ },
+
+ // Adds and removes tags for one or more uris.
+ _setTagsFromTagsInputField(aCurrentTags, aURIs) {
+ let { removedTags, newTags } = this._getTagsChanges(aCurrentTags);
+ if (removedTags.length + newTags.length == 0)
+ return false;
+
+ let setTags = async function() {
+ if (removedTags.length > 0) {
+ await PlacesTransactions.Untag({ urls: aURIs, tags: removedTags })
+ .transact();
+ }
+ if (newTags.length > 0) {
+ await PlacesTransactions.Tag({ urls: aURIs, tags: newTags })
+ .transact();
+ }
+ };
+
+ // Only in the library info-pane it's safe (and necessary) to batch these.
+ // TODO bug 1093030: cleanup this mess when the bookmarksProperties dialog
+ // and star UI code don't "run a batch in the background".
+ if (window.document.documentElement.id == "places")
+ PlacesTransactions.batch(setTags).catch(Cu.reportError);
+ else
+ setTags().catch(Cu.reportError);
+ return true;
+ },
+
+ async _updateTags() {
+ let uris = this._paneInfo.bulkTagging ?
+ this._paneInfo.uris : [this._paneInfo.uri];
+ let currentTags = this._paneInfo.bulkTagging ?
+ await this._getCommonTags() :
+ PlacesUtils.tagging.getTagsForURI(uris[0]);
+ let anyChanges = this._setTagsFromTagsInputField(currentTags, uris);
+ if (!anyChanges)
+ return false;
+
+ // The panel could have been closed in the meanwhile.
+ if (!this._paneInfo)
+ return false;
+
+ // Ensure the tagsField is in sync, clean it up from empty tags
+ currentTags = this._paneInfo.bulkTagging ?
+ this._getCommonTags() :
+ PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
+ this._initTextField(this._tagsField, currentTags.join(", "), false);
+ return true;
+ },
+
+ /**
+ * Stores the first-edit field for this dialog, if the passed-in field
+ * is indeed the first edited field
+ * @param aNewField
+ * the id of the field that may be set (without the "editBMPanel_"
+ * prefix)
+ */
+ _mayUpdateFirstEditField(aNewField) {
+ // * The first-edit-field behavior is not applied in the multi-edit case
+ // * if this._firstEditedField is already set, this is not the first field,
+ // so there's nothing to do
+ if (this._paneInfo.bulkTagging || this._firstEditedField)
+ return;
+
+ this._firstEditedField = aNewField;
+
+ // set the pref
+ Services.prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
+ },
+
+ async onNamePickerChange() {
+ if (this.readOnly || !(this._paneInfo.isItem || this._paneInfo.isTag))
+ return;
+
+ // Here we update either the item title or its cached static title
+ let newTitle = this._namePicker.value;
+ if (!newTitle && this._paneInfo.isTag) {
+ // We don't allow setting an empty title for a tag, restore the old one.
+ this._initNamePicker();
+ } else {
+ this._mayUpdateFirstEditField("namePicker");
+
+ let guid = this._paneInfo.isTag
+ ? (await PlacesUtils.promiseItemGuid(this._paneInfo.itemId))
+ : this._paneInfo.itemGuid;
+ await PlacesTransactions.EditTitle({ guid, title: newTitle }).transact();
+ }
+ },
+
+ onDescriptionFieldChange() {
+ if (this.readOnly || !this._paneInfo.isItem)
+ return;
+
+ let description = this._element("descriptionField").value;
+ if (description != PlacesUIUtils.getItemDescription(this._paneInfo.itemId)) {
+ let annotation =
+ { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Cu.reportError);
+ }
+ },
+
+ onLocationFieldChange() {
+ if (this.readOnly || !this._paneInfo.isBookmark)
+ return;
+
+ let newURI;
+ try {
+ newURI = PlacesUIUtils.createFixedURI(this._locationField.value);
+ } catch (ex) {
+ // TODO: Bug 1089141 - Provide some feedback about the invalid url.
+ return;
+ }
+
+ if (this._paneInfo.uri.equals(newURI))
+ return;
+
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.EditUrl({ guid, url: newURI })
+ .transact().catch(Cu.reportError);
+ },
+
+ onKeywordFieldChange() {
+ if (this.readOnly || !this._paneInfo.isBookmark)
+ return;
+
+ let oldKeyword = this._keyword;
+ let keyword = this._keyword = this._keywordField.value;
+ let postData = this._paneInfo.postData;
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.EditKeyword({ guid, keyword, postData, oldKeyword })
+ .transact().catch(Cu.reportError);
+ },
+
+ onLoadInSidebarCheckboxCommand() {
+ if (!this.initialized || !this._paneInfo.isBookmark)
+ return;
+
+ let annotation = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
+ if (this._loadInSidebarCheckbox.checked)
+ annotation.value = true;
+
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Cu.reportError);
+ },
+
+ toggleFolderTreeVisibility() {
+ var expander = this._element("foldersExpander");
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed) {
+ expander.className = "expander-down";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextdown"));
+ folderTreeRow.collapsed = true;
+ this._element("chooseFolderSeparator").hidden =
+ this._element("chooseFolderMenuItem").hidden = false;
+ } else {
+ expander.className = "expander-up";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextup"));
+ folderTreeRow.collapsed = false;
+
+ // XXXmano: Ideally we would only do this once, but for some odd reason,
+ // the editable mode set on this tree, together with its collapsed state
+ // breaks the view.
+ const FOLDER_TREE_PLACE_URI =
+ "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&type=" +
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
+ this._folderTree.place = FOLDER_TREE_PLACE_URI;
+
+ this._element("chooseFolderSeparator").hidden =
+ this._element("chooseFolderMenuItem").hidden = true;
+ this._folderTree.selectItems([this._paneInfo.parentGuid]);
+ this._folderTree.focus();
+ }
+ },
+
+ /**
+ * Get the corresponding menu-item in the folder-menu-list for a bookmarks
+ * folder if such an item exists. Otherwise, this creates a menu-item for the
+ * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
+ * the new item replaces the last menu-item.
+ * @param aFolderId
+ * The identifier of the bookmarks folder.
+ * @param aTitle
+ * The title to use in case of menuitem creation.
+ * @return handle to the menuitem.
+ */
+ _getFolderMenuItem(aFolderId, aTitle) {
+ let menupopup = this._folderMenuList.menupopup;
+ let menuItem = Array.prototype.find.call(
+ menupopup.childNodes, item => item.folderId === aFolderId);
+ if (menuItem !== undefined)
+ return menuItem;
+
+ // 3 special folders + separator + folder-items-count limit
+ if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
+ menupopup.removeChild(menupopup.lastChild);
+
+ return this._appendFolderItemToMenupopup(menupopup, aFolderId, aTitle);
+ },
+
+ async onFolderMenuListCommand(aEvent) {
+ // Check for _paneInfo existing as the dialog may be closing but receiving
+ // async updates from unresolved promises.
+ if (!this._paneInfo) {
+ return;
+ }
+ // Set a selectedIndex attribute to show special icons
+ this._folderMenuList.setAttribute("selectedIndex",
+ this._folderMenuList.selectedIndex);
+
+ if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
+ // reset the selection back to where it was and expand the tree
+ // (this menu-item is hidden when the tree is already visible
+ let item = this._getFolderMenuItem(this._paneInfo.parentId,
+ this._paneInfo.title);
+ this._folderMenuList.selectedItem = item;
+ // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
+ // menulist right away
+ setTimeout(() => this.toggleFolderTreeVisibility(), 100);
+ return;
+ }
+
+ // Move the item
+ let containerId = this._folderMenuList.selectedItem.folderId;
+ if (this._paneInfo.parentId != containerId &&
+ this._paneInfo.itemId != containerId) {
+ let newParentGuid = await PlacesUtils.promiseItemGuid(containerId);
+ let guid = this._paneInfo.itemGuid;
+ await PlacesTransactions.Move({ guid, newParentGuid }).transact();
+
+ // Mark the containing folder as recently-used if it isn't in the
+ // static list
+ if (containerId != PlacesUtils.unfiledBookmarksFolderId &&
+ containerId != PlacesUtils.toolbarFolderId &&
+ containerId != PlacesUtils.bookmarksMenuFolderId) {
+ this._markFolderAsRecentlyUsed(containerId)
+ .catch(Cu.reportError);
+ }
+
+ // Auto-show the bookmarks toolbar when adding / moving an item there.
+ if (containerId == PlacesUtils.toolbarFolderId) {
+ Services.obs.notifyObservers(null, "autoshow-bookmarks-toolbar");
+ }
+ }
+
+ // Update folder-tree selection
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed) {
+ var selectedNode = this._folderTree.selectedNode;
+ if (!selectedNode ||
+ PlacesUtils.getConcreteItemId(selectedNode) != containerId)
+ this._folderTree.selectItems([containerId]);
+ }
+ },
+
+ onFolderTreeSelect() {
+ var selectedNode = this._folderTree.selectedNode;
+
+ // Disable the "New Folder" button if we cannot create a new folder
+ this._element("newFolderButton")
+ .disabled = !this._folderTree.insertionPoint || !selectedNode;
+
+ if (!selectedNode)
+ return;
+
+ var folderId = PlacesUtils.getConcreteItemId(selectedNode);
+ if (this._folderMenuList.selectedItem.folderId == folderId)
+ return;
+
+ var folderItem = this._getFolderMenuItem(folderId, selectedNode.title);
+ this._folderMenuList.selectedItem = folderItem;
+ folderItem.doCommand();
+ },
+
+ async _markFolderAsRecentlyUsed(aFolderId) {
+ // Expire old unused recent folders.
+ let guids = [];
+ while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
+ let folderId = this._recentFolders.pop().folderId;
+ let guid = await PlacesUtils.promiseItemGuid(folderId);
+ guids.push(guid);
+ }
+ if (guids.length > 0) {
+ let annotation = this._getLastUsedAnnotationObject(false);
+ PlacesTransactions.Annotate({ guids, annotation })
+ .transact().catch(Cu.reportError);
+ }
+
+ // Mark folder as recently used
+ let annotation = this._getLastUsedAnnotationObject(true);
+ let guid = await PlacesUtils.promiseItemGuid(aFolderId);
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Cu.reportError);
+ },
+
+ /**
+ * Returns an object which could then be used to set/unset the
+ * LAST_USED_ANNO annotation for a folder.
+ *
+ * @param aLastUsed
+ * Whether to set or unset the LAST_USED_ANNO annotation.
+ * @returns an object representing the annotation which could then be used
+ * with the transaction manager.
+ */
+ _getLastUsedAnnotationObject(aLastUsed) {
+ return { name: LAST_USED_ANNO,
+ value: aLastUsed ? new Date().getTime() : null };
+ },
+
+ _rebuildTagsSelectorList() {
+ let tagsSelector = this._element("tagsSelector");
+ let tagsSelectorRow = this._element("tagsSelectorRow");
+ if (tagsSelectorRow.collapsed)
+ return;
+
+ // Save the current scroll position and restore it after the rebuild.
+ let firstIndex = tagsSelector.getIndexOfFirstVisibleRow();
+ let selectedIndex = tagsSelector.selectedIndex;
+ let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label
+ : null;
+
+ while (tagsSelector.hasChildNodes()) {
+ tagsSelector.removeChild(tagsSelector.lastChild);
+ }
+
+ let tagsInField = this._getTagsArrayFromTagsInputField();
+ let allTags = PlacesUtils.tagging.allTags;
+ for (let tag of allTags) {
+ let elt = document.createElement("listitem");
+ elt.setAttribute("type", "checkbox");
+ elt.setAttribute("label", tag);
+ if (tagsInField.includes(tag))
+ elt.setAttribute("checked", "true");
+ tagsSelector.appendChild(elt);
+ if (selectedTag === tag)
+ selectedIndex = tagsSelector.getIndexOfItem(elt);
+ }
+
+ // Restore position.
+ // The listbox allows to scroll only if the required offset doesn't
+ // overflow its capacity, thus need to adjust the index for removals.
+ firstIndex =
+ Math.min(firstIndex,
+ tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows());
+ tagsSelector.scrollToIndex(firstIndex);
+ if (selectedIndex >= 0 && tagsSelector.itemCount > 0) {
+ selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1);
+ tagsSelector.selectedIndex = selectedIndex;
+ tagsSelector.ensureIndexIsVisible(selectedIndex);
+ }
+ },
+
+ toggleTagsSelector() {
+ var tagsSelector = this._element("tagsSelector");
+ var tagsSelectorRow = this._element("tagsSelectorRow");
+ var expander = this._element("tagsSelectorExpander");
+ if (tagsSelectorRow.collapsed) {
+ expander.className = "expander-up";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextup"));
+ tagsSelectorRow.collapsed = false;
+ this._rebuildTagsSelectorList();
+
+ // This is a no-op if we've added the listener.
+ tagsSelector.addEventListener("CheckboxStateChange", this);
+ } else {
+ expander.className = "expander-down";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextdown"));
+ tagsSelectorRow.collapsed = true;
+ }
+ },
+
+ /**
+ * Splits "tagsField" element value, returning an array of valid tag strings.
+ *
+ * @return Array of tag strings found in the field value.
+ */
+ _getTagsArrayFromTagsInputField() {
+ let tags = this._element("tagsField").value;
+ return tags.trim()
+ .split(/\s*,\s*/) // Split on commas and remove spaces.
+ .filter(tag => tag.length > 0); // Kill empty tags.
+ },
+
+ async newFolder() {
+ let ip = this._folderTree.insertionPoint;
+
+ // default to the bookmarks menu folder
+ if (!ip) {
+ ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.bookmarksMenuFolderId,
+ parentGuid: PlacesUtils.bookmarks.menuGuid
+ });
+ }
+
+ // XXXmano: add a separate "New Folder" string at some point...
+ let title = this._element("newFolderButton").label;
+ await PlacesTransactions.NewFolder({ parentGuid: ip.guid, title,
+ index: await ip.getIndex() })
+ .transact().catch(Cu.reportError);
+
+ this._folderTree.focus();
+ this._folderTree.selectItems([ip.itemId]);
+ PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
+ this._folderTree.selectItems([this._lastNewItem]);
+ this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
+ this._folderTree.columns.getFirstColumn());
+ },
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "CheckboxStateChange":
+ // Update the tags field when items are checked/unchecked in the listbox
+ let tags = this._getTagsArrayFromTagsInputField();
+ let tagCheckbox = aEvent.target;
+
+ let curTagIndex = tags.indexOf(tagCheckbox.label);
+ let tagsSelector = this._element("tagsSelector");
+ tagsSelector.selectedItem = tagCheckbox;
+
+ if (tagCheckbox.checked) {
+ if (curTagIndex == -1)
+ tags.push(tagCheckbox.label);
+ } else if (curTagIndex != -1) {
+ tags.splice(curTagIndex, 1);
+ }
+ this._element("tagsField").value = tags.join(", ");
+ this._updateTags();
+ break;
+ case "unload":
+ this.uninitPanel(false);
+ break;
+ }
+ },
+
+ _initTagsField() {
+ let tags;
+ if (this._paneInfo.isURI)
+ tags = PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
+ else if (this._paneInfo.bulkTagging)
+ tags = this._getCommonTags();
+ else
+ throw new Error("_promiseTagsStr called unexpectedly");
+
+ this._initTextField(this._tagsField, tags.join(", "));
+ },
+
+ async _onTagsChange(guid, changedURI = null) {
+ let paneInfo = this._paneInfo;
+ let updateTagsField = false;
+ if (paneInfo.isURI) {
+ if (paneInfo.isBookmark && guid == paneInfo.itemGuid) {
+ updateTagsField = true;
+ } else if (!paneInfo.isBookmark) {
+ if (!changedURI) {
+ let href = (await PlacesUtils.bookmarks.fetch(guid)).url.href;
+ changedURI = Services.io.newURI(href);
+ }
+ updateTagsField = changedURI.equals(paneInfo.uri);
+ }
+ } else if (paneInfo.bulkTagging) {
+ if (!changedURI) {
+ let href = (await PlacesUtils.bookmarks.fetch(guid)).url.href;
+ changedURI = Services.io.newURI(href);
+ }
+ if (paneInfo.uris.some(uri => uri.equals(changedURI))) {
+ updateTagsField = true;
+ delete this._paneInfo._cachedCommonTags;
+ }
+ } else {
+ throw new Error("_onTagsChange called unexpectedly");
+ }
+
+ if (updateTagsField) {
+ this._initTagsField();
+ // Any tags change should be reflected in the tags selector.
+ if (this._element("tagsSelector")) {
+ this._rebuildTagsSelectorList();
+ }
+ }
+ },
+
+ _onItemTitleChange(aItemId, aNewTitle) {
+ if (aItemId == this._paneInfo.itemId) {
+ this._paneInfo.title = aNewTitle;
+ this._initTextField(this._namePicker, aNewTitle);
+ } else if (this._paneInfo.visibleRows.has("folderRow")) {
+ // If the title of a folder which is listed within the folders
+ // menulist has been changed, we need to update the label of its
+ // representing element.
+ let menupopup = this._folderMenuList.menupopup;
+ for (let menuitem of menupopup.childNodes) {
+ if ("folderId" in menuitem && menuitem.folderId == aItemId) {
+ menuitem.label = aNewTitle;
+ break;
+ }
+ }
+ }
+ // We need to also update title of recent folders.
+ if (this._recentFolders) {
+ for (let folder of this._recentFolders) {
+ if (folder.folderId == aItemId) {
+ folder.title = aNewTitle;
+ break;
+ }
+ }
+ }
+ },
+
+ // nsINavBookmarkObserver
+ onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue,
+ aLastModified, aItemType, aParentId, aGuid) {
+ if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow")) {
+ this._onTagsChange(aGuid).catch(Cu.reportError);
+ return;
+ }
+ if (aProperty == "title" && (this._paneInfo.isItem || this._paneInfo.isTag)) {
+ // This also updates titles of folders in the folder menu list.
+ this._onItemTitleChange(aItemId, aValue);
+ return;
+ }
+
+ if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) {
+ return;
+ }
+
+ switch (aProperty) {
+ case "uri":
+ let newURI = Services.io.newURI(aValue);
+ if (!newURI.equals(this._paneInfo.uri)) {
+ this._paneInfo.uri = newURI;
+ if (this._paneInfo.visibleRows.has("locationRow"))
+ this._initLocationField();
+
+ if (this._paneInfo.visibleRows.has("tagsRow")) {
+ delete this._paneInfo._cachedCommonTags;
+ this._onTagsChange(aGuid, newURI).catch(Cu.reportError);
+ }
+ }
+ break;
+ case "keyword":
+ if (this._paneInfo.visibleRows.has("keywordRow"))
+ this._initKeywordField(aValue).catch(Cu.reportError);
+ break;
+ case PlacesUIUtils.DESCRIPTION_ANNO:
+ if (this._paneInfo.visibleRows.has("descriptionRow"))
+ this._initDescriptionField();
+ break;
+ case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
+ if (this._paneInfo.visibleRows.has("loadInSidebarCheckbox"))
+ this._initLoadInSidebar();
+ break;
+ }
+ },
+
+ onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, type, guid,
+ oldParentGuid, newParentGuid) {
+ if (!this._paneInfo.isItem || this._paneInfo.itemId != id) {
+ return;
+ }
+
+ this._paneInfo.parentId = newParentId;
+ this._paneInfo.parentGuid = newParentGuid;
+
+ if (!this._paneInfo.visibleRows.has("folderRow") ||
+ newParentId == this._folderMenuList.selectedItem.folderId) {
+ return;
+ }
+
+ // Just setting selectItem _does not_ trigger oncommand, so we don't
+ // recurse.
+ PlacesUtils.bookmarks.fetch(newParentGuid).then(bm => {
+ this._folderMenuList.selectedItem = this._getFolderMenuItem(newParentId,
+ bm.title);
+ });
+ },
+
+ onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) {
+ this._lastNewItem = aItemId;
+ },
+
+ onItemRemoved() { },
+ onBeginUpdateBatch() { },
+ onEndUpdateBatch() { },
+ onItemVisited() { },
+};
+
+
+for (let elt of ["folderMenuList", "folderTree", "namePicker",
+ "locationField", "descriptionField", "keywordField",
+ "tagsField", "loadInSidebarCheckbox"]) {
+ let eltScoped = elt;
+ XPCOMUtils.defineLazyGetter(gEditItemOverlay, `_${eltScoped}`,
+ () => gEditItemOverlay._element(eltScoped));
+}
diff --git a/comm/suite/components/places/content/editBookmarkOverlay.xul b/comm/suite/components/places/content/editBookmarkOverlay.xul
new file mode 100644
index 0000000000..c4ff90a2bf
--- /dev/null
+++ b/comm/suite/components/places/content/editBookmarkOverlay.xul
@@ -0,0 +1,191 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/places/editBookmarkOverlay.dtd">
+%editBookmarkOverlayDTD;
+]>
+
+<?xml-stylesheet href="chrome://communicator/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
+
+<overlay id="editBookmarkOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="editBookmarkPanelContent" flex="1">
+ <hbox id="editBMPanel_selectionCount" pack="center">
+ <label id="editBMPanel_itemsCountText"/>
+ </hbox>
+
+ <grid id="editBookmarkPanelGrid" flex="1">
+ <columns id="editBMPanel_columns">
+ <column id="editBMPanel_labelColumn" />
+ <column flex="1" id="editBMPanel_editColumn" />
+ </columns>
+ <rows id="editBMPanel_rows">
+ <row id="editBMPanel_nameRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.name.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.name.accesskey;"
+ control="editBMPanel_namePicker"/>
+ <textbox id="editBMPanel_namePicker"
+ onchange="gEditItemOverlay.onNamePickerChange().catch(Cu.reportError);"/>
+ </row>
+
+ <row id="editBMPanel_locationRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.location.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.location.accesskey;"
+ control="editBMPanel_locationField"/>
+ <textbox id="editBMPanel_locationField"
+ class="uri-element"
+ onchange="gEditItemOverlay.onLocationFieldChange();"/>
+ </row>
+
+ <row id="editBMPanel_folderRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.folder.label;"
+ class="editBMPanel_rowLabel"
+ control="editBMPanel_folderMenuList"/>
+ <hbox flex="1" align="center">
+ <menulist id="editBMPanel_folderMenuList"
+ class="folder-icon"
+ flex="1"
+ oncommand="gEditItemOverlay.onFolderMenuListCommand(event).catch(Cu.reportError);">
+ <menupopup>
+ <!-- Static item for special folders -->
+ <menuitem id="editBMPanel_toolbarFolderItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuitem id="editBMPanel_bmRootItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuitem id="editBMPanel_unfiledRootItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuseparator id="editBMPanel_chooseFolderSeparator"/>
+ <menuitem id="editBMPanel_chooseFolderMenuItem"
+ label="&editBookmarkOverlay.choose.label;"
+ class="menuitem-iconic folder-icon"/>
+ <menuseparator id="editBMPanel_foldersSeparator" hidden="true"/>
+ </menupopup>
+ </menulist>
+ <button id="editBMPanel_foldersExpander"
+ class="expander-down"
+ tooltiptext="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
+ tooltiptextdown="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
+ tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
+ oncommand="gEditItemOverlay.toggleFolderTreeVisibility();"/>
+ </hbox>
+ </row>
+
+ <row id="editBMPanel_folderTreeRow"
+ collapsed="true"
+ flex="1">
+ <spacer/>
+ <vbox flex="1">
+ <tree id="editBMPanel_folderTree"
+ flex="1"
+ class="placesTree"
+ type="places"
+ treelines="true"
+ height="150"
+ minheight="150"
+ editable="true"
+ onselect="gEditItemOverlay.onFolderTreeSelect();"
+ hidecolumnpicker="true">
+ <treecols>
+ <treecol anonid="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <hbox id="editBMPanel_newFolderBox">
+ <button label="&editBookmarkOverlay.newFolderButton.label;"
+ id="editBMPanel_newFolderButton"
+ accesskey="&editBookmarkOverlay.newFolderButton.accesskey;"
+ oncommand="gEditItemOverlay.newFolder().catch(Cu.reportError);"/>
+ </hbox>
+ </vbox>
+ </row>
+
+ <row id="editBMPanel_tagsRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.tags.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.tags.accesskey;"
+ control="editBMPanel_tagsField"/>
+ <hbox flex="1" align="center">
+ <textbox id="editBMPanel_tagsField"
+ type="autocomplete"
+ flex="1"
+ autocompletesearch="places-tag-autocomplete"
+ autocompletepopup="PopupAutoComplete"
+ completedefaultindex="true"
+ tabscrolling="true"
+ placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"
+ onchange="gEditItemOverlay.onTagsFieldChange();"/>
+ <button id="editBMPanel_tagsSelectorExpander"
+ class="expander-down"
+ tooltiptext="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
+ tooltiptextdown="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
+ tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
+ oncommand="gEditItemOverlay.toggleTagsSelector();"/>
+ </hbox>
+ </row>
+
+ <row id="editBMPanel_tagsSelectorRow"
+ align="center"
+ collapsed="true">
+ <spacer/>
+ <listbox id="editBMPanel_tagsSelector"
+ height="150"/>
+ </row>
+
+ <row id="editBMPanel_keywordRow"
+ align="center"
+ collapsed="true">
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/>
+ <label value="&editBookmarkOverlay.keyword.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.keyword.accesskey;"
+ control="editBMPanel_keywordField"/>
+ <textbox id="editBMPanel_keywordField"
+ onchange="gEditItemOverlay.onKeywordFieldChange();"/>
+ </row>
+
+ <row id="editBMPanel_descriptionRow"
+ collapsed="true">
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/>
+ <label value="&editBookmarkOverlay.description.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.description.accesskey;"
+ control="editBMPanel_descriptionField"/>
+ <textbox id="editBMPanel_descriptionField"
+ multiline="true"
+ rows="4"
+ onchange="gEditItemOverlay.onDescriptionFieldChange();"/>
+ </row>
+ </rows>
+ </grid>
+
+ <checkbox id="editBMPanel_loadInSidebarCheckbox"
+ collapsed="true"
+ hidden="true"
+ disabled="true"
+ label="&editBookmarkOverlay.loadInSidebar.label;"
+ accesskey="&editBookmarkOverlay.loadInSidebar.accesskey;"
+ oncommand="gEditItemOverlay.onLoadInSidebarCheckboxCommand();">
+ <!-- Not yet supported
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/> -->
+ </checkbox>
+
+ <!-- If the ids are changing or additional fields are being added, be sure
+ to sync the values in places.js -->
+ <broadcaster id="additionalInfoBroadcaster"/>
+ </vbox>
+</overlay>
diff --git a/comm/suite/components/places/content/history-panel.js b/comm/suite/components/places/content/history-panel.js
new file mode 100644
index 0000000000..6353b51c66
--- /dev/null
+++ b/comm/suite/components/places/content/history-panel.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gHistoryTree;
+var gSearchBox;
+var gHistoryGrouping = "";
+var gSearching = false;
+
+function HistorySidebarInit() {
+ gHistoryTree = document.getElementById("historyTree");
+ gSearchBox = document.getElementById("search-box");
+
+ gHistoryGrouping = document.getElementById("viewButton").
+ getAttribute("selectedsort");
+
+ if (gHistoryGrouping == "site")
+ document.getElementById("bysite").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "visited")
+ document.getElementById("byvisited").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "lastvisited")
+ document.getElementById("bylastvisited").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "dayandsite")
+ document.getElementById("bydayandsite").setAttribute("checked", "true");
+ else
+ document.getElementById("byday").setAttribute("checked", "true");
+
+ searchHistory("");
+}
+
+function GroupBy(groupingType) {
+ gHistoryGrouping = groupingType;
+ searchHistory(gSearchBox.value);
+}
+
+function searchHistory(aInput) {
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ var sortingMode;
+ var resultType;
+
+ switch (gHistoryGrouping) {
+ case "visited":
+ resultType = NHQO.RESULTS_AS_URI;
+ sortingMode = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
+ break;
+ case "lastvisited":
+ resultType = NHQO.RESULTS_AS_URI;
+ sortingMode = NHQO.SORT_BY_DATE_DESCENDING;
+ break;
+ case "dayandsite":
+ resultType = NHQO.RESULTS_AS_DATE_SITE_QUERY;
+ break;
+ case "site":
+ resultType = NHQO.RESULTS_AS_SITE_QUERY;
+ sortingMode = NHQO.SORT_BY_TITLE_ASCENDING;
+ break;
+ case "day":
+ default:
+ resultType = NHQO.RESULTS_AS_DATE_QUERY;
+ break;
+ }
+
+ if (aInput) {
+ query.searchTerms = aInput;
+ if (gHistoryGrouping != "visited" && gHistoryGrouping != "lastvisited") {
+ sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING;
+ resultType = NHQO.RESULTS_AS_URI;
+ }
+ }
+
+ options.sortingMode = sortingMode;
+ options.resultType = resultType;
+ options.includeHidden = !!aInput;
+
+ // call load() on the tree manually
+ // instead of setting the place attribute in history-panel.xul
+ // otherwise, we will end up calling load() twice
+ gHistoryTree.load([query], options);
+}
+
+window.addEventListener("SidebarFocused",
+ () => gSearchBox.focus());
diff --git a/comm/suite/components/places/content/history-panel.xul b/comm/suite/components/places/content/history-panel.xul
new file mode 100644
index 0000000000..cb3b77ad17
--- /dev/null
+++ b/comm/suite/components/places/content/history-panel.xul
@@ -0,0 +1,95 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebarListView.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE page [
+<!ENTITY % placesDTD SYSTEM "chrome://communicator/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+]>
+
+<!-- we need to keep id="history-panel" for upgrade and switching
+ between versions of the browser -->
+
+<page id="history-panel" orient="vertical"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="HistorySidebarInit();"
+ onunload="SidebarUtils.setMouseoverURL('');">
+
+ <script src="chrome://communicator/content/bookmarks/sidebarUtils.js"/>
+ <script src="chrome://communicator/content/places/history-panel.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <commandset id="placesCommands"/>
+
+#include ../../../../../toolkit/content/editMenuKeys.inc.xhtml
+#ifdef XP_MACOSX
+ <keyset id="editMenuKeysExtra">
+ <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
+ </keyset>
+#endif
+
+ <!-- required to overlay the context menu -->
+ <menupopup id="placesContext"/>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip"/>
+
+ <hbox id="sidebar-search-container">
+ <textbox id="search-box" flex="1" type="search"
+ placeholder="&search.placeholder;"
+ aria-controls="historyTree"
+ oncommand="searchHistory(this.value);"/>
+ <button id="viewButton" style="min-width:0px !important;" type="menu"
+ label="&view.label;" accesskey="&view.accesskey;" selectedsort="day"
+ persist="selectedsort">
+ <menupopup>
+ <menuitem id="bydayandsite" label="&byDayAndSite.label;"
+ accesskey="&byDayAndSite.accesskey;" type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'dayandsite'); GroupBy('dayandsite');"/>
+ <menuitem id="bysite" label="&bySite.label;"
+ accesskey="&bySite.accesskey;" type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'site'); GroupBy('site');"/>
+ <menuitem id="byday" label="&byDate.label;"
+ accesskey="&byDate.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'day'); GroupBy('day');"/>
+ <menuitem id="byvisited" label="&byMostVisited.label;"
+ accesskey="&byMostVisited.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'visited'); GroupBy('visited');"/>
+ <menuitem id="bylastvisited" label="&byLastVisited.label;"
+ accesskey="&byLastVisited.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'lastvisited'); GroupBy('lastvisited');"/>
+ </menupopup>
+ </button>
+ </hbox>
+
+ <tree id="historyTree"
+ class="sidebar-placesTree"
+ flex="1"
+ type="places"
+ treelines="true"
+ context="placesContext"
+ hidecolumnpicker="true"
+ onkeypress="SidebarUtils.handleTreeKeyPress(event);"
+ onclick="SidebarUtils.handleTreeClick(this, event, true);"
+ onmousemove="SidebarUtils.handleTreeMouseMove(event);"
+ onmouseout="SidebarUtils.setMouseoverURL('');">
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/>
+ </tree>
+</page>
diff --git a/comm/suite/components/places/content/menu.xml b/comm/suite/components/places/content/menu.xml
new file mode 100644
index 0000000000..24af8629bb
--- /dev/null
+++ b/comm/suite/components/places/content/menu.xml
@@ -0,0 +1,624 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="placesMenuBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="places-popup-base"
+ extends="chrome://global/content/bindings/popup.xml#popup">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
+ <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
+ </xul:vbox>
+ <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
+ smoothscroll="false">
+ <children/>
+ </xul:arrowscrollbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+
+ <field name="AppConstants" readonly="true">
+ (ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+
+ <field name="_indicatorBar">
+ document.getAnonymousElementByAttribute(this, "class",
+ "menupopup-drop-indicator-bar");
+ </field>
+
+ <field name="_scrollBox">
+ document.getAnonymousElementByAttribute(this, "class",
+ "popup-internal-box");
+ </field>
+
+ <!-- This is the view that manage the popup -->
+ <field name="_rootView">PlacesUIUtils.getViewForNode(this);</field>
+
+ <!-- Check if we should hide the drop indicator for the target -->
+ <method name="_hideDropIndicator">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let target = aEvent.target;
+
+ // Don't draw the drop indicator outside of markers or if current
+ // node is not a Places node.
+ let betweenMarkers =
+ (this._startMarker.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING) &&
+ (this._endMarker.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_PRECEDING);
+
+ // Hide the dropmarker if current node is not a Places node.
+ return !(target && target._placesNode && betweenMarkers);
+ ]]></body>
+ </method>
+
+ <!-- This function returns information about where to drop when
+ dragging over this popup insertion point -->
+ <method name="_getDropPoint">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ // Can't drop if the menu isn't a folder
+ let resultNode = this._placesNode;
+
+ if (!PlacesUtils.nodeIsFolder(resultNode) ||
+ this._rootView.controller.disallowInsertion(resultNode)) {
+ return null;
+ }
+
+ var dropPoint = { ip: null, folderElt: null };
+
+ // The element we are dragging over
+ let elt = aEvent.target;
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ // Calculate positions taking care of arrowscrollbox
+ let scrollbox = this._scrollBox;
+ let eventY = aEvent.layerY + (scrollbox.boxObject.y - this.boxObject.y);
+ let scrollboxOffset = scrollbox.scrollBoxObject.y -
+ (scrollbox.boxObject.y - this.boxObject.y);
+ let eltY = elt.boxObject.y - scrollboxOffset;
+ let eltHeight = elt.boxObject.height;
+
+ if (!elt._placesNode) {
+ // If we are dragging over a non places node drop at the end.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(resultNode)
+ });
+ // We can set folderElt if we are dropping over a static menu that
+ // has an internal placespopup.
+ let isMenu = elt.localName == "menu" ||
+ (elt.localName == "toolbarbutton" &&
+ elt.getAttribute("type") == "menu");
+ if (isMenu && elt.lastChild &&
+ elt.lastChild.hasAttribute("placespopup"))
+ dropPoint.folderElt = elt;
+ return dropPoint;
+ }
+
+ let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+ elt._placesNode.title : null;
+ if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
+ !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this._rootView)) ||
+ PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
+ // This is a folder or a tag container.
+ if (eventY - eltY < eltHeight * 0.20) {
+ // If mouse is in the top part of the element, drop above folder.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ tagName,
+ dropNearNode: elt._placesNode
+ });
+ return dropPoint;
+ } else if (eventY - eltY < eltHeight * 0.80) {
+ // If mouse is in the middle of the element, drop inside folder.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(elt._placesNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
+ tagName
+ });
+ dropPoint.folderElt = elt;
+ return dropPoint;
+ }
+ } else if (eventY - eltY <= eltHeight / 2) {
+ // This is a non-folder node or a readonly folder.
+ // If the mouse is above the middle, drop above this item.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ tagName,
+ dropNearNode: elt._placesNode
+ });
+ return dropPoint;
+ }
+
+ // Drop below the item.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
+ orientation: Ci.nsITreeView.DROP_AFTER,
+ tagName,
+ dropNearNode: elt._placesNode,
+ });
+ return dropPoint;
+ ]]></body>
+ </method>
+
+ <!-- Sub-menus should be opened when the mouse drags over them, and closed
+ when the mouse drags off. The overFolder object manages opening and
+ closing of folders when the mouse hovers. -->
+ <field name="_overFolder"><![CDATA[({
+ _self: this,
+ _folder: {elt: null,
+ openTimer: null,
+ hoverTime: 350,
+ closeTimer: null},
+ _closeMenuTimer: null,
+
+ get elt() {
+ return this._folder.elt;
+ },
+ set elt(val) {
+ return this._folder.elt = val;
+ },
+
+ get openTimer() {
+ return this._folder.openTimer;
+ },
+ set openTimer(val) {
+ return this._folder.openTimer = val;
+ },
+
+ get hoverTime() {
+ return this._folder.hoverTime;
+ },
+ set hoverTime(val) {
+ return this._folder.hoverTime = val;
+ },
+
+ get closeTimer() {
+ return this._folder.closeTimer;
+ },
+ set closeTimer(val) {
+ return this._folder.closeTimer = val;
+ },
+
+ get closeMenuTimer() {
+ return this._closeMenuTimer;
+ },
+ set closeMenuTimer(val) {
+ return this._closeMenuTimer = val;
+ },
+
+ setTimer: function OF__setTimer(aTime) {
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ notify: function OF__notify(aTimer) {
+ // Function to process all timer notifications.
+
+ if (aTimer == this._folder.openTimer) {
+ // Timer to open a submenu that's being dragged over.
+ this._folder.elt.lastChild.setAttribute("autoopened", "true");
+ this._folder.elt.lastChild.showPopup(this._folder.elt);
+ this._folder.openTimer = null;
+ } else if (aTimer == this._folder.closeTimer) {
+ // Timer to close a submenu that's been dragged off of.
+ // Only close the submenu if the mouse isn't being dragged over any
+ // of its child menus.
+ var draggingOverChild = PlacesControllerDragHelper
+ .draggingOverChildNode(this._folder.elt);
+ if (draggingOverChild)
+ this._folder.elt = null;
+ this.clear();
+
+ // Close any parent folders which aren't being dragged over.
+ // (This is necessary because of the above code that keeps a folder
+ // open while its children are being dragged over.)
+ if (!draggingOverChild)
+ this.closeParentMenus();
+ } else if (aTimer == this.closeMenuTimer) {
+ // Timer to close this menu after the drag exit.
+ var popup = this._self;
+ // if we are no more dragging we can leave the menu open to allow
+ // for better D&D bookmark organization
+ if (PlacesControllerDragHelper.getSession() &&
+ !PlacesControllerDragHelper.draggingOverChildNode(popup.parentNode)) {
+ popup.hidePopup();
+ // Close any parent menus that aren't being dragged over;
+ // otherwise they'll stay open because they couldn't close
+ // while this menu was being dragged over.
+ this.closeParentMenus();
+ }
+ this._closeMenuTimer = null;
+ }
+ },
+
+ // Helper function to close all parent menus of this menu,
+ // as long as none of the parent's children are currently being
+ // dragged over.
+ closeParentMenus: function OF__closeParentMenus() {
+ var popup = this._self;
+ var parent = popup.parentNode;
+ while (parent) {
+ if (parent.localName == "menupopup" && parent._placesNode) {
+ if (PlacesControllerDragHelper.draggingOverChildNode(parent.parentNode))
+ break;
+ parent.hidePopup();
+ }
+ parent = parent.parentNode;
+ }
+ },
+
+ // The mouse is no longer dragging over the stored menubutton.
+ // Close the menubutton, clear out drag styles, and clear all
+ // timers for opening/closing it.
+ clear: function OF__clear() {
+ if (this._folder.elt && this._folder.elt.lastChild) {
+ if (!this._folder.elt.lastChild.hasAttribute("dragover"))
+ this._folder.elt.lastChild.hidePopup();
+ // remove menuactive style
+ this._folder.elt.removeAttribute("_moz-menuactive");
+ this._folder.elt = null;
+ }
+ if (this._folder.openTimer) {
+ this._folder.openTimer.cancel();
+ this._folder.openTimer = null;
+ }
+ if (this._folder.closeTimer) {
+ this._folder.closeTimer.cancel();
+ this._folder.closeTimer = null;
+ }
+ }
+ })]]></field>
+
+ <method name="_cleanupDragDetails">
+ <body><![CDATA[
+ // Called on dragend and drop.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this._rootView._draggedElt = null;
+ this.removeAttribute("dragover");
+ this.removeAttribute("dragstart");
+ this._indicatorBar.hidden = true;
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="DOMMenuItemActive"><![CDATA[
+ let elt = event.target;
+ if (elt.parentNode != this)
+ return;
+
+ if (this.AppConstants.platform === "macosx") {
+ // XXX: The following check is a temporary hack until bug 420033 is
+ // resolved.
+ let parentElt = elt.parent;
+ while (parentElt) {
+ if (parentElt.id == "bookmarksMenuPopup" ||
+ parentElt.id == "goPopup")
+ return;
+
+ parentElt = parentElt.parentNode;
+ }
+ }
+
+ if (window.XULBrowserWindow) {
+ let placesNode = elt._placesNode;
+
+ var linkURI;
+ if (placesNode && PlacesUtils.nodeIsURI(placesNode))
+ linkURI = placesNode.uri;
+ else if (elt.hasAttribute("targetURI"))
+ linkURI = elt.getAttribute("targetURI");
+
+ if (linkURI)
+ window.XULBrowserWindow.setOverLink(linkURI, null);
+ }
+ ]]></handler>
+
+ <handler event="DOMMenuItemInactive"><![CDATA[
+ let elt = event.target;
+ if (elt.parentNode != this)
+ return;
+
+ if (window.XULBrowserWindow)
+ window.XULBrowserWindow.setOverLink("", null);
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ let elt = event.target;
+ if (!elt._placesNode)
+ return;
+
+ let draggedElt = elt._placesNode;
+
+ // Force a copy action if parent node is a query or we are dragging a
+ // not-removable node.
+ if (!this._rootView.controller.canMoveNode(draggedElt))
+ event.dataTransfer.effectAllowed = "copyLink";
+
+ // Activate the view and cache the dragged element.
+ this._rootView._draggedElt = draggedElt;
+ this._rootView.controller.setDataTransfer(event);
+ this.setAttribute("dragstart", "true");
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+
+ let dropPoint = this._getDropPoint(event);
+ if (dropPoint && dropPoint.ip) {
+ PlacesControllerDragHelper.onDrop(dropPoint.ip, event.dataTransfer)
+ .catch(Cu.reportError);
+ event.preventDefault();
+ }
+
+ this._cleanupDragDetails();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let dt = event.dataTransfer;
+
+ let dropPoint = this._getDropPoint(event);
+ if (!dropPoint || !dropPoint.ip ||
+ !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
+ this._indicatorBar.hidden = true;
+ event.stopPropagation();
+ return;
+ }
+
+ // Mark this popup as being dragged over.
+ this.setAttribute("dragover", "true");
+
+ if (dropPoint.folderElt) {
+ // We are dragging over a folder.
+ // _overFolder should take the care of opening it on a timer.
+ if (this._overFolder.elt &&
+ this._overFolder.elt != dropPoint.folderElt) {
+ // We are dragging over a new folder, let's clear old values
+ this._overFolder.clear();
+ }
+ if (!this._overFolder.elt) {
+ this._overFolder.elt = dropPoint.folderElt;
+ // Create the timer to open this folder.
+ this._overFolder.openTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+ // Since we are dropping into a folder set the corresponding style.
+ dropPoint.folderElt.setAttribute("_moz-menuactive", true);
+ } else {
+ // We are not dragging over a folder.
+ // Clear out old _overFolder information.
+ this._overFolder.clear();
+ }
+
+ // Autoscroll the popup strip if we drag over the scroll buttons.
+ let anonid = event.originalTarget.getAttribute("anonid");
+ let scrollDir = 0;
+ if (anonid == "scrollbutton-up") {
+ scrollDir = -1;
+ } else if (anonid == "scrollbutton-down") {
+ scrollDir = 1;
+ }
+ if (scrollDir != 0) {
+ this._scrollBox.scrollByIndex(scrollDir, true);
+ }
+
+ // Check if we should hide the drop indicator for this target.
+ if (dropPoint.folderElt || this._hideDropIndicator(event)) {
+ this._indicatorBar.hidden = true;
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ // We should display the drop indicator relative to the arrowscrollbox.
+ let sbo = this._scrollBox.scrollBoxObject;
+ let newMarginTop = 0;
+ if (scrollDir == 0) {
+ let elt = this.firstChild;
+ while (elt && event.screenY > elt.boxObject.screenY +
+ elt.boxObject.height / 2)
+ elt = elt.nextSibling;
+ newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY :
+ sbo.height;
+ } else if (scrollDir == 1)
+ newMarginTop = sbo.height;
+
+ // Set the new marginTop based on arrowscrollbox.
+ newMarginTop += sbo.y - this._scrollBox.boxObject.y;
+ this._indicatorBar.firstChild.style.marginTop = newMarginTop + "px";
+ this._indicatorBar.hidden = false;
+
+ event.preventDefault();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragexit"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this.removeAttribute("dragover");
+
+ // If we have not moved to a valid new target clear the drop indicator
+ // this happens when moving out of the popup.
+ let target = event.relatedTarget;
+ if (!target || !this.contains(target))
+ this._indicatorBar.hidden = true;
+
+ // Close any folder being hovered over
+ if (this._overFolder.elt) {
+ this._overFolder.closeTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+
+ // The autoopened attribute is set when this folder was automatically
+ // opened after the user dragged over it. If this attribute is set,
+ // auto-close the folder on drag exit.
+ // We should also try to close this popup if the drag has started
+ // from here, the timer will check if we are dragging over a child.
+ if (this.hasAttribute("autoopened") ||
+ this.hasAttribute("dragstart")) {
+ this._overFolder.closeMenuTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ this._cleanupDragDetails();
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <!-- Most of this is copied from the arrowpanel binding in popup.xml -->
+ <binding id="places-popup-arrow"
+ extends="chrome://communicator/content/places/menu.xml#places-popup-base">
+ <content flip="both" side="top" position="bottomcenter topright">
+ <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
+ xbl:inherits="side,panelopen">
+ <xul:box anonid="arrowbox" class="panel-arrowbox">
+ <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
+ </xul:box>
+ <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
+ <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
+ <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
+ </xul:vbox>
+ <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
+ smoothscroll="false">
+ <children/>
+ </xul:arrowscrollbox>
+ </xul:box>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.style.pointerEvents = "none";
+ ]]></constructor>
+ <method name="adjustArrowPosition">
+ <body><![CDATA[
+ var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
+
+ var anchor = this.anchorNode;
+ if (!anchor) {
+ arrow.hidden = true;
+ return;
+ }
+
+ var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+ var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
+
+ var position = this.alignmentPosition;
+ var offset = this.alignmentOffset;
+
+ this.setAttribute("arrowposition", position);
+
+ // if this panel has a "sliding" arrow, we may have previously set margins...
+ arrowbox.style.removeProperty("transform");
+ if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
+ container.orient = "horizontal";
+ arrowbox.orient = "vertical";
+ if (position.indexOf("_after") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(0, " + -offset + "px)";
+
+ // The assigned side stays the same regardless of direction.
+ var isRTL = (window.getComputedStyle(this).direction == "rtl");
+
+ if (position.indexOf("start_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", isRTL ? "left" : "right");
+ } else {
+ container.dir = "";
+ this.setAttribute("side", isRTL ? "right" : "left");
+ }
+ } else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
+ container.orient = "";
+ arrowbox.orient = "";
+ if (position.indexOf("_end") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(" + -offset + "px, 0)";
+
+ if (position.indexOf("before_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", "bottom");
+ } else {
+ container.dir = "";
+ this.setAttribute("side", "top");
+ }
+ }
+
+ arrow.hidden = false;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing" phase="target"><![CDATA[
+ this.adjustArrowPosition();
+ this.setAttribute("animate", "open");
+ ]]></handler>
+ <handler event="popupshown" phase="target"><![CDATA[
+ this.setAttribute("panelopen", "true");
+ let disablePointerEvents;
+ if (!this.hasAttribute("disablepointereventsfortransition")) {
+ let container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+ let cs = getComputedStyle(container);
+ let transitionProp = cs.transitionProperty;
+ let transitionTime = parseFloat(cs.transitionDuration);
+ disablePointerEvents = (transitionProp.includes("transform") ||
+ transitionProp == "all") &&
+ transitionTime > 0;
+ this.setAttribute("disablepointereventsfortransition", disablePointerEvents);
+ } else {
+ disablePointerEvents = this.getAttribute("disablepointereventsfortransition") == "true";
+ }
+ if (!disablePointerEvents) {
+ this.style.removeProperty("pointer-events");
+ }
+ ]]></handler>
+ <handler event="transitionend"><![CDATA[
+ if (event.originalTarget.getAttribute("anonid") == "container" &&
+ (event.propertyName == "transform" || event.propertyName == "-moz-window-transform")) {
+ this.style.removeProperty("pointer-events");
+ }
+ ]]></handler>
+ <handler event="popuphiding" phase="target"><![CDATA[
+ this.setAttribute("animate", "cancel");
+ ]]></handler>
+ <handler event="popuphidden" phase="target"><![CDATA[
+ this.removeAttribute("panelopen");
+ if (this.getAttribute("disablepointereventsfortransition") == "true") {
+ this.style.pointerEvents = "none";
+ }
+ this.removeAttribute("animate");
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/places/content/organizer.css b/comm/suite/components/places/content/organizer.css
new file mode 100644
index 0000000000..47b1832c16
--- /dev/null
+++ b/comm/suite/components/places/content/organizer.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#searchFilter {
+ width: 23em;
+}
diff --git a/comm/suite/components/places/content/places.css b/comm/suite/components/places/content/places.css
new file mode 100644
index 0000000000..598e60b256
--- /dev/null
+++ b/comm/suite/components/places/content/places.css
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+tree[type="places"] {
+ -moz-binding: url("chrome://communicator/content/places/tree.xml#places-tree");
+}
+
+tree[type="places"] > treechildren::-moz-tree-cell {
+ /* ensure we use the direction of the website title / url instead of the
+ * browser locale */
+ unicode-bidi: plaintext;
+}
+
+#bhtTitleText {
+ /* ensure we use the direction of the website title instead of the
+ * browser locale */
+ unicode-bidi: plaintext;
+}
+
+.toolbar-drop-indicator {
+ position: relative;
+ z-index: 1;
+}
+
+menupopup[placespopup="true"] {
+ -moz-binding: url("chrome://communicator/content/places/menu.xml#places-popup-base");
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #bookmarksChildren,
+ .sidebar-placesTreechildren,
+ .placesTree > treechildren {
+ image-rendering: -moz-crisp-edges;
+ }
+}
diff --git a/comm/suite/components/places/content/places.js b/comm/suite/components/places/content/places.js
new file mode 100644
index 0000000000..f793bf4184
--- /dev/null
+++ b/comm/suite/components/places/content/places.js
@@ -0,0 +1,1366 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from editBookmarkOverlay.js */
+/* import-globals-from ../../../../../toolkit/content/contentAreaUtils.js */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ChromeUtils.defineModuleGetter(this, "MigrationUtils",
+ "resource:///modules/MigrationUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+ChromeUtils.defineModuleGetter(this, "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const RESTORE_FILEPICKER_FILTER_EXT = "*.json;*.jsonlz4";
+
+var PlacesOrganizer = {
+ _places: null,
+
+ // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
+ // is minimal. IDs should be kept in sync with the IDs of the elements
+ // observing additionalInfoBroadcaster.
+ _additionalInfoFields: [
+ "editBMPanel_descriptionRow",
+ "editBMPanel_loadInSidebarCheckbox",
+ "editBMPanel_keywordRow",
+ ],
+
+ _initFolderTree() {
+ var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
+ this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
+ },
+
+ /**
+ * Selects a left pane built-in item.
+ *
+ * @param {String} item The built-in item to select, may be one of (case sensitive):
+ * AllBookmarks, BookmarksMenu, BookmarksToolbar,
+ * History, Tags, UnfiledBookmarks.
+ */
+ selectLeftPaneBuiltIn(item) {
+ switch (item) {
+ case "AllBookmarks":
+ case "History":
+ case "Tags": {
+ var itemId = PlacesUIUtils.leftPaneQueries[item];
+ this._places.selectItems([itemId]);
+ // Forcefully expand all-bookmarks
+ if (item == "AllBookmarks" || item == "History")
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ break;
+ }
+ case "BookmarksMenu":
+ this.selectLeftPaneContainerByHierarchy([
+ PlacesUIUtils.leftPaneQueries.AllBookmarks,
+ PlacesUtils.bookmarks.virtualMenuGuid
+ ]);
+ break;
+ case "BookmarksToolbar":
+ this.selectLeftPaneContainerByHierarchy([
+ PlacesUIUtils.leftPaneQueries.AllBookmarks,
+ PlacesUtils.bookmarks.virtualToolbarGuid
+ ]);
+ break;
+ case "UnfiledBookmarks":
+ this.selectLeftPaneContainerByHierarchy([
+ PlacesUIUtils.leftPaneQueries.AllBookmarks,
+ PlacesUtils.bookmarks.virtualUnfiledGuid
+ ]);
+ break;
+ default:
+ throw new Error(`Unrecognized item ${item} passed to selectLeftPaneRootItem`);
+ }
+ },
+
+ /**
+ * Opens a given hierarchy in the left pane, stopping at the last reachable
+ * container. Note: item ids should be considered deprecated.
+ *
+ * @param aHierarchy A single container or an array of containers, sorted from
+ * the outmost to the innermost in the hierarchy. Each
+ * container may be either an item id, a Places URI string,
+ * or a named query, like:
+ * "BookmarksMenu", "BookmarksToolbar", "UnfiledBookmarks", "AllBookmarks".
+ * @see PlacesUIUtils.leftPaneQueries for supported named queries.
+ */
+ selectLeftPaneContainerByHierarchy(aHierarchy) {
+ if (!aHierarchy)
+ throw new Error("Containers hierarchy not specified");
+ let hierarchy = [].concat(aHierarchy);
+ let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed;
+ if (!selectWasSuppressed)
+ this._places.view.selection.selectEventsSuppressed = true;
+ try {
+ for (let container of hierarchy) {
+ switch (typeof container) {
+ case "number":
+ this._places.selectItems([container], false);
+ break;
+ case "string":
+ try {
+ this.selectLeftPaneBuiltIn(container);
+ } catch (ex) {
+ if (container.substr(0, 6) == "place:") {
+ this._places.selectPlaceURI(container);
+ } else {
+ // May be a guid.
+ this._places.selectItems([container], false);
+ }
+ }
+ break;
+ default:
+ throw new Error("Invalid container type found: " + container);
+ }
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ }
+ } finally {
+ if (!selectWasSuppressed)
+ this._places.view.selection.selectEventsSuppressed = false;
+ }
+ },
+
+ init: function PO_init() {
+ ContentArea.init();
+
+ this._places = document.getElementById("placesList");
+ this._initFolderTree();
+
+ var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
+ if (window.arguments && window.arguments[0])
+ leftPaneSelection = window.arguments[0];
+
+ this.selectLeftPaneContainerByHierarchy(leftPaneSelection);
+ if (leftPaneSelection === "History") {
+ let historyNode = this._places.selectedNode;
+ if (historyNode.childCount > 0)
+ this._places.selectNode(historyNode.getChild(0));
+ }
+
+ // clear the back-stack
+ this._backHistory.splice(0, this._backHistory.length);
+ document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
+
+ // Set up the search UI.
+ PlacesSearchBox.init();
+
+ window.addEventListener("AppCommand", this, true);
+
+ // remove the "Properties" context-menu item, we've our own details pane
+ document.getElementById("placesContext")
+ .removeChild(document.getElementById("placesContext_show:info"));
+
+ ContentArea.focus();
+ },
+
+ QueryInterface: function PO_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ handleEvent: function PO_handleEvent(aEvent) {
+ if (aEvent.type != "AppCommand")
+ return;
+
+ aEvent.stopPropagation();
+ switch (aEvent.command) {
+ case "Back":
+ if (this._backHistory.length > 0)
+ this.back();
+ break;
+ case "Forward":
+ if (this._forwardHistory.length > 0)
+ this.forward();
+ break;
+ case "Search":
+ PlacesSearchBox.findAll();
+ break;
+ }
+ },
+
+ destroy: function PO_destroy() {
+ },
+
+ _location: null,
+ get location() {
+ return this._location;
+ },
+
+ set location(aLocation) {
+ if (!aLocation || this._location == aLocation)
+ return aLocation;
+
+ if (this.location) {
+ this._backHistory.unshift(this.location);
+ this._forwardHistory.splice(0, this._forwardHistory.length);
+ }
+
+ this._location = aLocation;
+ this._places.selectPlaceURI(aLocation);
+
+ if (!this._places.hasSelection) {
+ // If no node was found for the given place: uri, just load it directly
+ ContentArea.currentPlace = aLocation;
+ }
+ this.updateDetailsPane();
+
+ // update navigation commands
+ if (this._backHistory.length == 0)
+ document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
+ else
+ document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
+ if (this._forwardHistory.length == 0)
+ document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
+ else
+ document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
+
+ return aLocation;
+ },
+
+ _backHistory: [],
+ _forwardHistory: [],
+
+ back: function PO_back() {
+ this._forwardHistory.unshift(this.location);
+ var historyEntry = this._backHistory.shift();
+ this._location = null;
+ this.location = historyEntry;
+ },
+ forward: function PO_forward() {
+ this._backHistory.unshift(this.location);
+ var historyEntry = this._forwardHistory.shift();
+ this._location = null;
+ this.location = historyEntry;
+ },
+
+ /**
+ * Called when a place folder is selected in the left pane.
+ * @param resetSearchBox
+ * true if the search box should also be reset, false otherwise.
+ * The search box should be reset when a new folder in the left
+ * pane is selected; the search scope and text need to be cleared in
+ * preparation for the new folder. Note that if the user manually
+ * resets the search box, either by clicking its reset button or by
+ * deleting its text, this will be false.
+ */
+ _cachedLeftPaneSelectedURI: null,
+ onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
+ // Don't change the right-hand pane contents when there's no selection.
+ if (!this._places.hasSelection)
+ return;
+
+ let node = this._places.selectedNode;
+ let placeURI = node.uri;
+
+ // If either the place of the content tree in the right pane has changed or
+ // the user cleared the search box, update the place, hide the search UI,
+ // and update the back/forward buttons by setting location.
+ if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
+ ContentArea.currentPlace = placeURI;
+ this.location = placeURI;
+ }
+
+ // When we invalidate a container we use suppressSelectionEvent, when it is
+ // unset a select event is fired, in many cases the selection did not really
+ // change, so we should check for it, and return early in such a case. Note
+ // that we cannot return any earlier than this point, because when
+ // !resetSearchBox, we need to update location and hide the UI as above,
+ // even though the selection has not changed.
+ if (placeURI == this._cachedLeftPaneSelectedURI)
+ return;
+ this._cachedLeftPaneSelectedURI = placeURI;
+
+ // At this point, resetSearchBox is true, because the left pane selection
+ // has changed; otherwise we would have returned earlier.
+
+ PlacesSearchBox.searchFilter.reset();
+ this._setSearchScopeForNode(node);
+ this.updateDetailsPane();
+ },
+
+ /**
+ * Sets the search scope based on aNode's properties.
+ * @param aNode
+ * the node to set up scope from
+ */
+ _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
+ let itemId = aNode.itemId;
+
+ if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
+ itemId == PlacesUIUtils.leftPaneQueries.History) {
+ PlacesQueryBuilder.setScope("history");
+ } else {
+ // Default to All Bookmarks for all other nodes, per bug 469437.
+ PlacesQueryBuilder.setScope("bookmarks");
+ }
+ },
+
+ /**
+ * Handle clicks on the places list.
+ * Single Left click, right click or modified click do not result in any
+ * special action, since they're related to selection.
+ * @param aEvent
+ * The mouse event.
+ */
+ onPlacesListClick: function PO_onPlacesListClick(aEvent) {
+ // Only handle clicks on tree children.
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ let node = this._places.selectedNode;
+ if (node) {
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (middleClick && PlacesUtils.nodeIsContainer(node)) {
+ // The command execution function will take care of seeing if the
+ // selection is a folder or a different container type, and will
+ // load its contents in tabs.
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._places);
+ }
+ }
+ },
+
+ /**
+ * Handle focus changes on the places list and the current content view.
+ */
+ updateDetailsPane: function PO_updateDetailsPane() {
+ if (!ContentArea.currentViewOptions.showDetailsPane)
+ return;
+ let view = PlacesUIUtils.getViewForNode(document.activeElement);
+ if (view) {
+ let selectedNodes = view.selectedNode ?
+ [view.selectedNode] : view.selectedNodes;
+ this._fillDetailsPane(selectedNodes);
+ }
+ },
+
+ openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
+ if (aContainer.itemId != -1) {
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ this._places.selectItems([aContainer.itemId], false);
+ } else if (PlacesUtils.nodeIsQuery(aContainer)) {
+ this._places.selectPlaceURI(aContainer.uri);
+ }
+ },
+
+ /**
+ * Returns the options associated with the query currently loaded in the
+ * main places pane.
+ */
+ getCurrentOptions: function PO_getCurrentOptions() {
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
+ },
+
+ /**
+ * Returns the queries associated with the query currently loaded in the
+ * main places pane.
+ */
+ getCurrentQueries: function PO_getCurrentQueries() {
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
+ },
+
+ /**
+ * Show the migration wizard for importing passwords,
+ * cookies, history, preferences, and bookmarks.
+ */
+ importFromBrowser: function PO_importFromBrowser() {
+ // We pass in the type of source we're using for future use:
+ MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_PLACES]);
+ },
+
+ /**
+ * Open a file-picker and import the selected file into the bookmarks store
+ */
+ importFromFile: function PO_importFromFile() {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
+ var {BookmarkHTMLUtils} = ChromeUtils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+ BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
+ .catch(Cu.reportError);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("SelectImport"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Allows simple exporting of bookmarks.
+ */
+ exportBookmarks: function PO_exportBookmarks() {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ var {BookmarkHTMLUtils} = ChromeUtils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+ BookmarkHTMLUtils.exportToFile(fp.file.path)
+ .catch(Cu.reportError);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("EnterExport"),
+ Ci.nsIFilePicker.modeSave);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.defaultString = "bookmarks.html";
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Populates the restore menu with the dates of the backups available.
+ */
+ populateRestoreMenu: function PO_populateRestoreMenu() {
+ let restorePopup = document.getElementById("fileRestorePopup");
+
+ const dtOptions = {
+ dateStyle: "long"
+ };
+ let dateFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
+
+ // Remove existing menu items. Last item is the restoreFromFile item.
+ while (restorePopup.childNodes.length > 1)
+ restorePopup.firstChild.remove();
+
+ (async function() {
+ let backupFiles = await PlacesBackups.getBackupFiles();
+ if (backupFiles.length == 0)
+ return;
+
+ // Populate menu with backups.
+ for (let i = 0; i < backupFiles.length; i++) {
+ let fileSize = (await OS.File.stat(backupFiles[i])).size;
+ let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
+ let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
+ [size, unit]);
+ let sizeInfo;
+ let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
+ if (bookmarkCount != null) {
+ sizeInfo = " (" + sizeString + " - " +
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ bookmarkCount,
+ [bookmarkCount]) +
+ ")";
+ } else {
+ sizeInfo = " (" + sizeString + ")";
+ }
+
+ let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
+ let m = restorePopup.insertBefore(document.createElement("menuitem"),
+ document.getElementById("restoreFromFile"));
+ m.setAttribute("label", dateFormatter.format(backupDate) + sizeInfo);
+ m.setAttribute("value", OS.Path.basename(backupFiles[i]));
+ m.setAttribute("oncommand",
+ "PlacesOrganizer.onRestoreMenuItemClick(this);");
+ }
+
+ // Add the restoreFromFile item.
+ restorePopup.insertBefore(document.createElement("menuseparator"),
+ document.getElementById("restoreFromFile"));
+ })();
+ },
+
+ /**
+ * Called when a menuitem is selected from the restore menu.
+ */
+ async onRestoreMenuItemClick(aMenuItem) {
+ let backupName = aMenuItem.getAttribute("value");
+ let backupFilePaths = await PlacesBackups.getBackupFiles();
+ for (let backupFilePath of backupFilePaths) {
+ if (OS.Path.basename(backupFilePath) == backupName) {
+ PlacesOrganizer.restoreBookmarksFromFile(backupFilePath);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when 'Choose File...' is selected from the restore menu.
+ * Prompts for a file and restores bookmarks to those in the file.
+ */
+ onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
+ let backupsDir = Services.dirsvc.get("Desk", Ci.nsIFile);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ this.restoreBookmarksFromFile(fp.file.path);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
+ RESTORE_FILEPICKER_FILTER_EXT);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.displayDirectory = backupsDir;
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Restores bookmarks from a JSON file.
+ */
+ restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) {
+ // check file extension
+ if (!aFilePath.toLowerCase().endsWith("json") &&
+ !aFilePath.toLowerCase().endsWith("jsonlz4")) {
+ this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
+ return;
+ }
+
+ // confirm ok to delete existing bookmarks
+ if (!Services.prompt.confirm(null,
+ PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
+ PlacesUIUtils.getString("bookmarksRestoreAlert")))
+ return;
+
+ (async function() {
+ try {
+ await BookmarkJSONUtils.importFromFile(aFilePath, true);
+ } catch (ex) {
+ PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
+ }
+ })();
+ },
+
+ _showErrorAlert: function PO__showErrorAlert(aMsg) {
+ var brandShortName = document.getElementById("brandStrings").
+ getString("brandShortName");
+
+ Services.prompt.alert(window, brandShortName, aMsg);
+ },
+
+ /**
+ * Backup bookmarks to desktop, auto-generate a filename with a date.
+ * The file is a JSON serialization of bookmarks, tags and any annotations
+ * of those items.
+ */
+ backupBookmarks: function PO_backupBookmarks() {
+ let backupsDir = Services.dirsvc.get("Desk", Ci.nsIFile);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ // There is no OS.File version of the filepicker yet (Bug 937812).
+ PlacesBackups.saveBookmarksToJSONFile(fp.file.path)
+ .catch(Cu.reportError);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
+ Ci.nsIFilePicker.modeSave);
+ fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
+ RESTORE_FILEPICKER_FILTER_EXT);
+ fp.defaultString = PlacesBackups.getFilenameForDate();
+ fp.defaultExtension = "json";
+ fp.displayDirectory = backupsDir;
+ fp.open(fpCallback);
+ },
+
+ _detectAndSetDetailsPaneMinimalState:
+ function PO__detectAndSetDetailsPaneMinimalState(aNode) {
+ /**
+ * The details of simple folder-items (as opposed to livemarks) or the
+ * of livemark-children are not likely to fill the infoBox anyway,
+ * thus we remove the "More/Less" button and show all details.
+ *
+ * the wasminimal attribute here is used to persist the "more/less"
+ * state in a bookmark->folder->bookmark scenario.
+ */
+ var infoBox = document.getElementById("infoBox");
+ var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
+ var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
+
+ if (!aNode) {
+ infoBoxExpanderWrapper.hidden = true;
+ return;
+ }
+ if (aNode.itemId != -1 &&
+ PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) {
+ if (infoBox.getAttribute("minimal") == "true")
+ infoBox.setAttribute("wasminimal", "true");
+ infoBox.removeAttribute("minimal");
+ infoBoxExpanderWrapper.hidden = true;
+ } else {
+ if (infoBox.getAttribute("wasminimal") == "true")
+ infoBox.setAttribute("minimal", "true");
+ infoBox.removeAttribute("wasminimal");
+ infoBoxExpanderWrapper.hidden =
+ this._additionalInfoFields.every(id =>
+ document.getElementById(id).collapsed);
+ }
+ additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
+ },
+
+ // NOT YET USED
+ updateThumbnailProportions: function PO_updateThumbnailProportions() {
+ var previewBox = document.getElementById("previewBox");
+ var canvas = document.getElementById("itemThumbnail");
+ var height = previewBox.boxObject.height;
+ var width = height * (screen.width / screen.height);
+ canvas.width = width;
+ canvas.height = height;
+ },
+
+ _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
+ var infoBox = document.getElementById("infoBox");
+ var detailsDeck = document.getElementById("detailsDeck");
+
+ // Make sure the infoBox UI is visible if we need to use it, we hide it
+ // below when we don't.
+ infoBox.hidden = false;
+ let selectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
+
+ // If a textbox within a panel is focused, force-blur it so its contents
+ // are saved
+ if (gEditItemOverlay.itemId != -1) {
+ var focusedElement = document.commandDispatcher.focusedElement;
+ if ((focusedElement instanceof HTMLInputElement ||
+ focusedElement instanceof HTMLTextAreaElement) &&
+ /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
+ focusedElement.blur();
+
+ // don't update the panel if we are already editing this node unless we're
+ // in multi-edit mode
+ if (selectedNode) {
+ let concreteId = PlacesUtils.getConcreteItemId(selectedNode);
+ var nodeIsSame = gEditItemOverlay.itemId == selectedNode.itemId ||
+ gEditItemOverlay.itemId == concreteId ||
+ (selectedNode.itemId == -1 && gEditItemOverlay.uri &&
+ gEditItemOverlay.uri == selectedNode.uri);
+ if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
+ !gEditItemOverlay.multiEdit)
+ return;
+ }
+ }
+
+ // Clean up the panel before initing it again.
+ gEditItemOverlay.uninitPanel(false);
+
+ if (selectedNode && !PlacesUtils.nodeIsSeparator(selectedNode)) {
+ detailsDeck.selectedIndex = 1;
+
+ gEditItemOverlay.initPanel({ node: selectedNode,
+ hiddenRows: ["folderPicker"] });
+
+ this._detectAndSetDetailsPaneMinimalState(selectedNode);
+ } else if (!selectedNode && aNodeList[0]) {
+ if (aNodeList.every(PlacesUtils.nodeIsURI)) {
+ let uris = aNodeList.map(node => Services.io.newURI(node.uri));
+ detailsDeck.selectedIndex = 1;
+ gEditItemOverlay.initPanel({ uris,
+ hiddenRows: ["folderPicker",
+ "loadInSidebar",
+ "location",
+ "keyword",
+ "description",
+ "name"]});
+ this._detectAndSetDetailsPaneMinimalState(selectedNode);
+ } else {
+ detailsDeck.selectedIndex = 0;
+ let selectItemDesc = document.getElementById("selectItemDescription");
+ let itemsCountLabel = document.getElementById("itemsCountText");
+ selectItemDesc.hidden = false;
+ itemsCountLabel.value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ aNodeList.length, [aNodeList.length]);
+ infoBox.hidden = true;
+ }
+ } else {
+ detailsDeck.selectedIndex = 0;
+ infoBox.hidden = true;
+ let selectItemDesc = document.getElementById("selectItemDescription");
+ let itemsCountLabel = document.getElementById("itemsCountText");
+ let itemsCount = 0;
+ if (ContentArea.currentView.result) {
+ let rootNode = ContentArea.currentView.result.root;
+ if (rootNode.containerOpen)
+ itemsCount = rootNode.childCount;
+ }
+ if (itemsCount == 0) {
+ selectItemDesc.hidden = true;
+ itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
+ } else {
+ selectItemDesc.hidden = false;
+ itemsCountLabel.value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ itemsCount, [itemsCount]);
+ }
+ }
+ },
+
+ // NOT YET USED
+ _updateThumbnail: function PO__updateThumbnail() {
+ var bo = document.getElementById("previewBox").boxObject;
+ var width = bo.width;
+ var height = bo.height;
+
+ var canvas = document.getElementById("itemThumbnail");
+ var ctx = canvas.getContext("2d");
+ var notAvailableText = canvas.getAttribute("notavailabletext");
+ ctx.save();
+ ctx.fillStyle = "-moz-Dialog";
+ ctx.fillRect(0, 0, width, height);
+ ctx.translate(width / 2, height / 2);
+
+ ctx.fillStyle = "GrayText";
+ ctx.mozTextStyle = "12pt sans serif";
+ var len = ctx.mozMeasureText(notAvailableText);
+ ctx.translate(-len / 2, 0);
+ ctx.mozDrawText(notAvailableText);
+ ctx.restore();
+ },
+
+ toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
+ var infoBox = document.getElementById("infoBox");
+ var infoBoxExpander = document.getElementById("infoBoxExpander");
+ var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
+ var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
+
+ if (infoBox.getAttribute("minimal") == "true") {
+ infoBox.removeAttribute("minimal");
+ infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
+ infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
+ infoBoxExpander.className = "expander-up";
+ additionalInfoBroadcaster.removeAttribute("hidden");
+ } else {
+ infoBox.setAttribute("minimal", "true");
+ infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
+ infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
+ infoBoxExpander.className = "expander-down";
+ additionalInfoBroadcaster.setAttribute("hidden", "true");
+ }
+ },
+};
+
+/**
+ * A set of utilities relating to search within Bookmarks and History.
+ */
+var PlacesSearchBox = {
+
+ /**
+ * The Search text field
+ */
+ get searchFilter() {
+ return document.getElementById("searchFilter");
+ },
+
+ /**
+ * Folders to include when searching.
+ */
+ _folders: [],
+ get folders() {
+ if (this._folders.length == 0) {
+ this._folders.push(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId,
+ PlacesUtils.mobileFolderId);
+ }
+ return this._folders;
+ },
+ set folders(aFolders) {
+ this._folders = aFolders;
+ return aFolders;
+ },
+
+ /**
+ * Run a search for the specified text, over the collection specified by
+ * the dropdown arrow. The default is all bookmarks, but can be
+ * localized to the active collection.
+ * @param filterString
+ * The text to search for.
+ */
+ search: function PSB_search(filterString) {
+ var PO = PlacesOrganizer;
+ // If the user empties the search box manually, reset it and load all
+ // contents of the current scope.
+ // XXX this might be to jumpy, maybe should search for "", so results
+ // are ungrouped, and search box not reset
+ if (filterString == "") {
+ PO.onPlaceSelected(false);
+ return;
+ }
+
+ let currentView = ContentArea.currentView;
+
+ // Search according to the current scope, which was set by
+ // PQB_setScope()
+ switch (PlacesSearchBox.filterCollection) {
+ case "bookmarks":
+ currentView.applyFilter(filterString, this.folders);
+ break;
+ case "history": {
+ let currentOptions = PO.getCurrentOptions();
+ if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+ let options = currentOptions.clone();
+ // Make sure we're getting uri results.
+ options.resultType = currentOptions.RESULTS_AS_URI;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ options.includeHidden = true;
+ currentView.load([query], options);
+ } else {
+ currentView.applyFilter(filterString, null, true);
+ }
+ break;
+ }
+ default:
+ throw "Invalid filterCollection on search";
+ }
+
+ // Update the details panel
+ PlacesOrganizer.updateDetailsPane();
+ },
+
+ /**
+ * Finds across all history or all bookmarks.
+ */
+ findAll: function PSB_findAll() {
+ switch (this.filterCollection) {
+ case "history":
+ PlacesQueryBuilder.setScope("history");
+ break;
+ default:
+ PlacesQueryBuilder.setScope("bookmarks");
+ break;
+ }
+ this.focus();
+ },
+
+ /**
+ * Updates the display with the title of the current collection.
+ * @param aTitle
+ * The title of the current collection.
+ */
+ updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) {
+ let title = "";
+ switch (this.filterCollection) {
+ case "history":
+ title = PlacesUIUtils.getString("searchHistory");
+ break;
+ default:
+ title = PlacesUIUtils.getString("searchBookmarks");
+ }
+ this.searchFilter.placeholder = title;
+ },
+
+ /**
+ * Gets/sets the active collection from the dropdown menu.
+ */
+ get filterCollection() {
+ return this.searchFilter.getAttribute("collection");
+ },
+ set filterCollection(collectionName) {
+ if (collectionName == this.filterCollection)
+ return collectionName;
+
+ this.searchFilter.setAttribute("collection", collectionName);
+ this.updateCollectionTitle();
+
+ return collectionName;
+ },
+
+ /**
+ * Focus the search box
+ */
+ focus: function PSB_focus() {
+ this.searchFilter.focus();
+ },
+
+ /**
+ * Set up the gray text in the search bar as the Places View loads.
+ */
+ init: function PSB_init() {
+ if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll", false)) {
+ this.searchFilter.setAttribute("clickSelectsAll", true);
+ }
+ this.updateCollectionTitle();
+ },
+
+ /**
+ * Gets or sets the text shown in the Places Search Box
+ */
+ get value() {
+ return this.searchFilter.value;
+ },
+ set value(value) {
+ return this.searchFilter.value = value;
+ },
+};
+
+/**
+ * Functions and data for advanced query builder
+ */
+var PlacesQueryBuilder = {
+
+ queries: [],
+ queryOptions: null,
+
+ /**
+ * Sets the search scope. This can be called when no search is active, and
+ * in that case, when the user does begin a search aScope will be used (see
+ * PSB_search()). If there is an active search, it's performed again to
+ * update the content tree.
+ * @param aScope
+ * The search scope: "bookmarks", "collection" or "history".
+ */
+ setScope: function PQB_setScope(aScope) {
+ // Determine filterCollection, folders, and scopeButtonId based on aScope.
+ var filterCollection;
+ var folders = [];
+ switch (aScope) {
+ case "history":
+ filterCollection = "history";
+ break;
+ case "bookmarks":
+ filterCollection = "bookmarks";
+ folders.push(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.toolbarFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.mobileFolderId);
+ break;
+ default:
+ throw "Invalid search scope";
+ }
+
+ // Update the search box. Re-search if there's an active search.
+ PlacesSearchBox.filterCollection = filterCollection;
+ PlacesSearchBox.folders = folders;
+ var searchStr = PlacesSearchBox.searchFilter.value;
+ if (searchStr)
+ PlacesSearchBox.search(searchStr);
+ }
+};
+
+/**
+ * Population and commands for the View Menu.
+ */
+var ViewMenu = {
+ /**
+ * Removes content generated previously from a menupopup.
+ * @param popup
+ * The popup that contains the previously generated content.
+ * @param startID
+ * The id attribute of an element that is the start of the
+ * dynamically generated region - remove elements after this
+ * item only.
+ * Must be contained by popup. Can be null (in which case the
+ * contents of popup are removed).
+ * @param endID
+ * The id attribute of an element that is the end of the
+ * dynamically generated region - remove elements up to this
+ * item only.
+ * Must be contained by popup. Can be null (in which case all
+ * items until the end of the popup will be removed). Ignored
+ * if startID is null.
+ * @returns The element for the caller to insert new items before,
+ * null if the caller should just append to the popup.
+ */
+ _clean: function VM__clean(popup, startID, endID) {
+ if (endID && !startID)
+ throw new Error("meaningless to have valid endID and null startID");
+ if (startID) {
+ var startElement = document.getElementById(startID);
+ if (!startElement)
+ throw new Error("startID does not correspond to an existing element");
+ if (startElement.parentNode != popup)
+ throw new Error("startElement is not in popup");
+ var endElement = null;
+ if (endID) {
+ endElement = document.getElementById(endID);
+ if (!endElement)
+ throw new Error("endID does not correspond to an existing element");
+ if (endElement.parentNode != popup)
+ throw new Error("endElement is not in popup");
+ }
+ while (startElement.nextSibling != endElement)
+ popup.removeChild(startElement.nextSibling);
+ return endElement;
+ }
+ while (popup.hasChildNodes()) {
+ popup.firstChild.remove();
+ }
+ return null;
+ },
+
+ /**
+ * Fills a menupopup with a list of columns
+ * @param event
+ * The popupshowing event that invoked this function.
+ * @param startID
+ * see _clean
+ * @param endID
+ * see _clean
+ * @param type
+ * the type of the menuitem, e.g. "radio" or "checkbox".
+ * Can be null (no-type).
+ * Checkboxes are checked if the column is visible.
+ * @param propertyPrefix
+ * If propertyPrefix is non-null:
+ * propertyPrefix + column ID + ".label" will be used to get the
+ * localized label string.
+ * propertyPrefix + column ID + ".accesskey" will be used to get the
+ * localized accesskey.
+ * If propertyPrefix is null, the column label is used as label and
+ * no accesskey is assigned.
+ */
+ fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
+ var popup = event.target;
+ var pivot = this._clean(popup, startID, endID);
+
+ var content = document.getElementById("placeContent");
+ var columns = content.columns;
+ for (var i = 0; i < columns.count; ++i) {
+ var column = columns.getColumnAt(i).element;
+ var menuitem = document.createElement("menuitem");
+ menuitem.id = "menucol_" + column.id;
+ menuitem.column = column;
+ var label = column.getAttribute("label");
+ if (propertyPrefix) {
+ var menuitemPrefix = propertyPrefix;
+ // for string properties, use "name" as the id, instead of "title"
+ // see bug #386287 for details
+ var columnId = column.getAttribute("anonid");
+ menuitemPrefix += columnId == "title" ? "name" : columnId;
+ label = PlacesUIUtils.getString(menuitemPrefix + ".label");
+ var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
+ menuitem.setAttribute("accesskey", accesskey);
+ }
+ menuitem.setAttribute("label", label);
+ if (type == "radio") {
+ menuitem.setAttribute("type", "radio");
+ menuitem.setAttribute("name", "columns");
+ // This column is the sort key. Its item is checked.
+ if (column.getAttribute("sortDirection") != "") {
+ menuitem.setAttribute("checked", "true");
+ }
+ } else if (type == "checkbox") {
+ menuitem.setAttribute("type", "checkbox");
+ // Cannot uncheck the primary column.
+ if (column.getAttribute("primary") == "true")
+ menuitem.setAttribute("disabled", "true");
+ // Items for visible columns are checked.
+ if (!column.hidden)
+ menuitem.setAttribute("checked", "true");
+ }
+ if (pivot)
+ popup.insertBefore(menuitem, pivot);
+ else
+ popup.appendChild(menuitem);
+ }
+ event.stopPropagation();
+ },
+
+ /**
+ * Set up the content of the view menu.
+ */
+ populateSortMenu: function VM_populateSortMenu(event) {
+ this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1.");
+
+ var sortColumn = this._getSortColumn();
+ var viewSortAscending = document.getElementById("viewSortAscending");
+ var viewSortDescending = document.getElementById("viewSortDescending");
+ // We need to remove an existing checked attribute because the unsorted
+ // menu item is not rebuilt every time we open the menu like the others.
+ var viewUnsorted = document.getElementById("viewUnsorted");
+ if (!sortColumn) {
+ viewSortAscending.removeAttribute("checked");
+ viewSortDescending.removeAttribute("checked");
+ viewUnsorted.setAttribute("checked", "true");
+ } else if (sortColumn.getAttribute("sortDirection") == "ascending") {
+ viewSortAscending.setAttribute("checked", "true");
+ viewSortDescending.removeAttribute("checked");
+ viewUnsorted.removeAttribute("checked");
+ } else if (sortColumn.getAttribute("sortDirection") == "descending") {
+ viewSortDescending.setAttribute("checked", "true");
+ viewSortAscending.removeAttribute("checked");
+ viewUnsorted.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Shows/Hides a tree column.
+ * @param element
+ * The menuitem element for the column
+ */
+ showHideColumn: function VM_showHideColumn(element) {
+ var column = element.column;
+
+ var splitter = column.nextSibling;
+ if (splitter && splitter.localName != "splitter")
+ splitter = null;
+
+ if (element.getAttribute("checked") == "true") {
+ column.setAttribute("hidden", "false");
+ if (splitter)
+ splitter.removeAttribute("hidden");
+ } else {
+ column.setAttribute("hidden", "true");
+ if (splitter)
+ splitter.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Gets the last column that was sorted.
+ * @returns the currently sorted column, null if there is no sorted column.
+ */
+ _getSortColumn: function VM__getSortColumn() {
+ var content = document.getElementById("placeContent");
+ var cols = content.columns;
+ for (var i = 0; i < cols.count; ++i) {
+ var column = cols.getColumnAt(i).element;
+ var sortDirection = column.getAttribute("sortDirection");
+ if (sortDirection == "ascending" || sortDirection == "descending")
+ return column;
+ }
+ return null;
+ },
+
+ /**
+ * Sorts the view by the specified column.
+ * @param aColumn
+ * The colum that is the sort key. Can be null - the
+ * current sort column or the title column will be used.
+ * @param aDirection
+ * The direction to sort - "ascending" or "descending".
+ * Can be null - the last direction or descending will be used.
+ *
+ * If both aColumnID and aDirection are null, the view will be unsorted.
+ */
+ setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
+ var result = document.getElementById("placeContent").result;
+ if (!aColumn && !aDirection) {
+ result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ return;
+ }
+
+ var columnId;
+ if (aColumn) {
+ columnId = aColumn.getAttribute("anonid");
+ if (!aDirection) {
+ let sortColumn = this._getSortColumn();
+ if (sortColumn)
+ aDirection = sortColumn.getAttribute("sortDirection");
+ }
+ } else {
+ let sortColumn = this._getSortColumn();
+ columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
+ }
+
+ // This maps the possible values of columnId (i.e., anonid's of treecols in
+ // placeContent) to the default sortingMode and sortingAnnotation values for
+ // each column.
+ // key: Sort key in the name of one of the
+ // nsINavHistoryQueryOptions.SORT_BY_* constants
+ // dir: Default sort direction to use if none has been specified
+ // anno: The annotation to sort by, if key is "ANNOTATION"
+ var colLookupTable = {
+ title: { key: "TITLE", dir: "ascending" },
+ tags: { key: "TAGS", dir: "ascending" },
+ url: { key: "URI", dir: "ascending" },
+ date: { key: "DATE", dir: "descending" },
+ visitCount: { key: "VISITCOUNT", dir: "descending" },
+ dateAdded: { key: "DATEADDED", dir: "descending" },
+ lastModified: { key: "LASTMODIFIED", dir: "descending" },
+ description: { key: "ANNOTATION",
+ dir: "ascending",
+ anno: PlacesUIUtils.DESCRIPTION_ANNO }
+ };
+
+ // Make sure we have a valid column.
+ if (!colLookupTable.hasOwnProperty(columnId))
+ throw new Error("Invalid column");
+
+ // Use a default sort direction if none has been specified. If aDirection
+ // is invalid, result.sortingMode will be undefined, which has the effect
+ // of unsorting the tree.
+ aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
+
+ var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
+ result.sortingAnnotation = colLookupTable[columnId].anno || "";
+ result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ }
+};
+
+var ContentArea = {
+ _specialViews: new Map(),
+
+ init: function CA_init() {
+ this._deck = document.getElementById("placesViewsDeck");
+ this._toolbar = document.getElementById("placesToolbar");
+ ContentTree.init();
+ this._setupView();
+ },
+
+ /**
+ * Gets the content view to be used for loading the given query.
+ * If a custom view was set by setContentViewForQueryString, that
+ * view would be returned, else the default tree view is returned
+ *
+ * @param aQueryString
+ * a query string
+ * @return the view to be used for loading aQueryString.
+ */
+ getContentViewForQueryString:
+ function CA_getContentViewForQueryString(aQueryString) {
+ try {
+ if (this._specialViews.has(aQueryString)) {
+ let { view, options } = this._specialViews.get(aQueryString);
+ if (typeof view == "function") {
+ view = view();
+ this._specialViews.set(aQueryString, { view, options });
+ }
+ return view;
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ return ContentTree.view;
+ },
+
+ /**
+ * Sets a custom view to be used rather than the default places tree
+ * whenever the given query is selected in the left pane.
+ * @param aQueryString
+ * a query string
+ * @param aView
+ * Either the custom view or a function that will return the view
+ * the first (and only) time it's called.
+ * @param [optional] aOptions
+ * Object defining special options for the view.
+ * @see ContentTree.viewOptions for supported options and default values.
+ */
+ setContentViewForQueryString:
+ function CA_setContentViewForQueryString(aQueryString, aView, aOptions) {
+ if (!aQueryString ||
+ typeof aView != "object" && typeof aView != "function")
+ throw new Error("Invalid arguments");
+
+ this._specialViews.set(aQueryString, { view: aView,
+ options: aOptions || {} });
+ },
+
+ get currentView() {
+ return PlacesUIUtils.getViewForNode(this._deck.selectedPanel);
+ },
+ set currentView(aNewView) {
+ let oldView = this.currentView;
+ if (oldView != aNewView) {
+ this._deck.selectedPanel = aNewView.associatedElement;
+
+ // If the content area inactivated view was focused, move focus
+ // to the new view.
+ if (document.activeElement == oldView.associatedElement)
+ aNewView.associatedElement.focus();
+ }
+ return aNewView;
+ },
+
+ get currentPlace() {
+ return this.currentView.place;
+ },
+ set currentPlace(aQueryString) {
+ let oldView = this.currentView;
+ let newView = this.getContentViewForQueryString(aQueryString);
+ newView.place = aQueryString;
+ if (oldView != newView) {
+ oldView.active = false;
+ this.currentView = newView;
+ this._setupView();
+ newView.active = true;
+ }
+ return aQueryString;
+ },
+
+ /**
+ * Applies view options.
+ */
+ _setupView: function CA__setupView() {
+ let options = this.currentViewOptions;
+
+ // showDetailsPane.
+ let detailsDeck = document.getElementById("detailsDeck");
+ detailsDeck.hidden = !options.showDetailsPane;
+
+ // toolbarSet.
+ for (let elt of this._toolbar.childNodes) {
+ elt.hidden = !options.toolbarSet.includes(elt.id);
+ }
+ },
+
+ /**
+ * Options for the current view.
+ *
+ * @see ContentTree.viewOptions for supported options and default values.
+ */
+ get currentViewOptions() {
+ // Use ContentTree options as default.
+ let viewOptions = ContentTree.viewOptions;
+ if (this._specialViews.has(this.currentPlace)) {
+ let { options } = this._specialViews.get(this.currentPlace);
+ for (let option in options) {
+ viewOptions[option] = options[option];
+ }
+ }
+ return viewOptions;
+ },
+
+ focus() {
+ this._deck.selectedPanel.focus();
+ }
+};
+
+var ContentTree = {
+ init: function CT_init() {
+ this._view = document.getElementById("placeContent");
+ },
+
+ get view() {
+ return this._view;
+ },
+
+ get viewOptions() {
+ return Object.seal({
+ showDetailsPane: true,
+ toolbarSet: "placesMenu, toolbar-spacer, searchFilter"
+ });
+ },
+
+ openSelectedNode: function CT_openSelectedNode(aEvent) {
+ let view = this.view;
+ PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, true);
+ },
+
+ onClick: function CT_onClick(aEvent) {
+ let node = this.view.selectedNode;
+ if (node) {
+ let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
+ // Open associated uri in the browser.
+ this.openSelectedNode(aEvent);
+ } else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
+ // The command execution function will take care of seeing if the
+ // selection is a folder or a different container type, and will
+ // load its contents in tabs.
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
+ }
+ }
+ },
+
+ onKeyPress: function CT_onKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ this.openSelectedNode(aEvent);
+ }
+};
diff --git a/comm/suite/components/places/content/places.xul b/comm/suite/components/places/content/places.xul
new file mode 100644
index 0000000000..d5acde063d
--- /dev/null
+++ b/comm/suite/components/places/content/places.xul
@@ -0,0 +1,337 @@
+<?xml version="1.0"?>
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/content/places/organizer.css"?>
+
+<?xml-stylesheet href="chrome://communicator/skin/"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/bookmarks.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/editBookmarkOverlay.xul"?>
+
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % placesDTD SYSTEM "chrome://communicator/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+<!ENTITY % navDTD SYSTEM "chrome://navigator/locale/navigator.dtd">
+%navDTD;
+]>
+
+<window id="places"
+ title="&places.library.title;"
+ windowtype="Places:Organizer"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="PlacesOrganizer.init();"
+ onunload="PlacesOrganizer.destroy();"
+ width="&places.library.width;" height="&places.library.height;"
+ screenX="10" screenY="10"
+ toggletoolbar="true"
+ persist="width height screenX screenY sizemode">
+
+ <script src="chrome://communicator/content/places/places.js"/>
+ <script src="chrome://communicator/content/places/editBookmarkOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <stringbundleset id="placesStringSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ </stringbundleset>
+
+ <commandset id="placesCommands"/>
+ <commandset id="tasksCommands"/>
+
+ <commandset id="organizerCommandSet">
+ <command id="OrganizerCommand_find:all"
+ oncommand="PlacesSearchBox.findAll();"/>
+ <command id="OrganizerCommand_export"
+ oncommand="PlacesOrganizer.exportBookmarks();"/>
+ <command id="OrganizerCommand_import"
+ oncommand="PlacesOrganizer.importFromFile();"/>
+ <command id="OrganizerCommand_backup"
+ oncommand="PlacesOrganizer.backupBookmarks();"/>
+ <command id="OrganizerCommand_restoreFromFile"
+ oncommand="PlacesOrganizer.onRestoreBookmarksFromFile();"/>
+ <command id="OrganizerCommand_search:save"
+ oncommand="PlacesOrganizer.saveSearch();"/>
+ <command id="OrganizerCommand_search:moreCriteria"
+ oncommand="PlacesQueryBuilder.addRow();"/>
+ <command id="OrganizerCommand:Back"
+ oncommand="PlacesOrganizer.back();"/>
+ <command id="OrganizerCommand:Forward"
+ oncommand="PlacesOrganizer.forward();"/>
+ </commandset>
+
+ <keyset id="placesOrganizerKeyset">
+ <!-- Instantiation Keys -->
+ <key id="placesKey_close" key="&cmd.close.key;" modifiers="accel"
+ oncommand="close();"/>
+
+ <!-- Command Keys -->
+ <key id="placesKey_find:all"
+ command="OrganizerCommand_find:all"
+ key="&cmd.find.key;"
+ modifiers="accel"/>
+
+ <!-- Back/Forward Keys Support -->
+ <key id="placesKey_goBackKb"
+ keycode="VK_LEFT"
+ command="OrganizerCommand:Back"
+ modifiers="accel"/>
+ <key id="placesKey_goForwardKb"
+ keycode="VK_RIGHT"
+ command="OrganizerCommand:Forward"
+ modifiers="accel"/>
+ </keyset>
+
+#include ../../../../../toolkit/content/editMenuKeys.inc.xhtml
+#ifdef XP_MACOSX
+ <keyset id="editMenuKeysExtra">
+ <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
+ </keyset>
+#endif
+
+ <keyset id="tasksKeys">
+ <key id="key_close2" disabled="true"/>
+ </keyset>
+
+ <popupset id="placesPopupset">
+ <menupopup id="placesContext"/>
+ <menupopup id="placesColumnsContext"
+ onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);"
+ oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/>
+ </popupset>
+
+ <toolbox id="placesToolbox">
+ <toolbar id="placesToolbar"
+ class="chromeclass-toolbar"
+ align="center">
+ <menubar id="placesMenu">
+ <menu id="menu_File">
+ <menupopup id="menu_FilePopup">
+ <menuitem id="newbookmark"
+ command="placesCmd_new:bookmark"
+ label="&cmd.new_bookmark.label;"
+ accesskey="&cmd.new_bookmark.accesskey;"/>
+ <menuitem id="newfolder"
+ command="placesCmd_new:folder"
+ label="&cmd.new_folder.label;"
+ accesskey="&cmd.new_folder.accesskey;"/>
+ <menuitem id="newseparator"
+ command="placesCmd_new:separator"
+ label="&cmd.new_separator.label;"
+ accesskey="&cmd.new_separator.accesskey;"/>
+ <menuseparator id="fileNewSeparator"/>
+ <menuitem id="orgClose"
+ key="placesKey_close"
+ label="&file.close.label;"
+ accesskey="&file.close.accesskey;"
+ oncommand="close();"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_Edit">
+ <menupopup id="menu_EditPopup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+
+ <menuseparator id="orgCutSeparator"/>
+
+ <menuitem id="menu_cut"
+ selection="separator|link|folder|mixed"/>
+ <menuitem id="menu_copy"
+ selection="separator|link|folder|mixed"/>
+ <menuitem id="menu_paste"
+ selection="mutable"/>
+ <menuitem id="menu_delete"/>
+
+ <menuseparator id="selectAllSeparator"/>
+
+ <menuitem id="menu_selectAll"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_View">
+ <menupopup id="menu_ViewPopup"
+ onpopupshowing="onViewToolbarsPopupShowing(event)"
+ oncommand="onViewToolbarCommand(event);">
+ <menuseparator id="toolbarmode-sep"/>
+ <menu id="viewColumns"
+ label="&view.columns.label;" accesskey="&view.columns.accesskey;">
+ <menupopup onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);"
+ oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/>
+ </menu>
+
+ <menu id="viewSort" label="&view.sort.label;"
+ accesskey="&view.sort.accesskey;">
+ <menupopup onpopupshowing="ViewMenu.populateSortMenu(event);"
+ oncommand="ViewMenu.setSortColumn(event.target.column, null);">
+ <menuitem id="viewUnsorted" type="radio" name="columns"
+ label="&view.unsorted.label;" accesskey="&view.unsorted.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, null);"/>
+ <menuseparator id="directionSeparator"/>
+ <menuitem id="viewSortAscending" type="radio" name="direction"
+ label="&view.sortAscending.label;" accesskey="&view.sortAscending.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, 'ascending'); event.stopPropagation();"/>
+ <menuitem id="viewSortDescending" type="radio" name="direction"
+ label="&view.sortDescending.label;" accesskey="&view.sortDescending.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, 'descending'); event.stopPropagation();"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+
+ <!-- tasks menu filled from tasksOverlay -->
+ <menu id="tasksMenu">
+ <menupopup id="taskPopup">
+ <menuitem id="backupBookmarks"
+ command="OrganizerCommand_backup"
+ label="&cmd.backup.label;"
+ accesskey="&cmd.backup.accesskey;"/>
+ <menu id="fileRestoreMenu" label="&cmd.restore2.label;"
+ accesskey="&cmd.restore2.accesskey;">
+ <menupopup id="fileRestorePopup" onpopupshowing="PlacesOrganizer.populateRestoreMenu();">
+ <menuitem id="restoreFromFile"
+ command="OrganizerCommand_restoreFromFile"
+ label="&cmd.restoreFromFile.label;"
+ accesskey="&cmd.restoreFromFile.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem id="fileImport"
+ command="OrganizerCommand_import"
+ label="&importBookmarksFromHTML.label;"
+ accesskey="&importBookmarksFromHTML.accesskey;"/>
+ <menuitem id="fileExport"
+ command="OrganizerCommand_export"
+ label="&exportBookmarksToHTML.label;"
+ accesskey="&exportBookmarksToHTML.accesskey;"/>
+ <menuseparator/>
+ </menupopup>
+ </menu>
+
+ <!-- window menu filled from tasksOverlay -->
+ <menu id="windowMenu"/>
+
+ <!-- help menu filled from globalOverlay -->
+ <menu id="menu_Help"/>
+ </menubar>
+
+ <toolbarspring id="toolbar-spacer"/>
+
+ <textbox id="searchFilter"
+ type="search"
+ aria-controls="placeContent"
+ oncommand="PlacesSearchBox.search(this.value);"
+ collection="bookmarks">
+ </textbox>
+ </toolbar>
+ </toolbox>
+
+ <hbox flex="1" id="placesView">
+ <tree id="placesList"
+ class="plain placesTree"
+ type="places"
+ hidecolumnpicker="true"
+ treelines="true"
+ context="placesContext"
+ onselect="PlacesOrganizer.onPlaceSelected(true);"
+ onclick="PlacesOrganizer.onPlacesListClick(event);"
+ onfocus="PlacesOrganizer.updateDetailsPane(event);"
+ seltype="single"
+ persist="width"
+ width="200"
+ minwidth="100"
+ maxwidth="400">
+ <treecols>
+ <treecol anonid="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+ <splitter collapse="none" persist="state"></splitter>
+ <vbox id="contentView" flex="4">
+ <deck id="placesViewsDeck"
+ selectedIndex="0"
+ flex="1">
+ <tree id="placeContent"
+ class="plain placesTree"
+ treelines="true"
+ context="placesContext"
+ flex="1"
+ type="places"
+ selectfirstnode="true"
+ enableColumnDrag="true"
+ onfocus="PlacesOrganizer.updateDetailsPane(event)"
+ onselect="PlacesOrganizer.updateDetailsPane(event)"
+ onkeypress="ContentTree.onKeyPress(event);"
+ onopenflatcontainer="PlacesOrganizer.openFlatContainer(aContainer);">
+ <treecols id="placeContentColumns" context="placesColumnsContext">
+ <treecol label="&col.name.label;" id="placesContentTitle" anonid="title" flex="5" primary="true" ordinal="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.tags.label;" id="placesContentTags" anonid="tags" flex="2"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.url.label;" id="placesContentUrl" anonid="url" flex="5"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.mostrecentvisit.label;" id="placesContentDate" anonid="date" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.visitcount.label;" id="placesContentVisitCount" anonid="visitCount" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.description.label;" id="placesContentDescription" anonid="description" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.dateadded.label;" id="placesContentDateAdded" anonid="dateAdded" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.lastmodified.label;" id="placesContentLastModified" anonid="lastModified" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ </treecols>
+ <treechildren flex="1" onclick="ContentTree.onClick(event);"/>
+ </tree>
+ </deck>
+ <deck id="detailsDeck" style="height: 11em;">
+ <vbox id="itemsCountBox" align="center">
+ <spacer flex="3"/>
+ <label id="itemsCountText"/>
+ <spacer flex="1"/>
+ <description id="selectItemDescription">
+ &detailsPane.selectAnItemText.description;
+ </description>
+ <spacer flex="3"/>
+ </vbox>
+ <vbox id="infoBox" minimal="true">
+ <vbox id="editBookmarkPanelContent" flex="1"/>
+ <hbox id="infoBoxExpanderWrapper" align="center">
+
+ <button type="image" id="infoBoxExpander"
+ class="expander-down"
+ oncommand="PlacesOrganizer.toggleAdditionalInfoFields();"
+ observes="paneElementsBroadcaster"/>
+
+ <label id="infoBoxExpanderLabel"
+ lesslabel="&detailsPane.less.label;"
+ lessaccesskey="&detailsPane.less.accesskey;"
+ morelabel="&detailsPane.more.label;"
+ moreaccesskey="&detailsPane.more.accesskey;"
+ value="&detailsPane.more.label;"
+ accesskey="&detailsPane.more.accesskey;"
+ control="infoBoxExpander"/>
+
+ </hbox>
+ </vbox>
+ </deck>
+ </vbox>
+ </hbox>
+</window>
diff --git a/comm/suite/components/places/content/placesOverlay.xul b/comm/suite/components/places/content/placesOverlay.xul
new file mode 100644
index 0000000000..a71f1e2269
--- /dev/null
+++ b/comm/suite/components/places/content/placesOverlay.xul
@@ -0,0 +1,228 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % placesDTD SYSTEM "chrome://communicator/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+]>
+
+<overlay id="placesOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script>
+ <![CDATA[
+ const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.defineModuleGetter(window,
+ "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
+ ChromeUtils.defineModuleGetter(window,
+ "PlacesUIUtils", "resource:///modules/PlacesUIUtils.jsm");
+ ChromeUtils.defineModuleGetter(window,
+ "PlacesTransactions", "resource://gre/modules/PlacesTransactions.jsm");
+ ChromeUtils.defineModuleGetter(window,
+ "ForgetAboutSite", "resource://gre/modules/ForgetAboutSite.jsm");
+ ChromeUtils.defineModuleGetter(window,
+ "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+ XPCOMUtils.defineLazyScriptGetter(window, "PlacesTreeView",
+ "chrome://communicator/content/places/treeView.js");
+ XPCOMUtils.defineLazyScriptGetter(window,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://communicator/content/places/controller.js");
+ ]]></script>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip" noautohide="true"
+ onpopupshowing="return window.top.BookmarksEventHandler.fillInBHTooltip(document, event)">
+ <vbox id="bhTooltipTextBox" flex="1">
+ <label id="bhtTitleText" class="tooltip-label" />
+ <label id="bhtUrlText" crop="center" class="tooltip-label uri-element" />
+ </vbox>
+ </tooltip>
+
+ <commandset id="placesCommands"
+ commandupdater="true"
+ events="focus,sort,places"
+ oncommandupdate="PlacesUIUtils.updateCommands(window);">
+ <command id="placesCmd_open"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open');"/>
+ <command id="placesCmd_open:window"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:window');"/>
+ <command id="placesCmd_open:privatewindow"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:privatewindow');"/>
+ <command id="placesCmd_open:tab"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:tab');"/>
+
+ <command id="placesCmd_new:bookmark"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:bookmark');"/>
+ <command id="placesCmd_new:folder"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:folder');"/>
+ <command id="placesCmd_new:separator"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:separator');"/>
+ <command id="placesCmd_show:info"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_show:info');"/>
+ <command id="placesCmd_rename"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_show:info');"
+ observes="placesCmd_show:info"/>
+ <command id="placesCmd_reload"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_reload');"/>
+ <command id="placesCmd_sortBy:name"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_sortBy:name');"/>
+ <command id="placesCmd_deleteDataHost"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_deleteDataHost');"/>
+ <command id="placesCmd_createBookmark"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_createBookmark');"/>
+
+ <!-- Special versions of cut/copy/paste/delete which check for an open context menu. -->
+ <command id="placesCmd_cut"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_cut');"/>
+ <command id="placesCmd_copy"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_copy');"/>
+ <command id="placesCmd_paste"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_paste');"/>
+ <command id="placesCmd_delete"
+ oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_delete');"/>
+ </commandset>
+
+ <menupopup id="placesContext"
+ onpopupshowing="this._view = PlacesUIUtils.getViewForNode(document.popupNode);
+ if (!PlacesUIUtils.openInTabClosesMenu) {
+ document.getElementById ('placesContext_open:newtab')
+ .setAttribute('closemenu', 'single');
+ }
+ return this._view.buildContextMenu(this);"
+ onpopuphiding="this._view.destroyContextMenu();">
+ <menuitem id="placesContext_open"
+ command="placesCmd_open"
+ label="&cmd.open.label;"
+ accesskey="&cmd.open.accesskey;"
+ default="true"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_open:newtab"
+ command="placesCmd_open:tab"
+ label="&cmd.open_tab.label;"
+ accesskey="&cmd.open_tab.accesskey;"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_openContainer:tabs"
+ oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode);
+ view.controller.openSelectionInTabs(event);"
+ onclick="checkForMiddleClick(this, event);"
+ label="&cmd.open_all_in_tabs.label;"
+ accesskey="&cmd.open_all_in_tabs.accesskey;"
+ selectiontype="single|none"
+ selection="folder|host|query"/>
+ <menuitem id="placesContext_openLinks:tabs"
+ oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode);
+ view.controller.openSelectionInTabs(event);"
+ onclick="checkForMiddleClick(this, event);"
+ label="&cmd.open_all_in_tabs.label;"
+ accesskey="&cmd.open_all_in_tabs.accesskey;"
+ selectiontype="multiple"
+ selection="link"/>
+ <menuitem id="placesContext_open:newwindow"
+ command="placesCmd_open:window"
+ label="&cmd.open_window.label;"
+ accesskey="&cmd.open_window.accesskey;"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_open:newprivatewindow"
+ command="placesCmd_open:privatewindow"
+ label="&cmd.open_private_window.label;"
+ accesskey="&cmd.open_private_window.accesskey;"
+ selectiontype="single"
+ selection="link"
+ hideifprivatebrowsing="true"/>
+ <menuseparator id="placesContext_openSeparator"/>
+ <menuitem id="placesContext_new:bookmark"
+ command="placesCmd_new:bookmark"
+ label="&cmd.new_bookmark.label;"
+ accesskey="&cmd.new_bookmark.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuitem id="placesContext_new:folder"
+ command="placesCmd_new:folder"
+ label="&cmd.new_folder.label;"
+ accesskey="&cmd.context_new_folder.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuitem id="placesContext_new:separator"
+ command="placesCmd_new:separator"
+ label="&cmd.new_separator.label;"
+ accesskey="&cmd.new_separator.accesskey;"
+ closemenu="single"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuseparator id="placesContext_newSeparator"/>
+ <menuitem id="placesContext_createBookmark"
+ command="placesCmd_createBookmark"
+ selection="link"
+ forcehideselection="bookmark|tagChild"/>
+ <menuitem id="placesContext_cut"
+ command="placesCmd_cut"
+ label="&cutCmd.label;"
+ accesskey="&cutCmd.accesskey;"
+ closemenu="single"
+ selection="bookmark|folder|separator|query"
+ forcehideselection="tagChild|livemarkChild"/>
+ <menuitem id="placesContext_copy"
+ command="placesCmd_copy"
+ label="&copyCmd.label;"
+ closemenu="single"
+ accesskey="&copyCmd.accesskey;"
+ selection="any"/>
+ <menuitem id="placesContext_paste"
+ command="placesCmd_paste"
+ label="&pasteCmd.label;"
+ closemenu="single"
+ accesskey="&pasteCmd.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuseparator id="placesContext_editSeparator"/>
+ <menuitem id="placesContext_delete"
+ command="placesCmd_delete"
+ label="&deleteCmd.label;"
+ accesskey="&deleteCmd.accesskey;"
+ closemenu="single"
+ selection="bookmark|tagChild|folder|query|dynamiccontainer|separator|host"/>
+ <menuitem id="placesContext_delete_history"
+ command="placesCmd_delete"
+ closemenu="single"
+ selection="link"
+ forcehideselection="bookmark"/>
+ <menuitem id="placesContext_deleteHost"
+ command="placesCmd_deleteDataHost"
+ label="&cmd.deleteDomainData.label;"
+ accesskey="&cmd.deleteDomainData.accesskey;"
+ closemenu="single"
+ selection="link|host"
+ selectiontype="single"
+ forcehideselection="bookmark"/>
+ <menuseparator id="placesContext_deleteSeparator"/>
+ <menuitem id="placesContext_sortBy:name"
+ command="placesCmd_sortBy:name"
+ label="&cmd.sortby_name.label;"
+ accesskey="&cmd.context_sortby_name.accesskey;"
+ closemenu="single"
+ selection="folder"/>
+ <menuitem id="placesContext_reload"
+ command="placesCmd_reload"
+ label="&cmd.reloadLivebookmark.label;"
+ accesskey="&cmd.reloadLivebookmark.accesskey;"
+ closemenu="single"
+ selection="livemark/feedURI"/>
+ <menuseparator id="placesContext_sortSeparator"/>
+ <menuitem id="placesContext_show:info"
+ command="placesCmd_show:info"
+ label="&cmd.properties.label;"
+ accesskey="&cmd.properties.accesskey;"
+ selection="bookmark|folder|query"
+ forcehideselection="livemarkChild"/>
+ </menupopup>
+
+</overlay>
diff --git a/comm/suite/components/places/content/sidebarUtils.js b/comm/suite/components/places/content/sidebarUtils.js
new file mode 100644
index 0000000000..b11f891187
--- /dev/null
+++ b/comm/suite/components/places/content/sidebarUtils.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+let uidensity = window.top.document.documentElement.getAttribute("uidensity");
+if (uidensity) {
+ document.documentElement.setAttribute("uidensity", uidensity);
+}
+
+var SidebarUtils = {
+ handleTreeClick: function SU_handleTreeClick(aTree, aEvent, aGutterSelect) {
+ // right-clicks are not handled here
+ if (aEvent.button == 2)
+ return;
+
+ var tbo = aTree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+
+ if (cell.row == -1 || cell.childElt == "twisty")
+ return;
+
+ var mouseInGutter = false;
+ if (aGutterSelect) {
+ var rect = tbo.getCoordsForCellItem(cell.row, cell.col, "image");
+ // getCoordsForCellItem returns the x coordinate in logical coordinates
+ // (i.e., starting from the left and right sides in LTR and RTL modes,
+ // respectively.) Therefore, we make sure to exclude the blank area
+ // before the tree item icon (that is, to the left or right of it in
+ // LTR and RTL modes, respectively) from the click target area.
+ var isRTL = window.getComputedStyle(aTree).direction == "rtl";
+ if (isRTL)
+ mouseInGutter = aEvent.clientX > rect.x;
+ else
+ mouseInGutter = aEvent.clientX < rect.x;
+ }
+
+ var metaKey = AppConstants.platform === "macosx" ? aEvent.metaKey
+ : aEvent.ctrlKey;
+ var modifKey = metaKey || aEvent.shiftKey;
+ var isContainer = tbo.view.isContainer(cell.row);
+ var openInTabs = isContainer &&
+ (aEvent.button == 1 ||
+ (aEvent.button == 0 && modifKey)) &&
+ PlacesUtils.hasChildURIs(aTree.view.nodeForTreeIndex(cell.row));
+
+ if (aEvent.button == 0 && isContainer && !openInTabs) {
+ tbo.view.toggleOpenState(cell.row);
+ } else if (!mouseInGutter && openInTabs &&
+ aEvent.originalTarget.localName == "treechildren") {
+ tbo.view.selection.select(cell.row);
+ PlacesUIUtils.openContainerNodeInTabs(aTree.selectedNode, aEvent, aTree);
+ } else if (!mouseInGutter && !isContainer &&
+ aEvent.originalTarget.localName == "treechildren") {
+ // Clear all other selection since we're loading a link now. We must
+ // do this *before* attempting to load the link since openURL uses
+ // selection as an indication of which link to load.
+ tbo.view.selection.select(cell.row);
+ PlacesUIUtils.openNodeWithEvent(aTree.selectedNode, aEvent);
+ }
+ },
+
+ handleTreeKeyPress: function SU_handleTreeKeyPress(aEvent) {
+ let node = aEvent.target.selectedNode;
+ if (node) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ PlacesUIUtils.openNodeWithEvent(node, aEvent);
+ }
+ },
+
+ /**
+ * The following function displays the URL of a node that is being
+ * hovered over.
+ */
+ handleTreeMouseMove: function SU_handleTreeMouseMove(aEvent) {
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ var tree = aEvent.target.parentNode;
+ var tbo = tree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+
+ // cell.row is -1 when the mouse is hovering an empty area within the tree.
+ // To avoid showing a URL from a previously hovered node for a currently
+ // hovered non-url node, we must clear the moused-over URL in these cases.
+ if (cell.row != -1) {
+ var node = tree.view.nodeForTreeIndex(cell.row);
+ if (PlacesUtils.nodeIsURI(node))
+ this.setMouseoverURL(node.uri);
+ else
+ this.setMouseoverURL("");
+ } else
+ this.setMouseoverURL("");
+ },
+
+ setMouseoverURL: function SU_setMouseoverURL(aURL) {
+ // When the browser window is closed with an open sidebar, the sidebar
+ // unload event happens after the browser's one. In this case
+ // top.XULBrowserWindow has been nullified already.
+ if (top.XULBrowserWindow) {
+ top.XULBrowserWindow.setOverLink(aURL, null);
+ }
+ }
+};
diff --git a/comm/suite/components/places/content/tree.xml b/comm/suite/components/places/content/tree.xml
new file mode 100644
index 0000000000..f033f193fa
--- /dev/null
+++ b/comm/suite/components/places/content/tree.xml
@@ -0,0 +1,812 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="placesTreeBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="places-tree" extends="chrome://global/content/bindings/tree.xml#tree">
+ <implementation>
+ <constructor><![CDATA[
+ // Force an initial build.
+ if (this.place)
+ this.place = this.place;
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ // Break the treeviewer->result->treeviewer cycle.
+ // Note: unsetting the result's viewer also unsets
+ // the viewer's reference to our treeBoxObject.
+ var result = this.result;
+ if (result) {
+ result.root.containerOpen = false;
+ }
+
+ // Unregister the controllber before unlinking the view, otherwise it
+ // may still try to update commands on a view with a null result.
+ if (this._controller) {
+ this._controller.terminate();
+ this.controllers.removeController(this._controller);
+ }
+
+ if (this.view) {
+ this.view.uninit();
+ }
+ this.view = null;
+ ]]></destructor>
+
+ <property name="controller"
+ readonly="true"
+ onget="return this._controller"/>
+
+ <!-- overriding -->
+ <property name="view">
+ <getter><![CDATA[
+ try {
+ return this.treeBoxObject.view.wrappedJSObject || null;
+ } catch (e) {
+ return null;
+ }
+ ]]></getter>
+ <setter><![CDATA[
+ return this.treeBoxObject.view = val;
+ ]]></setter>
+ </property>
+
+ <property name="associatedElement"
+ readonly="true"
+ onget="return this"/>
+
+ <method name="applyFilter">
+ <parameter name="filterString"/>
+ <parameter name="folderRestrict"/>
+ <parameter name="includeHidden"/>
+ <body><![CDATA[
+ // preserve grouping
+ var queryNode = PlacesUtils.asQuery(this.result.root);
+ var options = queryNode.queryOptions.clone();
+
+ // Make sure we're getting uri results.
+ // We do not yet support searching into grouped queries or into
+ // tag containers, so we must fall to the default case.
+ if (PlacesUtils.nodeIsHistoryContainer(queryNode) ||
+ options.resultType == options.RESULTS_AS_TAG_QUERY ||
+ options.resultType == options.RESULTS_AS_TAG_CONTENTS ||
+ options.resultType == options.RESULTS_AS_ROOTS_QUERY)
+ options.resultType = options.RESULTS_AS_URI;
+
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+
+ if (folderRestrict) {
+ query.setFolders(folderRestrict, folderRestrict.length);
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ }
+
+ options.includeHidden = !!includeHidden;
+
+ this.load([query], options);
+ ]]></body>
+ </method>
+
+ <method name="load">
+ <parameter name="queries"/>
+ <parameter name="options"/>
+ <body><![CDATA[
+ let result = PlacesUtils.history
+ .executeQueries(queries, queries.length,
+ options);
+ let callback;
+ if (this.flatList) {
+ let onOpenFlatContainer = this.onOpenFlatContainer;
+ if (onOpenFlatContainer)
+ callback = new Function("aContainer", onOpenFlatContainer);
+ }
+
+ if (!this._controller) {
+ this._controller = new PlacesController(this);
+ this.controllers.appendController(this._controller);
+ }
+
+ let treeView = new PlacesTreeView(this.flatList, callback, this._controller);
+
+ // Observer removal is done within the view itself. When the tree
+ // goes away, treeboxobject calls view.setTree(null), which then
+ // calls removeObserver.
+ result.addObserver(treeView);
+ this.view = treeView;
+
+ if (this.getAttribute("selectfirstnode") == "true" && treeView.rowCount > 0) {
+ treeView.selection.select(0);
+ }
+
+ this._cachedInsertionPoint = undefined;
+ ]]></body>
+ </method>
+
+ <property name="flatList">
+ <getter><![CDATA[
+ return this.getAttribute("flatList") == "true";
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.flatList != val) {
+ this.setAttribute("flatList", val);
+ // reload with the last place set
+ if (this.place)
+ this.place = this.place;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="onOpenFlatContainer">
+ <getter><![CDATA[
+ return this.getAttribute("onopenflatcontainer");
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.onOpenFlatContainer != val) {
+ this.setAttribute("onopenflatcontainer", val);
+ // reload with the last place set
+ if (this.place)
+ this.place = this.place;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <!--
+ Causes a particular node represented by the specified placeURI to be
+ selected in the tree. All containers above the node in the hierarchy
+ will be opened, so that the node is visible.
+ -->
+ <method name="selectPlaceURI">
+ <parameter name="placeURI"/>
+ <body><![CDATA[
+ // Do nothing if a node matching the given uri is already selected
+ if (this.hasSelection && this.selectedNode.uri == placeURI)
+ return;
+
+ function findNode(container, nodesURIChecked) {
+ var containerURI = container.uri;
+ if (containerURI == placeURI)
+ return container;
+ if (nodesURIChecked.includes(containerURI))
+ return null;
+
+ // never check the contents of the same query
+ nodesURIChecked.push(containerURI);
+
+ var wasOpen = container.containerOpen;
+ if (!wasOpen)
+ container.containerOpen = true;
+ for (var i = 0; i < container.childCount; ++i) {
+ var child = container.getChild(i);
+ var childURI = child.uri;
+ if (childURI == placeURI)
+ return child;
+ else if (PlacesUtils.nodeIsContainer(child)) {
+ var nested = findNode(PlacesUtils.asContainer(child), nodesURIChecked);
+ if (nested)
+ return nested;
+ }
+ }
+
+ if (!wasOpen)
+ container.containerOpen = false;
+
+ return null;
+ }
+
+ var container = this.result.root;
+ console.assert(container, "No result, cannot select place URI!");
+ if (!container)
+ return;
+
+ var child = findNode(container, []);
+ if (child)
+ this.selectNode(child);
+ else {
+ // If the specified child could not be located, clear the selection
+ var selection = this.view.selection;
+ selection.clearSelection();
+ }
+ ]]></body>
+ </method>
+
+ <!--
+ Causes a particular node to be selected in the tree, resulting in all
+ containers above the node in the hierarchy to be opened, so that the
+ node is visible.
+ -->
+ <method name="selectNode">
+ <parameter name="node"/>
+ <body><![CDATA[
+ var view = this.view;
+
+ var parent = node.parent;
+ if (parent && !parent.containerOpen) {
+ // Build a list of all of the nodes that are the parent of this one
+ // in the result.
+ var parents = [];
+ var root = this.result.root;
+ while (parent && parent != root) {
+ parents.push(parent);
+ parent = parent.parent;
+ }
+
+ // Walk the list backwards (opening from the root of the hierarchy)
+ // opening each folder as we go.
+ for (var i = parents.length - 1; i >= 0; --i) {
+ let index = view.treeIndexForNode(parents[i]);
+ if (index != -1 &&
+ view.isContainer(index) && !view.isContainerOpen(index))
+ view.toggleOpenState(index);
+ }
+ // Select the specified node...
+ }
+
+ let index = view.treeIndexForNode(node);
+ if (index == -1)
+ return;
+
+ view.selection.select(index);
+ // ... and ensure it's visible, not scrolled off somewhere.
+ this.treeBoxObject.ensureRowIsVisible(index);
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <property name="result">
+ <getter><![CDATA[
+ try {
+ return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result;
+ } catch (e) {
+ return null;
+ }
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="place">
+ <getter><![CDATA[
+ return this.getAttribute("place");
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("place", val);
+
+ var queriesRef = { };
+ var queryCountRef = { };
+ var optionsRef = { };
+ PlacesUtils.history.queryStringToQueries(val, queriesRef, queryCountRef, optionsRef);
+ if (queryCountRef.value == 0)
+ queriesRef.value = [PlacesUtils.history.getNewQuery()];
+ if (!optionsRef.value)
+ optionsRef.value = PlacesUtils.history.getNewQueryOptions();
+
+ this.load(queriesRef.value, optionsRef.value);
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="hasSelection">
+ <getter><![CDATA[
+ return this.view && this.view.selection.count >= 1;
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="selectedNodes">
+ <getter><![CDATA[
+ let nodes = [];
+ if (!this.hasSelection)
+ return nodes;
+
+ let selection = this.view.selection;
+ let rc = selection.getRangeCount();
+ let resultview = this.view;
+ for (let i = 0; i < rc; ++i) {
+ let min = { }, max = { };
+ selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ nodes.push(resultview.nodeForTreeIndex(j));
+ }
+ }
+ return nodes;
+ ]]></getter>
+ </property>
+
+ <method name="toggleCutNode">
+ <parameter name="aNode"/>
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.view.toggleCutNode(aNode, aValue);
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <property name="removableSelectionRanges">
+ <getter><![CDATA[
+ // This property exists in addition to selectedNodes because it
+ // encodes selection ranges (which only occur in list views) into
+ // the return value. For each removed range, the index at which items
+ // will be re-inserted upon the remove transaction being performed is
+ // the first index of the range, so that the view updates correctly.
+ //
+ // For example, if we remove rows 2,3,4 and 7,8 from a list, when we
+ // undo that operation, if we insert what was at row 3 at row 3 again,
+ // it will show up _after_ the item that was at row 5. So we need to
+ // insert all items at row 2, and the tree view will update correctly.
+ //
+ // Also, this function collapses the selection to remove redundant
+ // data, e.g. when deleting this selection:
+ //
+ // http://www.foo.com/
+ // (-) Some Folder
+ // http://www.bar.com/
+ //
+ // ... returning http://www.bar.com/ as part of the selection is
+ // redundant because it is implied by removing "Some Folder". We
+ // filter out all such redundancies since some partial amount of
+ // the folder's children may be selected.
+ //
+ let nodes = [];
+ if (!this.hasSelection)
+ return nodes;
+
+ var selection = this.view.selection;
+ var rc = selection.getRangeCount();
+ var resultview = this.view;
+ // This list is kept independently of the range selected (i.e. OUTSIDE
+ // the for loop) since the row index of a container is unique for the
+ // entire view, and we could have some really wacky selection and we
+ // don't want to blow up.
+ var containers = { };
+ for (var i = 0; i < rc; ++i) {
+ var range = [];
+ var min = { }, max = { };
+ selection.getRangeAt(i, min, max);
+
+ for (var j = min.value; j <= max.value; ++j) {
+ if (this.view.isContainer(j))
+ containers[j] = true;
+ if (!(this.view.getParentIndex(j) in containers))
+ range.push(resultview.nodeForTreeIndex(j));
+ }
+ nodes.push(range);
+ }
+ return nodes;
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="draggableSelection"
+ onget="return this.selectedNodes"/>
+
+ <!-- nsIPlacesView -->
+ <property name="selectedNode">
+ <getter><![CDATA[
+ var view = this.view;
+ if (!view || view.selection.count != 1)
+ return null;
+
+ var selection = view.selection;
+ var min = { }, max = { };
+ selection.getRangeAt(0, min, max);
+
+ return this.view.nodeForTreeIndex(min.value);
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="insertionPoint">
+ <getter><![CDATA[
+ // invalidated on selection and focus changes
+ if (this._cachedInsertionPoint !== undefined)
+ return this._cachedInsertionPoint;
+
+ // there is no insertion point for history queries
+ // so bail out now and save a lot of work when updating commands
+ var resultNode = this.result.root;
+ if (PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
+ return this._cachedInsertionPoint = null;
+
+ var orientation = Ci.nsITreeView.DROP_BEFORE;
+ // If there is no selection, insert at the end of the container.
+ if (!this.hasSelection) {
+ var index = this.view.rowCount - 1;
+ this._cachedInsertionPoint =
+ this._getInsertionPoint(index, orientation);
+ return this._cachedInsertionPoint;
+ }
+
+ // This is a two-part process. The first part is determining the drop
+ // orientation.
+ // * The default orientation is to drop _before_ the selected item.
+ // * If the selected item is a container, the default orientation
+ // is to drop _into_ that container.
+ //
+ // Warning: It may be tempting to use tree indexes in this code, but
+ // you must not, since the tree is nested and as your tree
+ // index may change when folders before you are opened and
+ // closed. You must convert your tree index to a node, and
+ // then use getChildIndex to find your absolute index in
+ // the parent container instead.
+ //
+ var resultView = this.view;
+ var selection = resultView.selection;
+ var rc = selection.getRangeCount();
+ var min = { }, max = { };
+ selection.getRangeAt(rc - 1, min, max);
+
+ // If the sole selection is a container, and we are not in
+ // a flatlist, insert into it.
+ // Note that this only applies to _single_ selections,
+ // if the last element within a multi-selection is a
+ // container, insert _adjacent_ to the selection.
+ //
+ // If the sole selection is the bookmarks toolbar folder, we insert
+ // into it even if it is not opened
+ if (selection.count == 1 && resultView.isContainer(max.value) &&
+ !this.flatList)
+ orientation = Ci.nsITreeView.DROP_ON;
+
+ this._cachedInsertionPoint =
+ this._getInsertionPoint(max.value, orientation);
+ return this._cachedInsertionPoint;
+ ]]></getter>
+ </property>
+
+ <method name="_getInsertionPoint">
+ <parameter name="index"/>
+ <parameter name="orientation"/>
+ <body><![CDATA[
+ var result = this.result;
+ var resultview = this.view;
+ var container = result.root;
+ var dropNearNode = null;
+ console.assert(container, "null container");
+ // When there's no selection, assume the container is the container
+ // the view is populated from (i.e. the result's itemId).
+ if (index != -1) {
+ var lastSelected = resultview.nodeForTreeIndex(index);
+ if (resultview.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
+ // If the last selected item is an open container, append _into_
+ // it, rather than insert adjacent to it.
+ container = lastSelected;
+ index = -1;
+ } else if (lastSelected.containerOpen &&
+ orientation == Ci.nsITreeView.DROP_AFTER &&
+ lastSelected.hasChildren) {
+ // If the last selected item is an open container and the user is
+ // trying to drag into it as a first item, really insert into it.
+ container = lastSelected;
+ orientation = Ci.nsITreeView.DROP_ON;
+ index = 0;
+ } else {
+ // Use the last-selected node's container.
+ container = lastSelected.parent;
+
+ // See comment in the treeView.js's copy of this method
+ if (!container || !container.containerOpen)
+ return null;
+
+ // Avoid the potentially expensive call to getChildIndex
+ // if we know this container doesn't allow insertion
+ if (this.controller.disallowInsertion(container))
+ return null;
+
+ var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
+ if (queryOptions.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // If we are within a sorted view, insert at the end
+ index = -1;
+ } else if (queryOptions.excludeItems ||
+ queryOptions.excludeQueries ||
+ queryOptions.excludeReadOnlyFolders) {
+ // Some item may be invisible, insert near last selected one.
+ // We don't replace index here to avoid requests to the db,
+ // instead it will be calculated later by the controller.
+ index = -1;
+ dropNearNode = lastSelected;
+ } else {
+ var lsi = container.getChildIndex(lastSelected);
+ index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+ }
+ }
+ }
+
+ if (this.controller.disallowInsertion(container))
+ return null;
+
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ let tagName = null;
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ if (!tagName)
+ return null;
+ }
+
+ return new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(container),
+ parentGuid: PlacesUtils.getConcreteItemGuid(container),
+ index, orientation, tagName, dropNearNode
+ });
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <method name="selectAll">
+ <body><![CDATA[
+ this.view.selection.selectAll();
+ ]]></body>
+ </method>
+
+ <!-- This method will select the first node in the tree that matches
+ each given item guid. It will open any folder nodes that it needs
+ to in order to show the selected items.
+ Note: An array of ids or guids (or a mixture) may be passed as aIDs.
+ Passing IDs should be considered deprecated.
+ -->
+ <method name="selectItems">
+ <parameter name="aIDs"/>
+ <parameter name="aOpenContainers"/>
+ <body><![CDATA[
+ // Never open containers in flat lists.
+ if (this.flatList)
+ aOpenContainers = false;
+ // By default, we do search and select within containers which were
+ // closed (note that containers in which nodes were not found are
+ // closed).
+ if (aOpenContainers === undefined)
+ aOpenContainers = true;
+
+ var ids = aIDs; // don't manipulate the caller's array
+
+ // Array of nodes found by findNodes which are to be selected
+ var nodes = [];
+
+ // Array of nodes found by findNodes which should be opened
+ var nodesToOpen = [];
+
+ // A set of GUIDs of container-nodes that were previously searched,
+ // and thus shouldn't be searched again. This is empty at the initial
+ // start of the recursion and gets filled in as the recursion
+ // progresses.
+ var checkedGuidsSet = new Set();
+
+ /**
+ * Recursively search through a node's children for items
+ * with the given IDs. When a matching item is found, remove its ID
+ * from the IDs array, and add the found node to the nodes dictionary.
+ *
+ * NOTE: This method will leave open any node that had matching items
+ * in its subtree.
+ */
+ function findNodes(node) {
+ var foundOne = false;
+ // See if node matches an ID we wanted; add to results.
+ // For simple folder queries, check both itemId and the concrete
+ // item id.
+ var index = ids.indexOf(node.itemId);
+ if (index == -1 &&
+ node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
+ index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId);
+
+ if (index == -1) {
+ index = ids.indexOf(node.bookmarkGuid);
+ if (index == -1) {
+ let concreteGuid = PlacesUtils.getConcreteItemGuid(node);
+ if (concreteGuid != node.bookmarkGuid) {
+ index = ids.indexOf(concreteGuid);
+ }
+ }
+ }
+
+ if (index != -1) {
+ nodes.push(node);
+ foundOne = true;
+ ids.splice(index, 1);
+ }
+
+ var concreteGuid = PlacesUtils.getConcreteItemGuid(node);
+ if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
+ checkedGuidsSet.has(concreteGuid))
+ return foundOne;
+
+ // Only follow a query if it has been been explicitly opened by the
+ // caller. We support the "AllBookmarks" case to allow callers to
+ // specify just the top-level bookmark folders.
+ let shouldOpen = aOpenContainers && (PlacesUtils.nodeIsFolder(node) ||
+ (PlacesUtils.nodeIsQuery(node) && node.itemId == PlacesUIUtils.leftPaneQueries.AllBookmarks));
+
+ PlacesUtils.asContainer(node);
+ if (!node.containerOpen && !shouldOpen)
+ return foundOne;
+
+ checkedGuidsSet.add(concreteGuid);
+
+ // Remember the beginning state so that we can re-close
+ // this node if we don't find any additional results here.
+ var previousOpenness = node.containerOpen;
+ node.containerOpen = true;
+ for (var child = 0; child < node.childCount && ids.length > 0; child++) {
+ var childNode = node.getChild(child);
+ var found = findNodes(childNode);
+ if (!foundOne)
+ foundOne = found;
+ }
+
+ // If we didn't find any additional matches in this node's
+ // subtree, revert the node to its previous openness.
+ if (foundOne)
+ nodesToOpen.unshift(node);
+ node.containerOpen = previousOpenness;
+ return foundOne;
+ }
+
+ // Disable notifications while looking for nodes.
+ let result = this.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ findNodes(this.result.root);
+ } finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+
+ // For all the nodes we've found, highlight the corresponding
+ // index in the tree.
+ var resultview = this.view;
+ var selection = this.view.selection;
+ selection.selectEventsSuppressed = true;
+ selection.clearSelection();
+ // Open nodes containing found items
+ for (let i = 0; i < nodesToOpen.length; i++) {
+ nodesToOpen[i].containerOpen = true;
+ }
+ for (let i = 0; i < nodes.length; i++) {
+ var index = resultview.treeIndexForNode(nodes[i]);
+ if (index == -1)
+ continue;
+ selection.rangedSelect(index, index, true);
+ }
+ selection.selectEventsSuppressed = false;
+ ]]></body>
+ </method>
+
+ <field name="_contextMenuShown">false</field>
+
+ <method name="buildContextMenu">
+ <parameter name="aPopup"/>
+ <body><![CDATA[
+ this._contextMenuShown = true;
+ return this.controller.buildContextMenu(aPopup);
+ ]]></body>
+ </method>
+
+ <method name="destroyContextMenu">
+ <parameter name="aPopup"/>
+ this._contextMenuShown = false;
+ <body/>
+ </method>
+
+ <property name="ownerWindow"
+ readonly="true"
+ onget="return window;"/>
+
+ <field name="_active">true</field>
+ <property name="active"
+ onget="return this._active"
+ onset="return this._active = val"/>
+
+ </implementation>
+ <handlers>
+ <handler event="focus"><![CDATA[
+ this._cachedInsertionPoint = undefined;
+
+ // See select handler. We need the sidebar's places commandset to be
+ // updated as well
+ document.commandDispatcher.updateCommands("focus");
+ ]]></handler>
+ <handler event="select"><![CDATA[
+ this._cachedInsertionPoint = undefined;
+
+ // This additional complexity is here for the sidebars
+ var win = window;
+ while (true) {
+ win.document.commandDispatcher.updateCommands("focus");
+ if (win == window.top)
+ break;
+
+ win = win.parent;
+ }
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ if (event.target.localName != "treechildren")
+ return;
+
+ let nodes = this.selectedNodes;
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+
+ // Disallow dragging the root node of a tree.
+ if (!node.parent) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ // If this node is child of a readonly container (e.g. a livemark)
+ // or cannot be moved, we must force a copy.
+ if (!this.controller.canMoveNode(node)) {
+ event.dataTransfer.effectAllowed = "copyLink";
+ break;
+ }
+ }
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ if (event.target.localName != "treechildren")
+ return;
+
+ let cell = this.treeBoxObject.getCellAt(event.clientX, event.clientY);
+ let node = cell.row != -1 ?
+ this.view.nodeForTreeIndex(cell.row) :
+ this.result.root;
+ // cache the dropTarget for the view
+ PlacesControllerDragHelper.currentDropTarget = node;
+
+ // We have to calculate the orientation since view.canDrop will use
+ // it and we want to be consistent with the dropfeedback.
+ let tbo = this.treeBoxObject;
+ let rowHeight = tbo.rowHeight;
+ let eventY = event.clientY - tbo.treeBody.boxObject.y -
+ rowHeight * (cell.row - tbo.getFirstVisibleRow());
+
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+
+ if (cell.row == -1) {
+ // If the row is not valid we try to insert inside the resultNode.
+ orientation = Ci.nsITreeView.DROP_ON;
+ } else if (PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.75) {
+ // If we are below the 75% of a container the treeview we try
+ // to drop after the node.
+ orientation = Ci.nsITreeView.DROP_AFTER;
+ } else if (PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.25) {
+ // If we are below the 25% of a container the treeview we try
+ // to drop inside the node.
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+
+ if (!this.view.canDrop(cell.row, orientation, event.dataTransfer))
+ return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = null;
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/places/content/treeView.js b/comm/suite/components/places/content/treeView.js
new file mode 100644
index 0000000000..209af6549a
--- /dev/null
+++ b/comm/suite/components/places/content/treeView.js
@@ -0,0 +1,1823 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PTV_interfaces = [Ci.nsITreeView,
+ Ci.nsINavHistoryResultObserver,
+ Ci.nsISupportsWeakReference];
+
+/**
+ * This returns the key for any node/details object.
+ *
+ * @param nodeOrDetails
+ * A node, or an object containing the following properties:
+ * - uri
+ * - time
+ * - itemId
+ * In case any of these is missing, an empty string will be returned. This is
+ * to facilitate easy delete statements which occur due to assignment to items in `this._rows`,
+ * since the item we are deleting may be undefined in the array.
+ *
+ * @return key or empty string.
+ */
+function makeNodeDetailsKey(nodeOrDetails) {
+ if (nodeOrDetails &&
+ typeof nodeOrDetails === "object" &&
+ "uri" in nodeOrDetails &&
+ "time" in nodeOrDetails &&
+ "itemId" in nodeOrDetails) {
+ return `${nodeOrDetails.uri}*${nodeOrDetails.time}*${nodeOrDetails.itemId}`;
+ }
+ return "";
+}
+
+function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) {
+ this._tree = null;
+ this._result = null;
+ this._selection = null;
+ this._rootNode = null;
+ this._rows = [];
+ this._flatList = aFlatList;
+ this._nodeDetails = new Map();
+ this._openContainerCallback = aOnOpenFlatContainer;
+ this._controller = aController;
+}
+
+PlacesTreeView.prototype = {
+ get wrappedJSObject() {
+ return this;
+ },
+
+ __xulStore: null,
+ get _xulStore() {
+ if (!this.__xulStore) {
+ this.__xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+ }
+ return this.__xulStore;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(PTV_interfaces),
+
+ // Bug 761494:
+ // ----------
+ // Some addons use methods from nsINavHistoryResultObserver and
+ // nsINavHistoryResultTreeViewer, without QIing to these interfaces first.
+ // That's not a problem when the view is retrieved through the
+ // <tree>.view getter (which returns the wrappedJSObject of this object),
+ // it raises an issue when the view retrieved through the treeBoxObject.view
+ // getter. Thus, to avoid breaking addons, the interfaces are prefetched.
+ classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }),
+
+ /**
+ * This is called once both the result and the tree are set.
+ */
+ _finishInit: function PTV__finishInit() {
+ let selection = this.selection;
+ if (selection)
+ selection.selectEventsSuppressed = true;
+
+ if (!this._rootNode.containerOpen) {
+ // This triggers containerStateChanged which then builds the visible
+ // section.
+ this._rootNode.containerOpen = true;
+ } else
+ this.invalidateContainer(this._rootNode);
+
+ // "Activate" the sorting column and update commands.
+ this.sortingChanged(this._result.sortingMode);
+
+ if (selection)
+ selection.selectEventsSuppressed = false;
+ },
+
+ uninit() {
+ if (this._editingObservers) {
+ for (let observer of this._editingObservers.values()) {
+ observer.disconnect();
+ }
+ delete this._editingObservers;
+ }
+ },
+
+ /**
+ * Plain Container: container result nodes which may never include sub
+ * hierarchies.
+ *
+ * When the rows array is constructed, we don't set the children of plain
+ * containers. Instead, we keep placeholders for these children. We then
+ * build these children lazily as the tree asks us for information about each
+ * row. Luckily, the tree doesn't ask about rows outside the visible area.
+ *
+ * @see _getNodeForRow and _getRowForNode for the actual magic.
+ *
+ * @note It's guaranteed that all containers are listed in the rows
+ * elements array. It's also guaranteed that separators (if they're not
+ * filtered, see below) are listed in the visible elements array, because
+ * bookmark folders are never built lazily, as described above.
+ *
+ * @param aContainer
+ * A container result node.
+ *
+ * @return true if aContainer is a plain container, false otherwise.
+ */
+ _isPlainContainer: function PTV__isPlainContainer(aContainer) {
+ // Livemarks are always plain containers.
+ if (this._controller.hasCachedLivemarkInfo(aContainer))
+ return true;
+
+ // We don't know enough about non-query containers.
+ if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode))
+ return false;
+
+ switch (aContainer.queryOptions.resultType) {
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY:
+ return false;
+ }
+
+ // If it's a folder, it's not a plain container.
+ let nodeType = aContainer.type;
+ return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
+ nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+ },
+
+ /**
+ * Gets the row number for a given node. Assumes that the given node is
+ * visible (i.e. it's not an obsolete node).
+ *
+ * @param aNode
+ * A result node. Do not pass an obsolete node, or any
+ * node which isn't supposed to be in the tree (e.g. separators in
+ * sorted trees).
+ * @param [optional] aForceBuild
+ * @see _isPlainContainer.
+ * If true, the row will be computed even if the node still isn't set
+ * in our rows array.
+ * @param [optional] aParentRow
+ * The row of aNode's parent. Ignored for the root node.
+ * @param [optional] aNodeIndex
+ * The index of aNode in its parent. Only used if aParentRow is
+ * set too.
+ *
+ * @throws if aNode is invisible.
+ * @note If aParentRow and aNodeIndex are passed and parent is a plain
+ * container, this method will just return a calculated row value, without
+ * making assumptions on existence of the node at that position.
+ * @return aNode's row if it's in the rows list or if aForceBuild is set, -1
+ * otherwise.
+ */
+ _getRowForNode:
+ function PTV__getRowForNode(aNode, aForceBuild, aParentRow, aNodeIndex) {
+ if (aNode == this._rootNode)
+ throw new Error("The root node is never visible");
+
+ // A node is removed form the view either if it has no parent or if its
+ // root-ancestor is not the root node (in which case that's the node
+ // for which nodeRemoved was called).
+ let ancestors = Array.from(PlacesUtils.nodeAncestors(aNode));
+ if (ancestors.length == 0 ||
+ ancestors[ancestors.length - 1] != this._rootNode) {
+ throw new Error("Removed node passed to _getRowForNode");
+ }
+
+ // Ensure that the entire chain is open, otherwise that node is invisible.
+ for (let ancestor of ancestors) {
+ if (!ancestor.containerOpen)
+ throw new Error("Invisible node passed to _getRowForNode");
+ }
+
+ // Non-plain containers are initially built with their contents.
+ let parent = aNode.parent;
+ let parentIsPlain = this._isPlainContainer(parent);
+ if (!parentIsPlain) {
+ if (parent == this._rootNode) {
+ return this._rows.indexOf(aNode);
+ }
+
+ return this._rows.indexOf(aNode, aParentRow);
+ }
+
+ let row = -1;
+ let useNodeIndex = typeof(aNodeIndex) == "number";
+ if (parent == this._rootNode) {
+ row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
+ } else if (useNodeIndex && typeof(aParentRow) == "number") {
+ // If we have both the row of the parent node, and the node's index, we
+ // can avoid searching the rows array if the parent is a plain container.
+ row = aParentRow + aNodeIndex + 1;
+ } else {
+ // Look for the node in the nodes array. Start the search at the parent
+ // row. If the parent row isn't passed, we'll pass undefined to indexOf,
+ // which is fine.
+ row = this._rows.indexOf(aNode, aParentRow);
+ if (row == -1 && aForceBuild) {
+ let parentRow = typeof(aParentRow) == "number" ? aParentRow
+ : this._getRowForNode(parent);
+ row = parentRow + parent.getChildIndex(aNode) + 1;
+ }
+ }
+
+ if (row != -1) {
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
+ this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
+ this._rows[row] = aNode;
+ }
+
+ return row;
+ },
+
+ /**
+ * Given a row, finds and returns the parent details of the associated node.
+ *
+ * @param aChildRow
+ * Row number.
+ * @return [parentNode, parentRow]
+ */
+ _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) {
+ let node = this._getNodeForRow(aChildRow);
+ let parent = (node === null) ? this._rootNode : node.parent;
+
+ // The root node is never visible
+ if (parent == this._rootNode)
+ return [this._rootNode, -1];
+
+ let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
+ return [parent, parentRow];
+ },
+
+ /**
+ * Gets the node at a given row.
+ */
+ _getNodeForRow: function PTV__getNodeForRow(aRow) {
+ if (aRow < 0) {
+ return null;
+ }
+
+ let node = this._rows[aRow];
+ if (node !== undefined)
+ return node;
+
+ // Find the nearest node.
+ let rowNode, row;
+ for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
+ rowNode = this._rows[i];
+ row = i;
+ }
+
+ // If there's no container prior to the given row, it's a child of
+ // the root node (remember: all containers are listed in the rows array).
+ if (!rowNode) {
+ let newNode = this._rootNode.getChild(aRow);
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
+ this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
+ return this._rows[aRow] = newNode;
+ }
+
+ // Unset elements may exist only in plain containers. Thus, if the nearest
+ // node is a container, it's the row's parent, otherwise, it's a sibling.
+ if (rowNode instanceof Ci.nsINavHistoryContainerResultNode) {
+ let newNode = rowNode.getChild(aRow - row - 1);
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
+ this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
+ return this._rows[aRow] = newNode;
+ }
+
+ let [parent, parentRow] = this._getParentByChildRow(row);
+ let newNode = parent.getChild(aRow - parentRow - 1);
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
+ this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
+ return this._rows[aRow] = newNode;
+ },
+
+ /**
+ * This takes a container and recursively appends our rows array per its
+ * contents. Assumes that the rows arrays has no rows for the given
+ * container.
+ *
+ * @param [in] aContainer
+ * A container result node.
+ * @param [in] aFirstChildRow
+ * The first row at which nodes may be inserted to the row array.
+ * In other words, that's aContainer's row + 1.
+ * @param [out] aToOpen
+ * An array of containers to open once the build is done.
+ *
+ * @return the number of rows which were inserted.
+ */
+ _buildVisibleSection:
+ function PTV__buildVisibleSection(aContainer, aFirstChildRow, aToOpen) {
+ // There's nothing to do if the container is closed.
+ if (!aContainer.containerOpen)
+ return 0;
+
+ // Inserting the new elements into the rows array in one shot (by
+ // Array.prototype.concat) is faster than resizing the array (by splice) on each loop
+ // iteration.
+ let cc = aContainer.childCount;
+ let newElements = new Array(cc);
+ // We need to clean up the node details from aFirstChildRow + 1 to the end of rows.
+ for (let i = aFirstChildRow + 1; i < this._rows.length; i++) {
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[i]));
+ }
+ this._rows = this._rows.splice(0, aFirstChildRow)
+ .concat(newElements, this._rows);
+
+ if (this._isPlainContainer(aContainer))
+ return cc;
+
+ let sortingMode = this._result.sortingMode;
+
+ let rowsInserted = 0;
+ for (let i = 0; i < cc; i++) {
+ let curChild = aContainer.getChild(i);
+ let curChildType = curChild.type;
+
+ let row = aFirstChildRow + rowsInserted;
+
+ // Don't display separators when sorted.
+ if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // Remove the element for the filtered separator.
+ // Notice that the rows array was initially resized to include all
+ // children.
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
+ this._rows.splice(row, 1);
+ continue;
+ }
+ }
+
+ this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
+ this._nodeDetails.set(makeNodeDetailsKey(curChild), curChild);
+ this._rows[row] = curChild;
+ rowsInserted++;
+
+ // Recursively do containers.
+ if (!this._flatList &&
+ curChild instanceof Ci.nsINavHistoryContainerResultNode &&
+ !this._controller.hasCachedLivemarkInfo(curChild)) {
+ let uri = curChild.uri;
+ let isopen = false;
+
+ if (uri) {
+ let val = this._xulStore.getValue(document.documentURI, uri, "open");
+ isopen = (val == "true");
+ }
+
+ if (isopen != curChild.containerOpen)
+ aToOpen.push(curChild);
+ else if (curChild.containerOpen && curChild.childCount > 0)
+ rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
+ }
+ }
+
+ return rowsInserted;
+ },
+
+ /**
+ * This counts how many rows a node takes in the tree. For containers it
+ * will count the node itself plus any child node following it.
+ */
+ _countVisibleRowsForNodeAtRow:
+ function PTV__countVisibleRowsForNodeAtRow(aNodeRow) {
+ let node = this._rows[aNodeRow];
+
+ // If it's not listed yet, we know that it's a leaf node (instanceof also
+ // null-checks).
+ if (!(node instanceof Ci.nsINavHistoryContainerResultNode))
+ return 1;
+
+ let outerLevel = node.indentLevel;
+ for (let i = aNodeRow + 1; i < this._rows.length; i++) {
+ let rowNode = this._rows[i];
+ if (rowNode && rowNode.indentLevel <= outerLevel)
+ return i - aNodeRow;
+ }
+
+ // This node plus its children take up the bottom of the list.
+ return this._rows.length - aNodeRow;
+ },
+
+ _getSelectedNodesInRange:
+ function PTV__getSelectedNodesInRange(aFirstRow, aLastRow) {
+ let selection = this.selection;
+ let rc = selection.getRangeCount();
+ if (rc == 0)
+ return [];
+
+ // The visible-area borders are needed for checking whether a
+ // selected row is also visible.
+ let firstVisibleRow = this._tree.getFirstVisibleRow();
+ let lastVisibleRow = this._tree.getLastVisibleRow();
+
+ let nodesInfo = [];
+ for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
+ let min = { }, max = { };
+ selection.getRangeAt(rangeIndex, min, max);
+
+ // If this range does not overlap the replaced chunk, we don't need to
+ // persist the selection.
+ if (max.value < aFirstRow || min.value > aLastRow)
+ continue;
+
+ let firstRow = Math.max(min.value, aFirstRow);
+ let lastRow = Math.min(max.value, aLastRow);
+ for (let i = firstRow; i <= lastRow; i++) {
+ nodesInfo.push({
+ node: this._rows[i],
+ oldRow: i,
+ wasVisible: i >= firstVisibleRow && i <= lastVisibleRow
+ });
+ }
+ }
+
+ return nodesInfo;
+ },
+
+ /**
+ * Tries to find an equivalent node for a node which was removed. We first
+ * look for the original node, in case it was just relocated. Then, if we
+ * that node was not found, we look for a node that has the same itemId, uri
+ * and time values.
+ *
+ * @param aUpdatedContainer
+ * An ancestor of the node which was removed. It does not have to be
+ * its direct parent.
+ * @param aOldNode
+ * The node which was removed.
+ *
+ * @return the row number of an equivalent node for aOldOne, if one was
+ * found, -1 otherwise.
+ */
+ _getNewRowForRemovedNode:
+ function PTV__getNewRowForRemovedNode(aUpdatedContainer, aOldNode) {
+ let parent = aOldNode.parent;
+ if (parent) {
+ // If the node's parent is still set, the node is not obsolete
+ // and we should just find out its new position.
+ // However, if any of the node's ancestor is closed, the node is
+ // invisible.
+ let ancestors = PlacesUtils.nodeAncestors(aOldNode);
+ for (let ancestor of ancestors) {
+ if (!ancestor.containerOpen) {
+ return -1;
+ }
+ }
+
+ return this._getRowForNode(aOldNode, true);
+ }
+
+ // There's a broken edge case here.
+ // If a visit appears in two queries, and the second one was
+ // the old node, we'll select the first one after refresh. There's
+ // nothing we could do about that, because aOldNode.parent is
+ // gone by the time invalidateContainer is called.
+ let newNode = this._nodeDetails.get(makeNodeDetailsKey(aOldNode));
+
+ if (!newNode)
+ return -1;
+
+ return this._getRowForNode(newNode, true);
+ },
+
+ /**
+ * Restores a given selection state as near as possible to the original
+ * selection state.
+ *
+ * @param aNodesInfo
+ * The persisted selection state as returned by
+ * _getSelectedNodesInRange.
+ * @param aUpdatedContainer
+ * The container which was updated.
+ */
+ _restoreSelection:
+ function PTV__restoreSelection(aNodesInfo, aUpdatedContainer) {
+ if (aNodesInfo.length == 0)
+ return;
+
+ let selection = this.selection;
+
+ // Attempt to ensure that previously-visible selection will be visible
+ // if it's re-selected. However, we can only ensure that for one row.
+ let scrollToRow = -1;
+ for (let i = 0; i < aNodesInfo.length; i++) {
+ let nodeInfo = aNodesInfo[i];
+ let row = this._getNewRowForRemovedNode(aUpdatedContainer,
+ nodeInfo.node);
+ // Select the found node, if any.
+ if (row != -1) {
+ selection.rangedSelect(row, row, true);
+ if (nodeInfo.wasVisible && scrollToRow == -1)
+ scrollToRow = row;
+ }
+ }
+
+ // If only one node was previously selected and there's no selection now,
+ // select the node at its old row, if any.
+ if (aNodesInfo.length == 1 && selection.count == 0) {
+ let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
+ if (row != -1) {
+ selection.rangedSelect(row, row, true);
+ if (aNodesInfo[0].wasVisible && scrollToRow == -1)
+ scrollToRow = aNodesInfo[0].oldRow;
+ }
+ }
+
+ if (scrollToRow != -1)
+ this._tree.ensureRowIsVisible(scrollToRow);
+ },
+
+ _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) {
+ const MS_PER_MINUTE = 60000;
+ const MS_PER_DAY = 86400000;
+ let timeMs = aTime / 1000; // PRTime is in microseconds
+
+ // Date is calculated starting from midnight, so the modulo with a day are
+ // milliseconds from today's midnight.
+ // getTimezoneOffset corrects that based on local time, notice midnight
+ // can have a different offset during DST-change days.
+ let dateObj = new Date();
+ let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
+ let midnight = now - (now % MS_PER_DAY);
+ midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
+
+ let timeObj = new Date(timeMs);
+ return timeMs >= midnight ? this._todayFormatter.format(timeObj)
+ : this._dateFormatter.format(timeObj);
+ },
+
+ // We use a different formatter for times within the current day,
+ // so we cache both a "today" formatter and a general date formatter.
+ __todayFormatter: null,
+ get _todayFormatter() {
+ if (!this.__todayFormatter) {
+ const dtOptions = { timeStyle: "short" };
+ this.__todayFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
+ }
+ return this.__todayFormatter;
+ },
+
+ __dateFormatter: null,
+ get _dateFormatter() {
+ if (!this.__dateFormatter) {
+ const dtOptions = {
+ dateStyle: "short",
+ timeStyle: "short"
+ };
+ this.__dateFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
+ }
+ return this.__dateFormatter;
+ },
+
+ COLUMN_TYPE_UNKNOWN: 0,
+ COLUMN_TYPE_TITLE: 1,
+ COLUMN_TYPE_URI: 2,
+ COLUMN_TYPE_DATE: 3,
+ COLUMN_TYPE_VISITCOUNT: 4,
+ COLUMN_TYPE_DESCRIPTION: 5,
+ COLUMN_TYPE_DATEADDED: 6,
+ COLUMN_TYPE_LASTMODIFIED: 7,
+ COLUMN_TYPE_TAGS: 8,
+
+ _getColumnType: function PTV__getColumnType(aColumn) {
+ let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
+
+ switch (columnType) {
+ case "title":
+ return this.COLUMN_TYPE_TITLE;
+ case "url":
+ return this.COLUMN_TYPE_URI;
+ case "date":
+ return this.COLUMN_TYPE_DATE;
+ case "visitCount":
+ return this.COLUMN_TYPE_VISITCOUNT;
+ case "description":
+ return this.COLUMN_TYPE_DESCRIPTION;
+ case "dateAdded":
+ return this.COLUMN_TYPE_DATEADDED;
+ case "lastModified":
+ return this.COLUMN_TYPE_LASTMODIFIED;
+ case "tags":
+ return this.COLUMN_TYPE_TAGS;
+ }
+ return this.COLUMN_TYPE_UNKNOWN;
+ },
+
+ _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) {
+ switch (aSortType) {
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
+ return [this.COLUMN_TYPE_TITLE, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
+ return [this.COLUMN_TYPE_TITLE, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
+ return [this.COLUMN_TYPE_DATE, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
+ return [this.COLUMN_TYPE_DATE, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
+ return [this.COLUMN_TYPE_URI, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
+ return [this.COLUMN_TYPE_URI, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
+ return [this.COLUMN_TYPE_VISITCOUNT, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
+ return [this.COLUMN_TYPE_VISITCOUNT, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
+ if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ return [this.COLUMN_TYPE_DESCRIPTION, false];
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
+ if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ return [this.COLUMN_TYPE_DESCRIPTION, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
+ return [this.COLUMN_TYPE_DATEADDED, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
+ return [this.COLUMN_TYPE_DATEADDED, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
+ return [this.COLUMN_TYPE_LASTMODIFIED, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
+ return [this.COLUMN_TYPE_LASTMODIFIED, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
+ return [this.COLUMN_TYPE_TAGS, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
+ return [this.COLUMN_TYPE_TAGS, true];
+ }
+ return [this.COLUMN_TYPE_UNKNOWN, false];
+ },
+
+ // nsINavHistoryResultObserver
+ nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) {
+ console.assert(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ let parentRow;
+ if (aParentNode != this._rootNode) {
+ parentRow = this._getRowForNode(aParentNode);
+
+ // Update parent when inserting the first item, since twisty has changed.
+ if (aParentNode.childCount == 1)
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Compute the new row number of the node.
+ let row = -1;
+ let cc = aParentNode.childCount;
+ if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
+ // We don't need to worry about sub hierarchies of the parent node
+ // if it's a plain container, or if the new node is its first child.
+ if (aParentNode == this._rootNode)
+ row = aNewIndex;
+ else
+ row = parentRow + aNewIndex + 1;
+ } else {
+ // Here, we try to find the next visible element in the child list so we
+ // can set the new visible index to be right before that. Note that we
+ // have to search down instead of up, because some siblings could have
+ // children themselves that would be in the way.
+ let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) &&
+ this.isSorted();
+ for (let i = aNewIndex + 1; i < cc; i++) {
+ let node = aParentNode.getChild(i);
+ if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
+ // The children have not been shifted so the next item will have what
+ // should be our index.
+ row = this._getRowForNode(node, false, parentRow, i);
+ break;
+ }
+ }
+ if (row < 0) {
+ // At the end of the child list without finding a visible sibling. This
+ // is a little harder because we don't know how many rows the last item
+ // in our list takes up (it could be a container with many children).
+ let prevChild = aParentNode.getChild(aNewIndex - 1);
+ let prevIndex = this._getRowForNode(prevChild, false, parentRow,
+ aNewIndex - 1);
+ row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
+ }
+ }
+
+ this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
+ this._rows.splice(row, 0, aNode);
+ this._tree.rowCountChanged(row, 1);
+
+ if (PlacesUtils.nodeIsContainer(aNode) &&
+ PlacesUtils.asContainer(aNode).containerOpen) {
+ this.invalidateContainer(aNode);
+ }
+ },
+
+ /**
+ * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
+ * removed but the node it is collapsed with is not being removed (this then
+ * just swap out the removee with its collapsing partner). The only time
+ * when we really remove things is when deleting URIs, which will apply to
+ * all collapsees. This function is called sometimes when resorting items.
+ * However, we won't do this when sorted by date because dates will never
+ * change for visits, and date sorting is the only time things are collapsed.
+ */
+ nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) {
+ console.assert(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // XXX bug 517701: We don't know what to do when the root node is removed.
+ if (aNode == this._rootNode)
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ let parentRow = aParentNode == this._rootNode ?
+ undefined : this._getRowForNode(aParentNode, true);
+ let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
+ if (oldRow < 0)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // If the node was exclusively selected, the node next to it will be
+ // selected.
+ let selectNext = false;
+ let selection = this.selection;
+ if (selection.getRangeCount() == 1) {
+ let min = { }, max = { };
+ selection.getRangeAt(0, min, max);
+ if (min.value == max.value &&
+ this.nodeForTreeIndex(min.value) == aNode)
+ selectNext = true;
+ }
+
+ // Remove the node and its children, if any.
+ let count = this._countVisibleRowsForNodeAtRow(oldRow);
+ for (let splicedNode of this._rows.splice(oldRow, count)) {
+ this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
+ }
+ this._tree.rowCountChanged(oldRow, -count);
+
+ // Redraw the parent if its twisty state has changed.
+ if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
+ parentRow = oldRow - 1;
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Restore selection if the node was exclusively selected.
+ if (!selectNext)
+ return;
+
+ // Restore selection.
+ let rowToSelect = Math.min(oldRow, this._rows.length - 1);
+ if (rowToSelect != -1)
+ this.selection.rangedSelect(rowToSelect, rowToSelect, true);
+ },
+
+ nodeMoved:
+ function PTV_nodeMoved(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) {
+ console.assert(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ // Note that at this point the node has already been moved by the backend,
+ // so we must give hints to _getRowForNode to get the old row position.
+ let oldParentRow = aOldParent == this._rootNode ?
+ undefined : this._getRowForNode(aOldParent, true);
+ let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
+ if (oldRow < 0)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // If this node is a container it could take up more than one row.
+ let count = this._countVisibleRowsForNodeAtRow(oldRow);
+
+ // Persist selection state.
+ let nodesToReselect =
+ this._getSelectedNodesInRange(oldRow, oldRow + count);
+ if (nodesToReselect.length > 0)
+ this.selection.selectEventsSuppressed = true;
+
+ // Redraw the parent if its twisty state has changed.
+ if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
+ let parentRow = oldRow - 1;
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Remove node and its children, if any, from the old position.
+ for (let splicedNode of this._rows.splice(oldRow, count)) {
+ this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
+ }
+ this._tree.rowCountChanged(oldRow, -count);
+
+ // Insert the node into the new position.
+ this.nodeInserted(aNewParent, aNode, aNewIndex);
+
+ // Restore selection.
+ if (nodesToReselect.length > 0) {
+ this._restoreSelection(nodesToReselect, aNewParent);
+ this.selection.selectEventsSuppressed = false;
+ }
+ },
+
+ _invalidateCellValue: function PTV__invalidateCellValue(aNode,
+ aColumnType) {
+ console.assert(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Nothing to do for the root node.
+ if (aNode == this._rootNode)
+ return;
+
+ let row = this._getRowForNode(aNode);
+ if (row == -1)
+ return;
+
+ let column = this._findColumnByType(aColumnType);
+ if (column && !column.element.hidden) {
+ if (aColumnType == this.COLUMN_TYPE_TITLE)
+ this._tree.removeImageCacheEntry(row, column);
+ this._tree.invalidateCell(row, column);
+ }
+
+ // Last modified time is altered for almost all node changes.
+ if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
+ let lastModifiedColumn =
+ this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
+ if (lastModifiedColumn && !lastModifiedColumn.hidden)
+ this._tree.invalidateCell(row, lastModifiedColumn);
+ }
+ },
+
+ _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) {
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ let placesNode = aNode;
+ // Need to check containerOpen since getLivemark is async.
+ if (!placesNode.containerOpen)
+ return;
+
+ let children = aLivemark.getNodesForContainer(placesNode);
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ this.nodeInserted(placesNode, child, i);
+ }
+ }, Cu.reportError);
+ },
+
+ nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ },
+
+ nodeURIChanged: function PTV_nodeURIChanged(aNode, aOldURI) {
+ this._nodeDetails.delete(makeNodeDetailsKey({uri: aOldURI,
+ itemId: aNode.itemId,
+ time: aNode.time}));
+ this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
+ },
+
+ nodeIconChanged: function PTV_nodeIconChanged(aNode) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ },
+
+ nodeHistoryDetailsChanged:
+ function PTV_nodeHistoryDetailsChanged(aNode, aOldVisitDate,
+ aOldVisitCount) {
+ this._nodeDetails.delete(makeNodeDetailsKey({uri: aNode.uri,
+ itemId: aNode.itemId,
+ time: aOldVisitDate}));
+ this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
+ if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) {
+ // Find the node in the parent.
+ let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent);
+ for (let i = parentRow; i < this._rows.length; i++) {
+ let child = this.nodeForTreeIndex(i);
+ if (child.uri == aNode.uri) {
+ this._cellProperties.delete(child);
+ this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE);
+ break;
+ }
+ }
+ return;
+ }
+
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
+ },
+
+ nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
+ },
+
+ nodeKeywordChanged(aNode, aNewKeyword) {},
+
+ nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
+ if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
+ } else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ this._controller.cacheLivemarkInfo(aNode, aLivemark);
+ let properties = this._cellProperties.get(aNode);
+ this._cellProperties.set(aNode, properties += " livemark");
+ // The livemark attribute is set as a cell property on the title cell.
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ }, Cu.reportError);
+ }
+ },
+
+ nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
+ },
+
+ nodeLastModifiedChanged:
+ function PTV_nodeLastModifiedChanged(aNode, aNewValue) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
+ },
+
+ containerStateChanged:
+ function PTV_containerStateChanged(aNode, aOldState, aNewState) {
+ this.invalidateContainer(aNode);
+
+ if (PlacesUtils.nodeIsFolder(aNode) ||
+ (this._flatList && aNode == this._rootNode)) {
+ let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
+ if (queryOptions.excludeItems) {
+ return;
+ }
+ if (aNode.itemId != -1) { // run when there's a valid node id
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ let shouldInvalidate =
+ !this._controller.hasCachedLivemarkInfo(aNode);
+ this._controller.cacheLivemarkInfo(aNode, aLivemark);
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ aLivemark.registerForUpdates(aNode, this);
+ // Prioritize the current livemark.
+ aLivemark.reload();
+ PlacesUtils.livemarks.reloadLivemarks();
+ if (shouldInvalidate)
+ this.invalidateContainer(aNode);
+ } else {
+ aLivemark.unregisterForUpdates(aNode);
+ }
+ }, () => undefined);
+ }
+ }
+ },
+
+ invalidateContainer: function PTV_invalidateContainer(aContainer) {
+ console.assert(this._result, "Need to have a result to update");
+ if (!this._tree)
+ return;
+
+ // If we are currently editing, don't invalidate the container until we
+ // finish.
+ if (this._tree.element.getAttribute("editing")) {
+ if (!this._editingObservers) {
+ this._editingObservers = new Map();
+ }
+ if (!this._editingObservers.has(aContainer)) {
+ let mutationObserver = new MutationObserver(() => {
+ Services.tm.dispatchToMainThread(
+ () => this.invalidateContainer(aContainer));
+ let observer = this._editingObservers.get(aContainer);
+ observer.disconnect();
+ this._editingObservers.delete(aContainer);
+ });
+
+ mutationObserver.observe(this._tree.element, {
+ attributes: true,
+ attributeFilter: ["editing"],
+ });
+
+ this._editingObservers.set(aContainer, mutationObserver);
+ }
+ return;
+ }
+
+ let startReplacement, replaceCount;
+ if (aContainer == this._rootNode) {
+ startReplacement = 0;
+ replaceCount = this._rows.length;
+
+ // If the root node is now closed, the tree is empty.
+ if (!this._rootNode.containerOpen) {
+ this._nodeDetails.clear();
+ this._rows = [];
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ return;
+ }
+ } else {
+ // Update the twisty state.
+ let row = this._getRowForNode(aContainer);
+ this._tree.invalidateRow(row);
+
+ // We don't replace the container node itself, so we should decrease the
+ // replaceCount by 1.
+ startReplacement = row + 1;
+ replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
+ }
+
+ // Persist selection state.
+ let nodesToReselect =
+ this._getSelectedNodesInRange(startReplacement,
+ startReplacement + replaceCount);
+
+ // Now update the number of elements.
+ this.selection.selectEventsSuppressed = true;
+
+ // First remove the old elements
+ for (let splicedNode of this._rows.splice(startReplacement, replaceCount)) {
+ this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
+ }
+
+ // If the container is now closed, we're done.
+ if (!aContainer.containerOpen) {
+ let oldSelectionCount = this.selection.count;
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ // Select the row next to the closed container if any of its
+ // children were selected, and nothing else is selected.
+ if (nodesToReselect.length > 0 &&
+ nodesToReselect.length == oldSelectionCount) {
+ this.selection.rangedSelect(startReplacement, startReplacement, true);
+ this._tree.ensureRowIsVisible(startReplacement);
+ }
+
+ this.selection.selectEventsSuppressed = false;
+ return;
+ }
+
+ // Otherwise, start a batch first.
+ this._tree.beginUpdateBatch();
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ let toOpenElements = [];
+ let elementsAddedCount = this._buildVisibleSection(aContainer,
+ startReplacement,
+ toOpenElements);
+ if (elementsAddedCount)
+ this._tree.rowCountChanged(startReplacement, elementsAddedCount);
+
+ if (!this._flatList) {
+ // Now, open any containers that were persisted.
+ for (let i = 0; i < toOpenElements.length; i++) {
+ let item = toOpenElements[i];
+ let parent = item.parent;
+
+ // Avoid recursively opening containers.
+ while (parent) {
+ if (parent.uri == item.uri)
+ break;
+ parent = parent.parent;
+ }
+
+ // If we don't have a parent, we made it all the way to the root
+ // and didn't find a match, so we can open our item.
+ if (!parent && !item.containerOpen)
+ item.containerOpen = true;
+ }
+ }
+
+ if (this._controller.hasCachedLivemarkInfo(aContainer)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (!queryOptions.excludeItems) {
+ this._populateLivemarkContainer(aContainer);
+ }
+ }
+
+ this._tree.endUpdateBatch();
+
+ // Restore selection.
+ this._restoreSelection(nodesToReselect, aContainer);
+ this.selection.selectEventsSuppressed = false;
+ },
+
+ _columns: [],
+ _findColumnByType: function PTV__findColumnByType(aColumnType) {
+ if (this._columns[aColumnType])
+ return this._columns[aColumnType];
+
+ let columns = this._tree.columns;
+ let colCount = columns.count;
+ for (let i = 0; i < colCount; i++) {
+ let column = columns.getColumnAt(i);
+ let columnType = this._getColumnType(column);
+ this._columns[columnType] = column;
+ if (columnType == aColumnType)
+ return column;
+ }
+
+ // That's completely valid. Most of our trees actually include just the
+ // title column.
+ return null;
+ },
+
+ sortingChanged: function PTV__sortingChanged(aSortingMode) {
+ if (!this._tree || !this._result)
+ return;
+
+ // Depending on the sort mode, certain commands may be disabled.
+ window.updateCommands("sort");
+
+ let columns = this._tree.columns;
+
+ // Clear old sorting indicator.
+ let sortedColumn = columns.getSortedColumn();
+ if (sortedColumn)
+ sortedColumn.element.removeAttribute("sortDirection");
+
+ // Set new sorting indicator by looking through all columns for ours.
+ if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE)
+ return;
+
+ let [desiredColumn, desiredIsDescending] =
+ this._sortTypeToColumnType(aSortingMode);
+ let column = this._findColumnByType(desiredColumn);
+ if (column) {
+ let sortDir = desiredIsDescending ? "descending" : "ascending";
+ column.element.setAttribute("sortDirection", sortDir);
+ }
+ },
+
+ _inBatchMode: false,
+ batching: function PTV__batching(aToggleMode) {
+ if (this._inBatchMode != aToggleMode) {
+ this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
+ if (this._inBatchMode) {
+ this._tree.beginUpdateBatch();
+ } else {
+ this._tree.endUpdateBatch();
+ }
+ }
+ },
+
+ get result() {
+ return this._result;
+ },
+ set result(val) {
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._rootNode.containerOpen = false;
+ }
+
+ if (val) {
+ this._result = val;
+ this._rootNode = this._result.root;
+ this._cellProperties = new Map();
+ this._cuttingNodes = new Set();
+ } else if (this._result) {
+ delete this._result;
+ delete this._rootNode;
+ delete this._cellProperties;
+ delete this._cuttingNodes;
+ }
+
+ // If the tree is not set yet, setTree will call finishInit.
+ if (this._tree && val)
+ this._finishInit();
+
+ return val;
+ },
+
+ /**
+ * This allows you to get at the real node for a given row index. This is
+ * only valid when a tree is attached.
+ *
+ * @param {Integer} aIndex The index for the node to get.
+ * @return {Ci.nsINavHistoryResultNode} The node.
+ * @throws Cr.NS_ERROR_INVALID_ARG if the index is greater than the number of
+ * rows.
+ */
+ nodeForTreeIndex(aIndex) {
+ if (aIndex > this._rows.length)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ return this._getNodeForRow(aIndex);
+ },
+
+ /**
+ * Reverse of nodeForTreeIndex, returns the row index for a given result node.
+ * The node should be part of the tree.
+ *
+ * @param {Ci.nsINavHistoryResultNode} aNode The node to look for in the tree.
+ * @returns {Integer} The found index, or -1 if the item is not visible or not found.
+ */
+ treeIndexForNode(aNode) {
+ // The API allows passing invisible nodes.
+ try {
+ return this._getRowForNode(aNode, true);
+ } catch (ex) { }
+
+ return -1;
+ },
+
+ // nsITreeView
+ get rowCount() {
+ return this._rows.length;
+ },
+ get selection() {
+ return this._selection;
+ },
+ set selection(val) {
+ this._selection = val;
+ },
+
+ getRowProperties() { return ""; },
+
+ getCellProperties:
+ function PTV_getCellProperties(aRow, aColumn) {
+ // for anonid-trees, we need to add the column-type manually
+ var props = "";
+ let columnType = aColumn.element.getAttribute("anonid");
+ if (columnType)
+ props += columnType;
+ else
+ columnType = aColumn.id;
+
+ // Set the "ltr" property on url cells
+ if (columnType == "url")
+ props += " ltr";
+
+ if (columnType != "title")
+ return props;
+
+ let node = this._getNodeForRow(aRow);
+
+ if (this._cuttingNodes.has(node)) {
+ props += " cutting";
+ }
+
+ let properties = this._cellProperties.get(node);
+ if (properties === undefined) {
+ properties = "";
+ let itemId = node.itemId;
+ let nodeType = node.type;
+ if (PlacesUtils.containerTypes.includes(nodeType)) {
+ if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
+ properties += " query";
+ if (PlacesUtils.nodeIsTagQuery(node))
+ properties += " tagContainer";
+ else if (PlacesUtils.nodeIsDay(node))
+ properties += " dayContainer";
+ else if (PlacesUtils.nodeIsHost(node))
+ properties += " hostContainer";
+ } else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
+ nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
+ if (itemId != -1) {
+ if (this._controller.hasCachedLivemarkInfo(node)) {
+ properties += " livemark";
+ } else {
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ this._controller.cacheLivemarkInfo(node, aLivemark);
+ let livemarkProps = this._cellProperties.get(node);
+ this._cellProperties.set(node, livemarkProps += " livemark");
+ // The livemark attribute is set as a cell property on the title cell.
+ this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
+ }, () => undefined);
+ }
+ }
+ }
+
+ if (itemId == -1) {
+ switch (node.bookmarkGuid) {
+ case PlacesUtils.bookmarks.virtualToolbarGuid:
+ properties += ` queryFolder_${PlacesUtils.bookmarks.toolbarGuid}`;
+ break;
+ case PlacesUtils.bookmarks.virtualMenuGuid:
+ properties += ` queryFolder_${PlacesUtils.bookmarks.menuGuid}`;
+ break;
+ case PlacesUtils.bookmarks.virtualUnfiledGuid:
+ properties += ` queryFolder_${PlacesUtils.bookmarks.unfiledGuid}`;
+ break;
+ }
+ } else {
+ let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
+ if (queryName)
+ properties += " OrganizerQuery_" + queryName;
+ }
+ } else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
+ properties += " separator";
+ else if (PlacesUtils.nodeIsURI(node)) {
+ properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
+
+ if (this._controller.hasCachedLivemarkInfo(node.parent)) {
+ properties += " livemarkItem";
+ if (node.accessCount) {
+ properties += " visited";
+ }
+ }
+ }
+
+ this._cellProperties.set(node, properties);
+ }
+
+ return props + " " + properties;
+ },
+
+ getColumnProperties(aColumn) { return ""; },
+
+ isContainer: function PTV_isContainer(aRow) {
+ // Only leaf nodes aren't listed in the rows array.
+ let node = this._rows[aRow];
+ if (node === undefined || !PlacesUtils.nodeIsContainer(node))
+ return false;
+
+ // Flat-lists may ignore expandQueries and other query options when
+ // they are asked to open a container.
+ if (this._flatList)
+ return true;
+
+ // Treat non-expandable childless queries as non-containers, unless they
+ // are tags.
+ if (PlacesUtils.nodeIsQuery(node) && !PlacesUtils.nodeIsTagQuery(node)) {
+ PlacesUtils.asQuery(node);
+ return node.queryOptions.expandQueries || node.hasChildren;
+ }
+ return true;
+ },
+
+ isContainerOpen: function PTV_isContainerOpen(aRow) {
+ if (this._flatList)
+ return false;
+
+ // All containers are listed in the rows array.
+ return this._rows[aRow].containerOpen;
+ },
+
+ isContainerEmpty: function PTV_isContainerEmpty(aRow) {
+ if (this._flatList)
+ return true;
+
+ let node = this._rows[aRow];
+ if (this._controller.hasCachedLivemarkInfo(node)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ return queryOptions.excludeItems;
+ }
+
+ // All containers are listed in the rows array.
+ return !node.hasChildren;
+ },
+
+ isSeparator: function PTV_isSeparator(aRow) {
+ // All separators are listed in the rows array.
+ let node = this._rows[aRow];
+ return node && PlacesUtils.nodeIsSeparator(node);
+ },
+
+ isSorted: function PTV_isSorted() {
+ return this._result.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ },
+
+ canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // Drop position into a sorted treeview would be wrong.
+ if (this.isSorted())
+ return false;
+
+ let ip = this._getInsertionPoint(aRow, aOrientation);
+ return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
+ },
+
+ _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
+ let container = this._result.root;
+ let dropNearNode = null;
+ // When there's no selection, assume the container is the container
+ // the view is populated from (i.e. the result's itemId).
+ if (index != -1) {
+ let lastSelected = this.nodeForTreeIndex(index);
+ if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
+ // If the last selected item is an open container, append _into_
+ // it, rather than insert adjacent to it.
+ container = lastSelected;
+ index = -1;
+ } else if (lastSelected.containerOpen &&
+ orientation == Ci.nsITreeView.DROP_AFTER &&
+ lastSelected.hasChildren) {
+ // If the last selected node is an open container and the user is
+ // trying to drag into it as a first node, really insert into it.
+ container = lastSelected;
+ orientation = Ci.nsITreeView.DROP_ON;
+ index = 0;
+ } else {
+ // Use the last-selected node's container.
+ container = lastSelected.parent;
+
+ // During its Drag & Drop operation, the tree code closes-and-opens
+ // containers very often (part of the XUL "spring-loaded folders"
+ // implementation). And in certain cases, we may reach a closed
+ // container here. However, we can simply bail out when this happens,
+ // because we would then be back here in less than a millisecond, when
+ // the container had been reopened.
+ if (!container || !container.containerOpen)
+ return null;
+
+ // Avoid the potentially expensive call to getChildIndex
+ // if we know this container doesn't allow insertion.
+ if (this._controller.disallowInsertion(container))
+ return null;
+
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (queryOptions.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // If we are within a sorted view, insert at the end.
+ index = -1;
+ } else if (queryOptions.excludeItems ||
+ queryOptions.excludeQueries ||
+ queryOptions.excludeReadOnlyFolders) {
+ // Some item may be invisible, insert near last selected one.
+ // We don't replace index here to avoid requests to the db,
+ // instead it will be calculated later by the controller.
+ index = -1;
+ dropNearNode = lastSelected;
+ } else {
+ let lsi = container.getChildIndex(lastSelected);
+ index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+ }
+ }
+ }
+
+ if (this._controller.disallowInsertion(container))
+ return null;
+
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ let tagName = null;
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ if (!tagName)
+ return null;
+ }
+
+ return new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(container),
+ parentGuid: PlacesUtils.getConcreteItemGuid(container),
+ index, orientation, tagName, dropNearNode
+ });
+ },
+
+ drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
+ // We are responsible for translating the |index| and |orientation|
+ // parameters into a container id and index within the container,
+ // since this information is specific to the tree view.
+ let ip = this._getInsertionPoint(aRow, aOrientation);
+ if (ip) {
+ PlacesControllerDragHelper.onDrop(ip, aDataTransfer, this._tree.element)
+ .catch(Cu.reportError)
+ .then(() => {
+ // We should only clear the drop target once
+ // the onDrop is complete, as it is an async function.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ });
+ }
+ },
+
+ getParentIndex: function PTV_getParentIndex(aRow) {
+ let [, parentRow] = this._getParentByChildRow(aRow);
+ return parentRow;
+ },
+
+ hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) {
+ if (aRow == this._rows.length - 1) {
+ // The last row has no sibling.
+ return false;
+ }
+
+ let node = this._rows[aRow];
+ if (node === undefined || this._isPlainContainer(node.parent)) {
+ // The node is a child of a plain container.
+ // If the next row is either unset or has the same parent,
+ // it's a sibling.
+ let nextNode = this._rows[aRow + 1];
+ return (nextNode == undefined || nextNode.parent == node.parent);
+ }
+
+ let thisLevel = node.indentLevel;
+ for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
+ let rowNode = this._getNodeForRow(i);
+ let nextLevel = rowNode.indentLevel;
+ if (nextLevel == thisLevel)
+ return true;
+ if (nextLevel < thisLevel)
+ break;
+ }
+
+ return false;
+ },
+
+ getLevel(aRow) {
+ return this._getNodeForRow(aRow).indentLevel;
+ },
+
+ getImageSrc: function PTV_getImageSrc(aRow, aColumn) {
+ // Only the title column has an image.
+ if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE)
+ return "";
+
+ let node = this._getNodeForRow(aRow);
+ return node.icon;
+ },
+
+ getCellValue(aRow, aColumn) { },
+
+ getCellText: function PTV_getCellText(aRow, aColumn) {
+ let node = this._getNodeForRow(aRow);
+ switch (this._getColumnType(aColumn)) {
+ case this.COLUMN_TYPE_TITLE:
+ // normally, this is just the title, but we don't want empty items in
+ // the tree view so return a special string if the title is empty.
+ // Do it here so that callers can still get at the 0 length title
+ // if they go through the "result" API.
+ if (PlacesUtils.nodeIsSeparator(node))
+ return "";
+ return PlacesUIUtils.getBestTitle(node, true);
+ case this.COLUMN_TYPE_TAGS:
+ return node.tags;
+ case this.COLUMN_TYPE_URI:
+ if (PlacesUtils.nodeIsURI(node))
+ return node.uri;
+ return "";
+ case this.COLUMN_TYPE_DATE:
+ let nodeTime = node.time;
+ if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
+ // hosts and days shouldn't have a value for the date column.
+ // Actually, you could argue this point, but looking at the
+ // results, seeing the most recently visited date is not what
+ // I expect, and gives me no information I know how to use.
+ // Only show this for URI-based items.
+ return "";
+ }
+
+ return this._convertPRTimeToString(nodeTime);
+ case this.COLUMN_TYPE_VISITCOUNT:
+ return node.accessCount;
+ case this.COLUMN_TYPE_DESCRIPTION:
+ if (node.itemId != -1) {
+ try {
+ return PlacesUtils.annotations.
+ getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
+ } catch (ex) { /* has no description */ }
+ }
+ return "";
+ case this.COLUMN_TYPE_DATEADDED:
+ if (node.dateAdded)
+ return this._convertPRTimeToString(node.dateAdded);
+ return "";
+ case this.COLUMN_TYPE_LASTMODIFIED:
+ if (node.lastModified)
+ return this._convertPRTimeToString(node.lastModified);
+ return "";
+ }
+ return "";
+ },
+
+ setTree: function PTV_setTree(aTree) {
+ // If we are replacing the tree during a batch, there is a concrete risk
+ // that the treeView goes out of sync, thus it's safer to end the batch now.
+ // This is a no-op if we are not batching.
+ this.batching(false);
+
+ let hasOldTree = this._tree != null;
+ this._tree = aTree;
+
+ if (this._result) {
+ if (hasOldTree) {
+ // detach from result when we are detaching from the tree.
+ // This breaks the reference cycle between us and the result.
+ if (!aTree) {
+ this._result.removeObserver(this);
+ this._rootNode.containerOpen = false;
+ }
+ }
+ if (aTree)
+ this._finishInit();
+ }
+ },
+
+ toggleOpenState: function PTV_toggleOpenState(aRow) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ let node = this._rows[aRow];
+ if (this._flatList && this._openContainerCallback) {
+ this._openContainerCallback(node);
+ return;
+ }
+
+ // Persist containers open status, but never persist livemarks.
+ if (!this._controller.hasCachedLivemarkInfo(node)) {
+ let uri = node.uri;
+
+ if (uri) {
+ let docURI = document.documentURI;
+
+ if (node.containerOpen) {
+ this._xulStore.removeValue(docURI, uri, "open");
+ } else {
+ this._xulStore.setValue(docURI, uri, "open", "true");
+ }
+ }
+ }
+
+ node.containerOpen = !node.containerOpen;
+ },
+
+ cycleHeader: function PTV_cycleHeader(aColumn) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // Sometimes you want a tri-state sorting, and sometimes you don't. This
+ // rule allows tri-state sorting when the root node is a folder. This will
+ // catch the most common cases. When you are looking at folders, you want
+ // the third state to reset the sorting to the natural bookmark order. When
+ // you are looking at history, that third state has no meaning so we try
+ // to disallow it.
+ //
+ // The problem occurs when you have a query that results in bookmark
+ // folders. One example of this is the subscriptions view. In these cases,
+ // this rule doesn't allow you to sort those sub-folders by their natural
+ // order.
+ let allowTriState = PlacesUtils.nodeIsFolder(this._result.root);
+
+ let oldSort = this._result.sortingMode;
+ let oldSortingAnnotation = this._result.sortingAnnotation;
+ let newSort;
+ let newSortingAnnotation = "";
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ switch (this._getColumnType(aColumn)) {
+ case this.COLUMN_TYPE_TITLE:
+ if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING)
+ newSort = NHQO.SORT_BY_TITLE_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_TITLE_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_URI:
+ if (oldSort == NHQO.SORT_BY_URI_ASCENDING)
+ newSort = NHQO.SORT_BY_URI_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_URI_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_DATE:
+ if (oldSort == NHQO.SORT_BY_DATE_ASCENDING)
+ newSort = NHQO.SORT_BY_DATE_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_DATE_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_DATE_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_VISITCOUNT:
+ // visit count default is unusual because we sort by descending
+ // by default because you are most likely to be looking for
+ // highly visited sites when you click it
+ if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
+ newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
+
+ break;
+ case this.COLUMN_TYPE_DESCRIPTION:
+ if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
+ oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
+ newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+ newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
+ } else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
+ oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ newSort = NHQO.SORT_BY_NONE;
+ else {
+ newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING;
+ newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
+ }
+
+ break;
+ case this.COLUMN_TYPE_DATEADDED:
+ if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING)
+ newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_LASTMODIFIED:
+ if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING)
+ newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_TAGS:
+ if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING)
+ newSort = NHQO.SORT_BY_TAGS_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_TAGS_ASCENDING;
+
+ break;
+ default:
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ this._result.sortingAnnotation = newSortingAnnotation;
+ this._result.sortingMode = newSort;
+ },
+
+ isEditable: function PTV_isEditable(aRow, aColumn) {
+ // At this point we only support editing the title field.
+ if (aColumn.index != 0)
+ return false;
+
+ let node = this._rows[aRow];
+ if (!node) {
+ Cu.reportError("isEditable called for an unbuilt row.");
+ return false;
+ }
+ let itemGuid = node.bookmarkGuid;
+
+ // Only bookmark-nodes are editable. Fortunately, this checks also takes
+ // care of livemark children.
+ if (itemGuid == "")
+ return false;
+
+ // The following items are also not editable, even though they are bookmark
+ // items.
+ // * places-roots
+ // * the left pane special folders and queries (those are place: uri
+ // bookmarks)
+ // * separators
+ //
+ // Note that concrete itemIds aren't used intentionally. For example, we
+ // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar,
+ // except for the one under All Bookmarks.
+ if (PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemGuid) ||
+ PlacesUtils.isQueryGeneratedFolder(itemGuid))
+ return false;
+
+ let parentId = PlacesUtils.getConcreteItemId(node.parent);
+ if (parentId == PlacesUIUtils.leftPaneFolderId) {
+ // Note that the for the time being this is the check that actually
+ // blocks renaming places "roots", and not the isRootItem check above.
+ // That's because places root are only exposed through folder shortcuts
+ // descendants of the left pane folder.
+ return false;
+ }
+
+ return true;
+ },
+
+ setCellText: function PTV_setCellText(aRow, aColumn, aText) {
+ // We may only get here if the cell is editable.
+ let node = this._rows[aRow];
+ if (node.title != aText) {
+ PlacesTransactions.EditTitle({ guid: node.bookmarkGuid, title: aText })
+ .transact().catch(Cu.reportError);
+ }
+ },
+
+ toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
+ let currentVal = this._cuttingNodes.has(aNode);
+ if (currentVal != aValue) {
+ if (aValue)
+ this._cuttingNodes.add(aNode);
+ else
+ this._cuttingNodes.delete(aNode);
+
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ }
+ },
+
+ selectionChanged() { },
+ cycleCell(aRow, aColumn) { },
+ isSelectable(aRow, aColumn) { return false; },
+};
diff --git a/comm/suite/components/places/jar.mn b/comm/suite/components/places/jar.mn
new file mode 100644
index 0000000000..e0bb80042b
--- /dev/null
+++ b/comm/suite/components/places/jar.mn
@@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+# Provide another URI for the bookmarkProperties dialog so we can persist the
+# attributes separately
+ content/communicator/places/bookmarkProperties2.xul (content/bookmarkProperties.xul)
+* content/communicator/places/places.xul (content/places.xul)
+ content/communicator/places/places.js (content/places.js)
+ content/communicator/places/places.css (content/places.css)
+ content/communicator/places/organizer.css (content/organizer.css)
+ content/communicator/places/bookmarkProperties.xul (content/bookmarkProperties.xul)
+ content/communicator/places/bookmarkProperties.js (content/bookmarkProperties.js)
+ content/communicator/places/placesOverlay.xul (content/placesOverlay.xul)
+ content/communicator/places/menu.xml (content/menu.xml)
+ content/communicator/places/tree.xml (content/tree.xml)
+ content/communicator/places/controller.js (content/controller.js)
+ content/communicator/places/treeView.js (content/treeView.js)
+ content/communicator/places/browserPlacesViews.js (content/browserPlacesViews.js)
+# keep the Places version of the history sidebar at history/history-panel.xul
+# to prevent having to worry about between versions of the browser
+* content/communicator/history/history-panel.xul (content/history-panel.xul)
+ content/communicator/places/history-panel.js (content/history-panel.js)
+# ditto for the bookmarks sidebar
+ content/communicator/bookmarks/bookmarksPanel.xul (content/bookmarksPanel.xul)
+ content/communicator/bookmarks/bookmarksPanel.js (content/bookmarksPanel.js)
+ content/communicator/bookmarks/sidebarUtils.js (content/sidebarUtils.js)
+ content/communicator/places/editBookmarkOverlay.xul (content/editBookmarkOverlay.xul)
+ content/communicator/places/editBookmarkOverlay.js (content/editBookmarkOverlay.js)
diff --git a/comm/suite/components/places/moz.build b/comm/suite/components/places/moz.build
new file mode 100644
index 0000000000..6c33011ed4
--- /dev/null
+++ b/comm/suite/components/places/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "tests/autocomplete/xpcshell.ini",
+ "tests/unit/xpcshell.ini",
+]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+EXTRA_COMPONENTS += [
+ "nsPlacesAutoComplete.js",
+ "nsPlacesAutoComplete.manifest",
+]
+
+EXTRA_JS_MODULES += [
+ "PlacesUIUtils.jsm",
+]
+
+
+with Files("**"):
+ BUG_COMPONENT = ("SeaMonkey", "Bookmarks & History")
diff --git a/comm/suite/components/places/nsPlacesAutoComplete.js b/comm/suite/components/places/nsPlacesAutoComplete.js
new file mode 100644
index 0000000000..89c6d66753
--- /dev/null
+++ b/comm/suite/components/places/nsPlacesAutoComplete.js
@@ -0,0 +1,1323 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+// This SQL query fragment provides the following:
+// - whether the entry is bookmarked (kQueryIndexBookmarked)
+// - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
+// - the tags associated with a bookmarked entry (kQueryIndexTags)
+const kBookTagSQLFragment =
+ `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,
+ (
+ SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL
+ ORDER BY lastModified DESC LIMIT 1
+ ) AS btitle,
+ (
+ SELECT GROUP_CONCAT(t.title, ',')
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent
+ WHERE b.fk = h.id
+ ) AS tags`;
+
+// observer topics
+const kTopicShutdown = "places-shutdown";
+const kPrefChanged = "nsPref:changed";
+
+// Match type constants. These indicate what type of search function we should
+// be using.
+const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
+const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
+const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
+const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
+
+// AutoComplete index constants. All AutoComplete queries will provide these
+// columns in this order.
+const kQueryIndexURL = 0;
+const kQueryIndexTitle = 1;
+const kQueryIndexBookmarked = 2;
+const kQueryIndexBookmarkTitle = 3;
+const kQueryIndexTags = 4;
+const kQueryIndexVisitCount = 5;
+const kQueryIndexTyped = 6;
+const kQueryIndexPlaceId = 7;
+const kQueryIndexQueryType = 8;
+const kQueryIndexOpenPageCount = 9;
+
+// AutoComplete query type constants. Describes the various types of queries
+// that we can process.
+const kQueryTypeKeyword = 0;
+const kQueryTypeFiltered = 1;
+
+// This separator is used as an RTL-friendly way to split the title and tags.
+// It can also be used by an nsIAutoCompleteResult consumer to re-split the
+// "comment" back into the title and the tag.
+const kTitleTagsSeparator = " \u2013 ";
+
+const kBrowserUrlbarBranch = "browser.urlbar.";
+// Toggle autocomplete.
+const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService",
+ "@mozilla.org/intl/texttosuburi;1",
+ "nsITextToSubURI");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helpers
+
+/**
+ * Initializes our temporary table on a given database.
+ *
+ * @param aDatabase
+ * The mozIStorageConnection to set up the temp table on.
+ */
+function initTempTable(aDatabase)
+{
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTables.h.
+ let stmt = aDatabase.createAsyncStatement(
+ `CREATE TEMP TABLE moz_openpages_temp (
+ url TEXT PRIMARY KEY
+ , open_count INTEGER
+ )`
+ );
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTriggers.h.
+ stmt = aDatabase.createAsyncStatement(
+ `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger
+ AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
+ WHEN NEW.open_count = 0
+ BEGIN
+ DELETE FROM moz_openpages_temp
+ WHERE url = NEW.url;
+ END`
+ );
+ stmt.executeAsync();
+ stmt.finalize();
+}
+
+/**
+ * Used to unescape encoded URI strings, and drop information that we do not
+ * care about for searching.
+ *
+ * @param aURIString
+ * The text to unescape and modify.
+ * @return the modified uri.
+ */
+function fixupSearchText(aURIString)
+{
+ let uri = stripPrefix(aURIString);
+ return gTextURIService.unEscapeURIForUI("UTF-8", uri);
+}
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param aURIString
+ * The text to modify.
+ * @return the modified uri.
+ */
+function stripPrefix(aURIString)
+{
+ let uri = aURIString;
+
+ if (uri.indexOf("http://") == 0) {
+ uri = uri.slice(7);
+ }
+ else if (uri.indexOf("https://") == 0) {
+ uri = uri.slice(8);
+ }
+ else if (uri.indexOf("ftp://") == 0) {
+ uri = uri.slice(6);
+ }
+
+ if (uri.indexOf("www.") == 0) {
+ uri = uri.slice(4);
+ }
+ return uri;
+}
+
+/**
+ * safePrefGetter get the pref with type safety.
+ * This will return the default value provided if no pref is set.
+ *
+ * @param aPrefBranch
+ * The nsIPrefBranch containing the required preference
+ * @param aName
+ * A preference name
+ * @param aDefault
+ * The preference's default value
+ * @return the preference value or provided default
+ */
+
+function safePrefGetter(aPrefBranch, aName, aDefault) {
+ let types = {
+ boolean: "Bool",
+ number: "Int",
+ string: "Char"
+ };
+ let type = types[typeof(aDefault)];
+ if (!type) {
+ throw "Unknown type!";
+ }
+
+ // If the pref isn't set, we want to use the default.
+ if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) {
+ return aDefault;
+ }
+ try {
+ return aPrefBranch["get" + type + "Pref"](aName);
+ }
+ catch (e) {
+ return aDefault;
+ }
+}
+
+/**
+ * Whether UnifiedComplete is alive.
+ */
+function isUnifiedCompleteInstantiated() {
+ try {
+ return Components.manager.QueryInterface(Ci.nsIServiceManager)
+ .isServiceInstantiated(Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"],
+ Ci.mozIPlacesAutoComplete);
+ } catch (ex) {
+ return false;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoCompleteStatementCallbackWrapper class
+
+/**
+ * Wraps a callback and ensures that handleCompletion is not dispatched if the
+ * query is no longer tracked.
+ *
+ * @param aAutocomplete
+ * A reference to a nsPlacesAutoComplete.
+ * @param aCallback
+ * A reference to a mozIStorageStatementCallback
+ * @param aDBConnection
+ * The database connection to execute the queries on.
+ */
+function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback,
+ aDBConnection)
+{
+ this._autocomplete = aAutocomplete;
+ this._callback = aCallback;
+ this._db = aDBConnection;
+}
+
+AutoCompleteStatementCallbackWrapper.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ handleResult: function ACSCW_handleResult(aResultSet)
+ {
+ this._callback.handleResult.apply(this._callback, arguments);
+ },
+
+ handleError: function ACSCW_handleError(aError)
+ {
+ this._callback.handleError.apply(this._callback, arguments);
+ },
+
+ handleCompletion: function ACSCW_handleCompletion(aReason)
+ {
+ // Only dispatch handleCompletion if we are not done searching and are a
+ // pending search.
+ if (!this._autocomplete.isSearchComplete() &&
+ this._autocomplete.isPendingSearch(this._handle)) {
+ this._callback.handleCompletion.apply(this._callback, arguments);
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// AutoCompleteStatementCallbackWrapper
+
+ /**
+ * Executes the specified query asynchronously. This object will notify
+ * this._callback if we should notify (logic explained in handleCompletion).
+ *
+ * @param aQueries
+ * The queries to execute asynchronously.
+ * @return a mozIStoragePendingStatement that can be used to cancel the
+ * queries.
+ */
+ executeAsync: function ACSCW_executeAsync(aQueries)
+ {
+ return this._handle = this._db.executeAsync(aQueries, aQueries.length,
+ this);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozIStorageStatementCallback,
+ ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsPlacesAutoComplete class
+//// @mozilla.org/autocomplete/search;1?name=history
+
+function nsPlacesAutoComplete()
+{
+ //////////////////////////////////////////////////////////////////////////////
+ //// Shared Constants for Smart Getters
+
+ // TODO bug 412736 in case of a frecency tie, break it with h.typed and
+ // h.visit_count which is better than nothing. This is slow, so not doing it
+ // yet...
+ function baseQuery(conditions = "") {
+ let query = `SELECT h.url, h.title, ${kBookTagSQLFragment},
+ h.visit_count, h.typed, h.id, :query_type,
+ t.open_count
+ FROM moz_places h
+ LEFT JOIN moz_openpages_temp t ON t.url = h.url
+ WHERE h.frecency <> 0
+ AND AUTOCOMPLETE_MATCH(:searchString, h.url,
+ IFNULL(btitle, h.title), tags,
+ h.visit_count, h.typed,
+ bookmarked, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT :maxResults`;
+ return query;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Smart Getters
+
+ XPCOMUtils.defineLazyGetter(this, "_db", function() {
+ // Get a cloned, read-only version of the database. We'll only ever write
+ // to our own in-memory temp table, and having a cloned copy means we do not
+ // run the risk of our queries taking longer due to the main database
+ // connection performing a long-running task.
+ let db = PlacesUtils.history.DBConnection.clone(true);
+
+ // Autocomplete often fallbacks to a table scan due to lack of text indices.
+ // In such cases a larger cache helps reducing IO. The default Storage
+ // value is MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp.
+ let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Create our in-memory tables for tab tracking.
+ initTempTable(db);
+
+ // Populate the table with current open pages cache contents.
+ if (this._openPagesCache.length > 0) {
+ // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter.
+ let stmt = this._registerOpenPageQuery =
+ db.createAsyncStatement(this._registerOpenPageQuerySQL);
+ let params = stmt.newBindingParamsArray();
+ for (let i = 0; i < this._openPagesCache.length; i++) {
+ let bp = params.newBindingParams();
+ bp.bindByName("page_url", this._openPagesCache[i]);
+ params.addParams(bp);
+ }
+ stmt.bindParameters(params);
+ stmt.executeAsync();
+ stmt.finalize();
+ delete this._openPagesCache;
+ }
+
+ return db;
+ });
+
+ this._customQuery = (conditions = "") => {
+ return this._db.createAsyncStatement(baseQuery(conditions));
+ };
+
+ XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() {
+ return this._db.createAsyncStatement(baseQuery());
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() {
+ // Enforce ignoring the visit_count index, since the frecency one is much
+ // faster in this case. ANALYZE helps the query planner to figure out the
+ // faster path, but it may not have run yet.
+ return this._db.createAsyncStatement(baseQuery("AND +h.visit_count > 0"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND bookmarked"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND tags IS NOT NULL"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() {
+ return this._db.createAsyncStatement(
+ `SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL,
+ :query_type, t.open_count, NULL
+ FROM moz_openpages_temp t
+ LEFT JOIN moz_places h ON h.url_hash = hash(t.url) AND h.url = t.url
+ WHERE h.id IS NULL
+ AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
+ NULL, NULL, NULL, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY t.ROWID DESC
+ LIMIT :maxResults`
+ );
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND h.typed = 1"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() {
+ return this._db.createAsyncStatement(
+ `/* do not warn (bug 487789) */
+ SELECT h.url, h.title, ${kBookTagSQLFragment},
+ h.visit_count, h.typed, h.id, :query_type, t.open_count
+ FROM (
+ SELECT ROUND(
+ MAX(use_count) * (1 + (input = :search_string)), 1
+ ) AS rank, place_id
+ FROM moz_inputhistory
+ WHERE input BETWEEN :search_string AND :search_string || X'FFFF'
+ GROUP BY place_id
+ ) AS i
+ JOIN moz_places h ON h.id = i.place_id
+ LEFT JOIN moz_openpages_temp t ON t.url = h.url
+ WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
+ IFNULL(btitle, h.title), tags,
+ h.visit_count, h.typed, bookmarked,
+ t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY rank DESC, h.frecency DESC`
+ );
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() {
+ return this._db.createAsyncStatement(
+ `/* do not warn (bug 487787) */
+ SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
+ 1, NULL, NULL, h.visit_count, h.typed, h.id,
+ :query_type, t.open_count
+ FROM moz_keywords k
+ JOIN moz_places h ON k.place_id = h.id
+ LEFT JOIN moz_openpages_temp t ON t.url = search_url
+ WHERE k.keyword = LOWER(:keyword)`
+ );
+ });
+
+ this._registerOpenPageQuerySQL =
+ `INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)
+ VALUES (:page_url,
+ IFNULL(
+ (
+ SELECT open_count + 1
+ FROM moz_openpages_temp
+ WHERE url = :page_url
+ ),
+ 1
+ )
+ )`;
+ XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() {
+ return this._db.createAsyncStatement(this._registerOpenPageQuerySQL);
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() {
+ return this._db.createAsyncStatement(
+ `UPDATE moz_openpages_temp
+ SET open_count = open_count - 1
+ WHERE url = :page_url`
+ );
+ });
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ // load preferences
+ this._prefs = Services.prefs.getBranch(kBrowserUrlbarBranch);
+ this._syncEnabledPref();
+ this._loadPrefs(true);
+
+ // register observers
+ Services.obs.addObserver(this, kTopicShutdown);
+}
+
+nsPlacesAutoComplete.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSearch
+
+ startSearch: function PAC_startSearch(aSearchString, aSearchParam,
+ aPreviousResult, aListener)
+ {
+ // Stop the search in case the controller has not taken care of it.
+ this.stopSearch();
+
+ // Note: We don't use aPreviousResult to make sure ordering of results are
+ // consistent. See bug 412730 for more details.
+
+ // We want to store the original string with no leading or trailing
+ // whitespace for case sensitive searches.
+ this._originalSearchString = aSearchString.trim();
+
+ this._currentSearchString =
+ fixupSearchText(this._originalSearchString.toLowerCase());
+
+ let params = new Set(aSearchParam.split(" "));
+ this._enableActions = params.has("enable-actions");
+ this._disablePrivateActions = params.has("disable-private-actions");
+
+ this._listener = aListener;
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
+ createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.setSearchString(aSearchString);
+ result.setListener(this);
+ this._result = result;
+
+ // If we are not enabled, we need to return now.
+ if (!this._enabled) {
+ this._finishSearch(true);
+ return;
+ }
+
+ // Reset our search behavior to the default.
+ if (this._currentSearchString) {
+ this._behavior = this._defaultBehavior;
+ }
+ else {
+ this._behavior = this._emptySearchDefaultBehavior;
+ }
+ // For any given search, we run up to four queries:
+ // 1) keywords (this._keywordQuery)
+ // 2) adaptive learning (this._adaptiveQuery)
+ // 3) open pages not supported by history (this._openPagesQuery)
+ // 4) query from this._getSearch
+ // (1) only gets ran if we get any filtered tokens from this._getSearch,
+ // since if there are no tokens, there is nothing to match, so there is no
+ // reason to run the query).
+ let {query, tokens} =
+ this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
+ let queries = tokens.length ?
+ [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery()] :
+ [this._getBoundAdaptiveQuery()];
+
+ if (this._hasBehavior("openpage")) {
+ queries.push(this._getBoundOpenPagesQuery(tokens));
+ }
+ queries.push(query);
+
+ // Start executing our queries.
+ this._executeQueries(queries);
+
+ // Set up our persistent state for the duration of the search.
+ this._searchTokens = tokens;
+ this._usedPlaces = {};
+ },
+
+ stopSearch: function PAC_stopSearch()
+ {
+ // We need to cancel our searches so we do not get any [more] results.
+ // However, it's possible we haven't actually started any searches, so this
+ // method may throw because this._pendingQuery may be undefined.
+ if (this._pendingQuery) {
+ this._stopActiveQuery();
+ }
+
+ this._finishSearch(false);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSimpleResultListener
+
+ onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
+ {
+ if (aRemoveFromDB) {
+ PlacesUtils.history.removePage(NetUtil.newURI(aURISpec));
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIPlacesAutoComplete
+
+ // If the connection has not yet been started, use this local cache. This
+ // prevents autocomplete from initing the database till the first search.
+ _openPagesCache: [],
+ registerOpenPage: function PAC_registerOpenPage(aURI)
+ {
+ if (!this._databaseInitialized) {
+ this._openPagesCache.push(aURI.spec);
+ return;
+ }
+
+ let stmt = this._registerOpenPageQuery;
+ stmt.params.page_url = aURI.spec;
+ stmt.executeAsync();
+ },
+
+ unregisterOpenPage: function PAC_unregisterOpenPage(aURI)
+ {
+ if (!this._databaseInitialized) {
+ let index = this._openPagesCache.indexOf(aURI.spec);
+ if (index != -1) {
+ this._openPagesCache.splice(index, 1);
+ }
+ return;
+ }
+
+ let stmt = this._unregisterOpenPageQuery;
+ stmt.params.page_url = aURI.spec;
+ stmt.executeAsync();
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ handleResult: function PAC_handleResult(aResultSet)
+ {
+ let row, haveMatches = false;
+ while ((row = aResultSet.getNextRow())) {
+ let match = this._processRow(row);
+ haveMatches = haveMatches || match;
+
+ if (this._result.matchCount == this._maxRichResults) {
+ // We have enough results, so stop running our search.
+ this._stopActiveQuery();
+
+ // And finish our search.
+ this._finishSearch(true);
+ return;
+ }
+
+ }
+
+ // Notify about results if we've gotten them.
+ if (haveMatches) {
+ this._notifyResults(true);
+ }
+ },
+
+ handleError: function PAC_handleError(aError)
+ {
+ Cu.reportError("Places AutoComplete: An async statement encountered an " +
+ "error: " + aError.result + ", '" + aError.message + "'");
+ },
+
+ handleCompletion: function PAC_handleCompletion(aReason)
+ {
+ // If we have already finished our search, we should bail out early.
+ if (this.isSearchComplete()) {
+ return;
+ }
+
+ // If we do not have enough results, and our match type is
+ // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
+ // results.
+ if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
+ this._result.matchCount < this._maxRichResults && !this._secondPass) {
+ this._secondPass = true;
+ let queries = [
+ this._getBoundAdaptiveQuery(MATCH_ANYWHERE),
+ this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens),
+ ];
+ this._executeQueries(queries);
+ return;
+ }
+
+ this._finishSearch(true);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function PAC_observe(aSubject, aTopic, aData)
+ {
+ if (aTopic == kTopicShutdown) {
+ Services.obs.removeObserver(this, kTopicShutdown);
+
+ // Remove our preference observer.
+ this._prefs.removeObserver("", this);
+ delete this._prefs;
+
+ // Finalize the statements that we have used.
+ let stmts = [
+ "_defaultQuery",
+ "_historyQuery",
+ "_bookmarkQuery",
+ "_tagsQuery",
+ "_openPagesQuery",
+ "_typedQuery",
+ "_adaptiveQuery",
+ "_keywordQuery",
+ "_registerOpenPageQuery",
+ "_unregisterOpenPageQuery",
+ ];
+ for (let i = 0; i < stmts.length; i++) {
+ // We do not want to create any query we haven't already created, so
+ // see if it is a getter first.
+ if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) {
+ this[stmts[i]].finalize();
+ }
+ }
+
+ if (this._databaseInitialized) {
+ this._db.asyncClose();
+ }
+ }
+ else if (aTopic == kPrefChanged) {
+ // Avoid re-entrancy when flipping linked preferences.
+ if (this._ignoreNotifications)
+ return;
+ this._ignoreNotifications = true;
+ this._loadPrefs(false, aTopic, aData);
+ this._ignoreNotifications = false;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsPlacesAutoComplete
+
+ get _databaseInitialized() {
+ return Object.getOwnPropertyDescriptor(this, "_db").value !== undefined;
+ },
+
+ /**
+ * Generates the tokens used in searching from a given string.
+ *
+ * @param aSearchString
+ * The string to generate tokens from.
+ * @return an array of tokens.
+ */
+ _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
+ {
+ // Calling split on an empty string will return an array containing one
+ // empty string. We don't want that, as it'll break our logic, so return an
+ // empty array then.
+ return aSearchString.length ? aSearchString.split(" ") : [];
+ },
+
+ /**
+ * Properly cleans up when searching is completed.
+ *
+ * @param aNotify
+ * Indicates if we should notify the AutoComplete listener about our
+ * results or not.
+ */
+ _finishSearch: function PAC_finishSearch(aNotify)
+ {
+ // Notify about results if we are supposed to.
+ if (aNotify) {
+ this._notifyResults(false);
+ }
+
+ // Clear our state
+ delete this._originalSearchString;
+ delete this._currentSearchString;
+ delete this._strippedPrefix;
+ delete this._searchTokens;
+ delete this._listener;
+ delete this._result;
+ delete this._usedPlaces;
+ delete this._pendingQuery;
+ this._secondPass = false;
+ this._enableActions = false;
+ },
+
+ /**
+ * Executes the given queries asynchronously.
+ *
+ * @param aQueries
+ * The queries to execute.
+ */
+ _executeQueries: function PAC_executeQueries(aQueries)
+ {
+ // Because we might get a handleCompletion for canceled queries, we want to
+ // filter out queries we no longer care about (described in the
+ // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).
+
+ // Create our wrapper object and execute the queries.
+ let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db);
+ this._pendingQuery = wrapper.executeAsync(aQueries);
+ },
+
+ /**
+ * Stops executing our active query.
+ */
+ _stopActiveQuery: function PAC_stopActiveQuery()
+ {
+ this._pendingQuery.cancel();
+ delete this._pendingQuery;
+ },
+
+ /**
+ * Notifies the listener about results.
+ *
+ * @param aSearchOngoing
+ * Indicates if the search is ongoing or not.
+ */
+ _notifyResults: function PAC_notifyResults(aSearchOngoing)
+ {
+ let result = this._result;
+ let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ if (aSearchOngoing) {
+ resultCode += "_ONGOING";
+ }
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ this._listener.onSearchResult(this, result);
+ },
+
+ /**
+ * Synchronize suggest.* prefs with autocomplete.enabled.
+ */
+ _syncEnabledPref: function PAC_syncEnabledPref()
+ {
+ let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
+ let types = ["History", "Bookmark", "Openpage"];
+
+ this._enabled = safePrefGetter(this._prefs, kBrowserUrlbarAutocompleteEnabledPref,
+ true);
+ this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
+ this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
+ this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);
+
+ if (this._enabled) {
+ // If the autocomplete preference is active, activate all suggest
+ // preferences only if all of them are false.
+ if (types.every(type => this["_suggest" + type] == false)) {
+ for (let type of suggestPrefs) {
+ this._prefs.setBoolPref(type, true);
+ }
+ }
+ } else {
+ // If the preference was deactivated, deactivate all suggest preferences.
+ for (let type of suggestPrefs) {
+ this._prefs.setBoolPref(type, false);
+ }
+ }
+ },
+
+ /**
+ * Loads the preferences that we care about.
+ *
+ * @param [optional] aRegisterObserver
+ * Indicates if the preference observer should be added or not. The
+ * default value is false.
+ * @param [optional] aTopic
+ * Observer's topic, if any.
+ * @param [optional] aSubject
+ * Observer's subject, if any.
+ */
+ _loadPrefs: function PAC_loadPrefs(aRegisterObserver, aTopic, aData)
+ {
+ // Avoid race conditions with UnifiedComplete component.
+ if (aData && !isUnifiedCompleteInstantiated()) {
+ // Synchronize suggest.* prefs with autocomplete.enabled.
+ if (aData == kBrowserUrlbarAutocompleteEnabledPref) {
+ this._syncEnabledPref();
+ } else if (aData.startsWith("suggest.")) {
+ let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
+ this._prefs.setBoolPref(kBrowserUrlbarAutocompleteEnabledPref,
+ suggestPrefs.some(pref => safePrefGetter(this._prefs, pref, true)));
+ }
+ }
+
+ this._enabled = safePrefGetter(this._prefs,
+ kBrowserUrlbarAutocompleteEnabledPref,
+ true);
+ this._matchBehavior = safePrefGetter(this._prefs,
+ "matchBehavior",
+ MATCH_BOUNDARY_ANYWHERE);
+ this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true);
+ this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25);
+ this._restrictHistoryToken = safePrefGetter(this._prefs,
+ "restrict.history", "^");
+ this._restrictBookmarkToken = safePrefGetter(this._prefs,
+ "restrict.bookmark", "*");
+ this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~");
+ this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+");
+ this._restrictOpenPageToken = safePrefGetter(this._prefs,
+ "restrict.openpage", "%");
+ this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#");
+ this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@");
+
+ this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
+ this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
+ this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);
+ this._suggestTyped = safePrefGetter(this._prefs, "suggest.history.onlyTyped", false);
+
+ // If history is not set, onlyTyped value should be ignored.
+ if (!this._suggestHistory) {
+ this._suggestTyped = false;
+ }
+ let types = ["History", "Bookmark", "Openpage", "Typed"];
+ this._defaultBehavior = types.reduce((memo, type) => {
+ let prefValue = this["_suggest" + type];
+ return memo | (prefValue &&
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
+ }, 0);
+
+ // Further restrictions to apply for "empty searches" (i.e. searches for "").
+ // The empty behavior is typed history, if history is enabled. Otherwise,
+ // it is bookmarks, if they are enabled. If both history and bookmarks are disabled,
+ // it defaults to open pages.
+ this._emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
+ if (this._suggestHistory) {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
+ } else if (this._suggestBookmark) {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+ } else {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
+ }
+
+ // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+ if (this._matchBehavior != MATCH_ANYWHERE &&
+ this._matchBehavior != MATCH_BOUNDARY &&
+ this._matchBehavior != MATCH_BEGINNING) {
+ this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+ }
+ // register observer
+ if (aRegisterObserver) {
+ this._prefs.addObserver("", this);
+ }
+ },
+
+ /**
+ * Given an array of tokens, this function determines which query should be
+ * ran. It also removes any special search tokens.
+ *
+ * @param aTokens
+ * An array of search tokens.
+ * @return an object with two properties:
+ * query: the correctly optimized, bound query to search the database
+ * with.
+ * tokens: the filtered list of tokens to search with.
+ */
+ _getSearch: function PAC_getSearch(aTokens)
+ {
+ let foundToken = false;
+ let restrict = (behavior) => {
+ if (!foundToken) {
+ this._behavior = 0;
+ this._setBehavior("restrict");
+ foundToken = true;
+ }
+ this._setBehavior(behavior);
+ };
+
+ // Set the proper behavior so our call to _getBoundSearchQuery gives us the
+ // correct query.
+ for (let i = aTokens.length - 1; i >= 0; i--) {
+ switch (aTokens[i]) {
+ case this._restrictHistoryToken:
+ restrict("history");
+ break;
+ case this._restrictBookmarkToken:
+ restrict("bookmark");
+ break;
+ case this._restrictTagToken:
+ restrict("tag");
+ break;
+ case this._restrictOpenPageToken:
+ if (!this._enableActions) {
+ continue;
+ }
+ restrict("openpage");
+ break;
+ case this._matchTitleToken:
+ restrict("title");
+ break;
+ case this._matchURLToken:
+ restrict("url");
+ break;
+ case this._restrictTypedToken:
+ restrict("typed");
+ break;
+ default:
+ // We do not want to remove the token if we did not match.
+ continue;
+ }
+
+ aTokens.splice(i, 1);
+ }
+
+ // Set the right JavaScript behavior based on our preference. Note that the
+ // preference is whether or not we should filter JavaScript, and the
+ // behavior is if we should search it or not.
+ if (!this._filterJavaScript) {
+ this._setBehavior("javascript");
+ }
+
+ return {
+ query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
+ tokens: aTokens
+ };
+ },
+
+ /**
+ * @return a string consisting of the search query to be used based on the
+ * previously set urlbar suggestion preferences.
+ */
+ _getSuggestionPrefQuery: function PAC_getSuggestionPrefQuery()
+ {
+ if (!this._hasBehavior("restrict") && this._hasBehavior("history") &&
+ this._hasBehavior("bookmark")) {
+ return this._hasBehavior("typed") ? this._customQuery("AND h.typed = 1")
+ : this._defaultQuery;
+ }
+ let conditions = [];
+ if (this._hasBehavior("history")) {
+ // Enforce ignoring the visit_count index, since the frecency one is much
+ // faster in this case. ANALYZE helps the query planner to figure out the
+ // faster path, but it may not have up-to-date information yet.
+ conditions.push("+h.visit_count > 0");
+ }
+ if (this._hasBehavior("typed")) {
+ conditions.push("h.typed = 1");
+ }
+ if (this._hasBehavior("bookmark")) {
+ conditions.push("bookmarked");
+ }
+ if (this._hasBehavior("tag")) {
+ conditions.push("tags NOTNULL");
+ }
+
+ return conditions.length ? this._customQuery("AND " + conditions.join(" AND "))
+ : this._defaultQuery;
+ },
+
+ /**
+ * Obtains the search query to be used based on the previously set search
+ * behaviors (accessed by this._hasBehavior). The query is bound and ready to
+ * execute.
+ *
+ * @param aMatchBehavior
+ * How this query should match its tokens to the search string.
+ * @param aTokens
+ * An array of search tokens.
+ * @return the correctly optimized query to search the database with and the
+ * new list of tokens to search with. The query has all the needed
+ * parameters bound, so consumers can execute it without doing any
+ * additional work.
+ */
+ _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
+ aTokens)
+ {
+ let query = this._getSuggestionPrefQuery();
+
+ // Bind the needed parameters to the query so consumers can use it.
+ let params = query.params;
+ params.parent = PlacesUtils.tagsFolderId;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = aMatchBehavior;
+ params.searchBehavior = this._behavior;
+
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ params.searchString = aTokens.join(" ");
+
+ // Limit the query to the the maximum number of desired results.
+ // This way we can avoid doing more work than needed.
+ params.maxResults = this._maxRichResults;
+
+ return query;
+ },
+
+ _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens)
+ {
+ let query = this._openPagesQuery;
+
+ // Bind the needed parameters to the query so consumers can use it.
+ let params = query.params;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = this._matchBehavior;
+ params.searchBehavior = this._behavior;
+
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ params.searchString = aTokens.join(" ");
+ params.maxResults = this._maxRichResults;
+
+ return query;
+ },
+
+ /**
+ * Obtains the keyword query with the properly bound parameters.
+ *
+ * @param aTokens
+ * The array of search tokens to check against.
+ * @return the bound keyword query.
+ */
+ _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
+ {
+ // The keyword is the first word in the search string, with the parameters
+ // following it.
+ let searchString = this._originalSearchString;
+ let queryString = "";
+ let queryIndex = searchString.indexOf(" ");
+ if (queryIndex != -1) {
+ queryString = searchString.substring(queryIndex + 1);
+ }
+ // We need to escape the parameters as if they were the query in a URL
+ queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
+
+ // The first word could be a keyword, so that's what we'll search.
+ let keyword = aTokens[0];
+
+ let query = this._keywordQuery;
+ let params = query.params;
+ params.keyword = keyword;
+ params.query_string = queryString;
+ params.query_type = kQueryTypeKeyword;
+
+ return query;
+ },
+
+ /**
+ * Obtains the adaptive query with the properly bound parameters.
+ *
+ * @return the bound adaptive query.
+ */
+ _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
+ {
+ // If we were not given a match behavior, use the stored match behavior.
+ if (arguments.length == 0) {
+ aMatchBehavior = this._matchBehavior;
+ }
+
+ let query = this._adaptiveQuery;
+ let params = query.params;
+ params.parent = PlacesUtils.tagsFolderId;
+ params.search_string = this._currentSearchString;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = aMatchBehavior;
+ params.searchBehavior = this._behavior;
+
+ return query;
+ },
+
+ /**
+ * Processes a mozIStorageRow to generate the proper data for the AutoComplete
+ * result. This will add an entry to the current result if it matches the
+ * criteria.
+ *
+ * @param aRow
+ * The row to process.
+ * @return true if the row is accepted, and false if not.
+ */
+ _processRow: function PAC_processRow(aRow)
+ {
+ // Before we do any work, make sure this entry isn't already in our results.
+ let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
+ let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
+ let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0;
+
+ // If actions are enabled and the page is open, add only the switch-to-tab
+ // result. Otherwise, add the normal result.
+ let [url, action] = this._enableActions && openPageCount > 0 && this._hasBehavior("openpage") ?
+ ["moz-action:switchtab," + escapedEntryURL, "action "] :
+ [escapedEntryURL, ""];
+
+ if (this._inResults(entryId, url)) {
+ return false;
+ }
+
+ let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
+ let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked);
+ let entryBookmarkTitle = entryBookmarked ?
+ aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
+ let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
+
+ // Always prefer the bookmark title unless it is empty
+ let title = entryBookmarkTitle || entryTitle;
+
+ let style;
+ if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
+ style = "keyword";
+ title = NetUtil.newURI(escapedEntryURL).host;
+ }
+
+ // We will always prefer to show tags if we have them.
+ let showTags = !!entryTags;
+
+ // However, we'll act as if a page is not bookmarked if the user wants
+ // only history and not bookmarks and there are no tags.
+ if (this._hasBehavior("history") && !this._hasBehavior("bookmark") &&
+ !showTags) {
+ showTags = false;
+ style = "favicon";
+ }
+
+ // If we have tags and should show them, we need to add them to the title.
+ if (showTags) {
+ title += kTitleTagsSeparator + entryTags;
+ }
+ // We have to determine the right style to display. Tags show the tag icon,
+ // bookmarks get the bookmark icon, and keywords get the keyword icon. If
+ // the result does not fall into any of those, it just gets the favicon.
+ if (!style) {
+ // It is possible that we already have a style set (from a keyword
+ // search or because of the user's preferences), so only set it if we
+ // haven't already done so.
+ if (showTags) {
+ style = "tag";
+ }
+ else if (entryBookmarked) {
+ style = "bookmark";
+ }
+ else {
+ style = "favicon";
+ }
+ }
+
+ this._addToResults(entryId, url, title, action + style);
+ return true;
+ },
+
+ /**
+ * Checks to see if the given place has already been added to the results.
+ *
+ * @param aPlaceId
+ * The place id to check for, may be null.
+ * @param aUrl
+ * The url to check for.
+ * @return true if the place has been added, false otherwise.
+ *
+ * @note Must check both the id and the url for a negative match, since
+ * autocomplete may run in the middle of a new page addition. In such
+ * a case the switch-to-tab query would hash the page by url, then a
+ * next query, running after the page addition, would hash it by id.
+ * It's not possible to just rely on url though, since keywords
+ * dynamically modify the url to include their search string.
+ */
+ _inResults: function PAC_inResults(aPlaceId, aUrl)
+ {
+ if (aPlaceId && aPlaceId in this._usedPlaces) {
+ return true;
+ }
+ return aUrl in this._usedPlaces;
+ },
+
+ /**
+ * Adds a result to the AutoComplete results. Also tracks that we've added
+ * this place_id into the result set.
+ *
+ * @param aPlaceId
+ * The place_id of the item to be added to the result set. This is
+ * used by _inResults.
+ * @param aURISpec
+ * The URI spec for the entry.
+ * @param aTitle
+ * The title to give the entry.
+ * @param aStyle
+ * Indicates how the entry should be styled when displayed.
+ */
+ _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
+ aStyle)
+ {
+ // Add this to our internal tracker to ensure duplicates do not end up in
+ // the result. _usedPlaces is an Object that is being used as a set.
+ // Not all entries have a place id, thus we fallback to the url for them.
+ // We cannot use only the url since keywords entries are modified to
+ // include the search string, and would be returned multiple times. Ids
+ // are faster too.
+ this._usedPlaces[aPlaceId || aURISpec] = true;
+
+ // Obtain the favicon for this URI.
+ let favicon = "page-icon:" + aURISpec;
+ this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
+ },
+
+ /**
+ * Determines if the specified AutoComplete behavior is set.
+ *
+ * @param aType
+ * The behavior type to test for.
+ * @return true if the behavior is set, false otherwise.
+ */
+ _hasBehavior: function PAC_hasBehavior(aType)
+ {
+ let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
+
+ if (this._disablePrivateActions &&
+ behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) {
+ return false;
+ }
+
+ return this._behavior & behavior;
+ },
+
+ /**
+ * Enables the desired AutoComplete behavior.
+ *
+ * @param aType
+ * The behavior type to set.
+ */
+ _setBehavior: function PAC_setBehavior(aType)
+ {
+ this._behavior |=
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
+ },
+
+ /**
+ * Determines if we are done searching or not.
+ *
+ * @return true if we have completed searching, false otherwise.
+ */
+ isSearchComplete: function PAC_isSearchComplete()
+ {
+ // If _pendingQuery is null, we should no longer do any work since we have
+ // already called _finishSearch. This means we completed our search.
+ return this._pendingQuery == null;
+ },
+
+ /**
+ * Determines if the given handle of a pending statement is a pending search
+ * or not.
+ *
+ * @param aHandle
+ * A mozIStoragePendingStatement to check and see if we are waiting for
+ * results from it still.
+ * @return true if it is a pending query, false otherwise.
+ */
+ isPendingSearch: function PAC_isPendingSearch(aHandle)
+ {
+ return this._pendingQuery == aHandle;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSimpleResultListener,
+ Ci.mozIPlacesAutoComplete,
+ Ci.mozIStorageStatementCallback,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ])
+};
+
+var components = [nsPlacesAutoComplete];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/comm/suite/components/places/nsPlacesAutoComplete.manifest b/comm/suite/components/places/nsPlacesAutoComplete.manifest
new file mode 100644
index 0000000000..77dc732af2
--- /dev/null
+++ b/comm/suite/components/places/nsPlacesAutoComplete.manifest
@@ -0,0 +1,3 @@
+component {d0272978-beab-4adc-a3d4-04b76acfa4e7} nsPlacesAutoComplete.js
+contract @mozilla.org/autocomplete/search;1?name=history {d0272978-beab-4adc-a3d4-04b76acfa4e7}
+
diff --git a/comm/suite/components/places/tests/autocomplete/head_autocomplete.js b/comm/suite/components/places/tests/autocomplete/head_autocomplete.js
new file mode 100644
index 0000000000..bccc7f56d2
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/head_autocomplete.js
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+{
+ let commonFile = do_get_file("../head_common.js", false);
+ let uri = Services.io.newFileURI(commonFile);
+ Services.scriptloader.loadSubScript(uri.spec, this);
+}
+
+// Put any other stuff relative to this test folder below.
+
+
+/**
+ * Header file for autocomplete testcases that create a set of pages with uris,
+ * titles, tags and tests that a given search term matches certain pages.
+ */
+
+var current_test = 0;
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ timeout: 10,
+ textValue: "",
+ searches: null,
+ searchParam: "",
+ popupOpen: false,
+ minResultsForPopup: 0,
+ invalidate: function() {},
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+ get popup() { return this; },
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+ setSelectedIndex: function() {},
+ get searchCount() { return this.searches.length; },
+ getSearchAt: function(aIndex) { return this.searches[aIndex]; },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ ])
+};
+
+function toURI(aSpec) {
+ return uri(aSpec);
+}
+
+var appendTags = true;
+// Helper to turn off tag matching in results
+function ignoreTags()
+{
+ print("Ignoring tags from results");
+ appendTags = false;
+}
+
+function ensure_results(aSearch, aExpected)
+{
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ let input = new AutoCompleteInput(["history"]);
+
+ controller.input = input;
+
+ if (typeof kSearchParam == "string")
+ input.searchParam = kSearchParam;
+
+ let numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ Assert.equal(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ Assert.equal(numSearchesStarted, 1);
+ aExpected = aExpected.slice();
+
+ // Check to see the expected uris and titles match up (in any order)
+ for (let i = 0; i < controller.matchCount; i++) {
+ let value = controller.getValueAt(i);
+ let comment = controller.getCommentAt(i);
+
+ print("Looking for '" + value + "', '" + comment + "' in expected results...");
+ let j;
+ for (j = 0; j < aExpected.length; j++) {
+ // Skip processed expected results
+ if (aExpected[j] == undefined)
+ continue;
+
+ let [uri, title, tags] = gPages[aExpected[j]];
+
+ // Load the real uri and titles and tags if necessary
+ uri = toURI(kURIs[uri]).spec;
+ title = kTitles[title];
+ if (tags && appendTags)
+ title += " \u2013 " + tags.map(aTag => kTitles[aTag]);
+ print("Checking against expected '" + uri + "', '" + title + "'...");
+
+ // Got a match on both uri and title?
+ if (uri == value && title == comment) {
+ print("Got it at index " + j + "!!");
+ // Make it undefined so we don't process it again
+ aExpected[j] = undefined;
+ break;
+ }
+ }
+
+ // We didn't hit the break, so we must have not found it
+ if (j == aExpected.length)
+ do_throw("Didn't find the current result ('" + value + "', '" + comment + "') in expected: " + aExpected);
+ }
+
+ // Make sure we have the right number of results
+ print("Expecting " + aExpected.length + " results; got " +
+ controller.matchCount + " results");
+ Assert.equal(controller.matchCount, aExpected.length);
+
+ // If we expect results, make sure we got matches
+ Assert.equal(controller.searchStatus, aExpected.length ?
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+
+ // Fetch the next test if we have more
+ if (++current_test < gTests.length)
+ run_test();
+
+ do_test_finished();
+ };
+
+ print("Searching for.. '" + aSearch + "'");
+ controller.startSearch(aSearch);
+}
+
+// Get history services
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var tagsvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+// Some date not too long ago
+var gDate = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+// Store the page info for each uri
+var gPages = [];
+
+// Initialization tasks to be run before the next test
+var gNextTestSetupTasks = [];
+
+/**
+ * Adds a page, and creates various properties for it depending on the
+ * parameters passed in. This function will also add one visit, unless
+ * aNoVisit is true.
+ *
+ * @param aURI
+ * An index into kURIs that holds the string for the URI we are to add a
+ * page for.
+ * @param aTitle
+ * An index into kTitles that holds the string for the title we are to
+ * associate with the specified URI.
+ * @param aBook [optional]
+ * An index into kTitles that holds the string for the title we are to
+ * associate with the bookmark. If this is undefined, no bookmark is
+ * created.
+ * @param aTags [optional]
+ * An array of indexes into kTitles that hold the strings for the tags we
+ * are to associate with the URI. If this is undefined (or aBook is), no
+ * tags are added.
+ * @param aKey [optional]
+ * A string to associate as the keyword for this bookmark. aBook must be
+ * a valid index into kTitles for this to be checked and used.
+ * @param aTransitionType [optional]
+ * The transition type to use when adding the visit. The default is
+ * nsINavHistoryService::TRANSITION_LINK.
+ * @param aNoVisit [optional]
+ * If true, no visit is added for the URI. If false or undefined, a
+ * visit is added.
+ */
+function addPageBook(aURI, aTitle, aBook, aTags, aKey, aTransitionType, aNoVisit)
+{
+ gNextTestSetupTasks.push([task_addPageBook, arguments]);
+}
+
+async function task_addPageBook(aURI, aTitle, aBook, aTags, aKey, aTransitionType, aNoVisit)
+{
+ // Add a page entry for the current uri
+ gPages[aURI] = [aURI, aBook != undefined ? aBook : aTitle, aTags];
+
+ let uri = toURI(kURIs[aURI]);
+ let title = kTitles[aTitle];
+
+ let out = [aURI, aTitle, aBook, aTags, aKey];
+ out.push("\nuri=" + kURIs[aURI]);
+ out.push("\ntitle=" + title);
+
+ // Add the page and a visit if we need to
+ if (!aNoVisit) {
+ await PlacesTestUtils.addVisits({
+ uri: uri,
+ transition: aTransitionType || TRANSITION_LINK,
+ visitDate: gDate,
+ title: title
+ });
+ out.push("\nwith visit");
+ }
+
+ // Add a bookmark if we need to
+ if (aBook != undefined) {
+ let book = kTitles[aBook];
+ let bmid = bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, uri,
+ bmsvc.DEFAULT_INDEX, book);
+ out.push("\nbook=" + book);
+
+ // Add a keyword to the bookmark if we need to
+ if (aKey != undefined)
+ await PlacesUtils.keywords.insert({url: uri.spec, keyword: aKey});
+
+ // Add tags if we need to
+ if (aTags != undefined && aTags.length > 0) {
+ // Convert each tag index into the title
+ let tags = aTags.map(aTag => kTitles[aTag]);
+ tagsvc.tagURI(uri, tags);
+ out.push("\ntags=" + tags);
+ }
+ }
+
+ print("\nAdding page/book/tag: " + out.join(", "));
+}
+
+function run_test() {
+ print("\n");
+ // always search in history + bookmarks, no matter what the default is
+ prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ prefs.setBoolPref("browser.urlbar.suggest.openpage", true);
+ prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ // Load the test and print a description then run the test
+ let [description, search, expected, func] = gTests[current_test];
+ print(description);
+
+ // By default assume we want to match tags
+ appendTags = true;
+
+ // Do an extra function if necessary
+ if (func)
+ func();
+
+ (async function() {
+ // Iterate over all tasks and execute them
+ for (let [fn, args] of gNextTestSetupTasks) {
+ await fn.apply(this, args);
+ }
+
+ // Clean up to allow tests to register more functions.
+ gNextTestSetupTasks = [];
+
+ // At this point frecency could still be updating due to latest pages
+ // updates. This is not a problem in real life, but autocomplete tests
+ // should return reliable resultsets, thus we have to wait.
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ })().then(() => ensure_results(search, expected),
+ do_report_unexpected_exception);
+}
+
+// Utility function to remove history pages
+function removePages(aURIs)
+{
+ gNextTestSetupTasks.push([do_removePages, arguments]);
+}
+
+function do_removePages(aURIs)
+{
+ for (let uri of aURIs)
+ histsvc.removePage(toURI(kURIs[uri]));
+}
+
+// Utility function to mark pages as typed
+function markTyped(aURIs, aTitle)
+{
+ gNextTestSetupTasks.push([task_markTyped, arguments]);
+}
+
+async function task_markTyped(aURIs, aTitle)
+{
+ for (let uri of aURIs) {
+ await PlacesTestUtils.addVisits({
+ uri: toURI(kURIs[uri]),
+ transition: TRANSITION_TYPED,
+ title: kTitles[aTitle]
+ });
+ }
+}
diff --git a/comm/suite/components/places/tests/autocomplete/test_416211.js b/comm/suite/components/places/tests/autocomplete/test_416211.js
new file mode 100644
index 0000000000..8f662b5b13
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_416211.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test bug 416211 to make sure results that match the tag show the bookmark
+ * title instead of the page title.
+ */
+
+var theTag = "superTag";
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://theuri/",
+];
+var kTitles = [
+ "Page title",
+ "Bookmark title",
+ theTag,
+];
+
+// Add page with a title, bookmark, and [tags]
+addPageBook(0, 0, 1, [2]);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Make sure the tag match gives the bookmark title",
+ theTag, [0]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_416214.js b/comm/suite/components/places/tests/autocomplete/test_416214.js
new file mode 100644
index 0000000000..f891180069
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_416214.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test autocomplete for non-English URLs that match the tag bug 416214. Also
+ * test bug 417441 by making sure escaped ascii characters like "+" remain
+ * escaped.
+ *
+ * - add a visit for a page with a non-English URL
+ * - add a tag for the page
+ * - search for the tag
+ * - test number of matches (should be exactly one)
+ * - make sure the url is decoded
+ */
+
+var theTag = "superTag";
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://escaped/ユニコード",
+ "http://asciiescaped/blocking-firefox3%2B",
+];
+var kTitles = [
+ "title",
+ theTag,
+];
+
+// Add pages that match the tag
+addPageBook(0, 0, 0, [1]);
+addPageBook(1, 0, 0, [1]);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Make sure tag matches return the right url as well as '+' remain escaped",
+ theTag, [0, 1]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_417798.js b/comm/suite/components/places/tests/autocomplete/test_417798.js
new file mode 100644
index 0000000000..6306c4125c
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_417798.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 417798 to make sure javascript: URIs don't show up unless the
+ * user searches for javascript: explicitly.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://abc/def",
+ "javascript:5",
+];
+var kTitles = [
+ "Title with javascript:",
+];
+
+addPageBook(0, 0); // regular url
+// javascript: uri as bookmark (no visit)
+addPageBook(1, 0, 0, undefined, undefined, undefined, true);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Match non-javascript: with plain search",
+ "a", [0]],
+ ["1: Match non-javascript: with almost javascript:",
+ "javascript", [0]],
+ ["2: Match javascript:",
+ "javascript:", [0, 1]],
+ ["3: Match nothing with non-first javascript:",
+ "5 javascript:", []],
+ ["4: Match javascript: with multi-word search",
+ "javascript: 5", [1]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_418257.js b/comm/suite/components/places/tests/autocomplete/test_418257.js
new file mode 100644
index 0000000000..edff19d8b0
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_418257.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 418257 by making sure tags are returned with the title as part of
+ * the "comment" if there are tags even if we didn't match in the tags. They
+ * are separated from the title by a endash.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://page1",
+ "http://page2",
+ "http://page3",
+ "http://page4",
+];
+var kTitles = [
+ "tag1",
+ "tag2",
+ "tag3",
+];
+
+// Add pages with varying number of tags
+addPageBook(0, 0, 0, [0]);
+addPageBook(1, 0, 0, [0, 1]);
+addPageBook(2, 0, 0, [0, 2]);
+addPageBook(3, 0, 0, [0, 1, 2]);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Make sure tags come back in the title when matching tags",
+ "page1 tag", [0]],
+ ["1: Check tags in title for page2",
+ "page2 tag", [1]],
+ ["2: Make sure tags appear even when not matching the tag",
+ "page3", [2]],
+ ["3: Multiple tags come in commas for page4",
+ "page4", [3]],
+ ["4: Extra test just to make sure we match the title",
+ "tag2", [1, 3]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_422277.js b/comm/suite/components/places/tests/autocomplete/test_422277.js
new file mode 100644
index 0000000000..d6eb193dc8
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_422277.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 422277 to make sure bad escaped uris don't get escaped. This makes
+ * sure we don't hit an assertion for "not a UTF8 string".
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://site/%EAid",
+];
+var kTitles = [
+ "title",
+];
+
+addPageBook(0, 0);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Bad escaped uri stays escaped",
+ "site", [0]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js b/comm/suite/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js
new file mode 100644
index 0000000000..5969fc9f5b
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Need to test that removing a page from autocomplete actually removes a page
+ * Description From Shawn Wilsher :sdwilsh 2009-02-18 11:29:06 PST
+ * We don't test the code path of onValueRemoved
+ * for the autocomplete implementation
+ * Bug 479089
+ */
+
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+getService(Ci.nsINavHistoryService);
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(async function test_autocomplete_on_value_removed()
+{
+ // QI to nsIAutoCompleteSimpleResultListener
+ var listener = Cc["@mozilla.org/autocomplete/search;1?name=history"].
+ getService(Ci.nsIAutoCompleteSimpleResultListener);
+
+ // add history visit
+ var testUri = uri("http://foo.mozilla.com/");
+ await PlacesTestUtils.addVisits({
+ uri: testUri,
+ referrer: uri("http://mozilla.com/")
+ });
+ // create a query object
+ var query = histsvc.getNewQuery();
+ // create the options object we will never use
+ var options = histsvc.getNewQueryOptions();
+ // look for this uri only
+ query.uri = testUri;
+ // execute
+ var queryRes = histsvc.executeQuery(query, options);
+ // open the result container
+ queryRes.root.containerOpen = true;
+ // debug queries
+ // dump_table("moz_places");
+ Assert.equal(queryRes.root.childCount, 1);
+ // call the untested code path
+ listener.onValueRemoved(null, testUri.spec, true);
+ // make sure it is GONE from the DB
+ Assert.equal(queryRes.root.childCount, 0);
+ // close the container
+ queryRes.root.containerOpen = false;
+});
diff --git a/comm/suite/components/places/tests/autocomplete/test_download_embed_bookmarks.js b/comm/suite/components/places/tests/autocomplete/test_download_embed_bookmarks.js
new file mode 100644
index 0000000000..65bec60cc7
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_download_embed_bookmarks.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests bug 449406 to ensure that TRANSITION_DOWNLOAD, TRANSITION_EMBED and
+ * TRANSITION_FRAMED_LINK bookmarked uri's show up in the location bar.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://download/bookmarked",
+ "http://embed/bookmarked",
+ "http://framed/bookmarked",
+ "http://download",
+ "http://embed",
+ "http://framed",
+];
+var kTitles = [
+ "download-bookmark",
+ "embed-bookmark",
+ "framed-bookmark",
+ "download2",
+ "embed2",
+ "framed2",
+];
+
+// Add download and embed uris
+addPageBook(0, 0, 0, undefined, undefined, TRANSITION_DOWNLOAD);
+addPageBook(1, 1, 1, undefined, undefined, TRANSITION_EMBED);
+addPageBook(2, 2, 2, undefined, undefined, TRANSITION_FRAMED_LINK);
+addPageBook(3, 3, undefined, undefined, undefined, TRANSITION_DOWNLOAD);
+addPageBook(4, 4, undefined, undefined, undefined, TRANSITION_EMBED);
+addPageBook(5, 5, undefined, undefined, undefined, TRANSITION_FRAMED_LINK);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Searching for bookmarked download uri matches",
+ kTitles[0], [0]],
+ ["1: Searching for bookmarked embed uri matches",
+ kTitles[1], [1]],
+ ["2: Searching for bookmarked framed uri matches",
+ kTitles[2], [2]],
+ ["3: Searching for download uri does not match",
+ kTitles[3], []],
+ ["4: Searching for embed uri does not match",
+ kTitles[4], []],
+ ["5: Searching for framed uri does not match",
+ kTitles[5], []],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_empty_search.js b/comm/suite/components/places/tests/autocomplete/test_empty_search.js
new file mode 100644
index 0000000000..df8eac383a
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_empty_search.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 426864 that makes sure the empty search (drop down list) only
+ * shows typed pages from history.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://foo/0",
+ "http://foo/1",
+ "http://foo/2",
+ "http://foo/3",
+ "http://foo/4",
+ "http://foo/5",
+];
+var kTitles = [
+ "title",
+];
+
+// Visited (in history)
+addPageBook(0, 0); // history
+addPageBook(1, 0, 0); // bookmark
+addPageBook(2, 0); // history typed
+addPageBook(3, 0, 0); // bookmark typed
+
+// Unvisited bookmark
+addPageBook(4, 0, 0); // bookmark
+addPageBook(5, 0, 0); // bookmark typed
+
+// Set some pages as typed
+markTyped([2, 3, 5], 0);
+// Remove pages from history to treat them as unvisited
+removePages([4, 5]);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Match everything",
+ "foo", [0, 1, 2, 3, 4, 5]],
+ ["1: Match only typed history",
+ "foo ^ ~", [2, 3]],
+ ["2: Drop-down empty search matches only typed history",
+ "", [2, 3]],
+ ["3: Drop-down empty search matches only bookmarks",
+ "", [2, 3], matchBookmarks],
+ ["4: Drop-down empty search matches only typed",
+ "", [2, 3], matchTyped],
+];
+
+function matchBookmarks() {
+ prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ clearPrefs();
+}
+
+function matchTyped() {
+ prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
+ clearPrefs();
+}
+
+function clearPrefs() {
+ prefs.clearUserPref("browser.urlbar.suggest.history");
+ prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ prefs.clearUserPref("browser.urlbar.suggest.history.onlyTyped");
+}
diff --git a/comm/suite/components/places/tests/autocomplete/test_enabled.js b/comm/suite/components/places/tests/autocomplete/test_enabled.js
new file mode 100644
index 0000000000..a12242763f
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_enabled.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 471903 to make sure searching in autocomplete can be turned on
+ * and off. Also test bug 463535 for pref changing search.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://url/0",
+];
+var kTitles = [
+ "title",
+];
+
+addPageBook(0, 0); // visited page
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["1: plain search",
+ "url", [0]],
+ ["2: search disabled",
+ "url", [], () => setSearch(0)],
+ ["3: resume normal search",
+ "url", [0], () => setSearch(1)],
+];
+
+function setSearch(aSearch) {
+ prefs.setBoolPref("browser.urlbar.autocomplete.enabled", !!aSearch);
+}
+
+add_task(async function test_sync_enabled() {
+ // Initialize autocomplete component.
+ Cc["@mozilla.org/autocomplete/search;1?name=history"]
+ .getService(Ci.mozIPlacesAutoComplete);
+
+ let types = [ "history", "bookmark", "openpage" ];
+
+ // Test the service keeps browser.urlbar.autocomplete.enabled synchronized
+ // with browser.urlbar.suggest prefs.
+ for (let type of types) {
+ Services.prefs.setBoolPref("browser.urlbar.suggest." + type, true);
+ }
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true);
+
+ // Disable autocomplete and check all the suggest prefs are set to false.
+ Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
+ for (let type of types) {
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false);
+ }
+
+ // Setting even a single suggest pref to true should enable autocomplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ for (let type of types.filter(t => t != "history")) {
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false);
+ }
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true);
+
+ // Disable autocoplete again, then re-enable it and check suggest prefs
+ // have been reset.
+ Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
+ Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true);
+ for (let type of types.filter(t => t != "history")) {
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+ }
+});
diff --git a/comm/suite/components/places/tests/autocomplete/test_escape_self.js b/comm/suite/components/places/tests/autocomplete/test_escape_self.js
new file mode 100644
index 0000000000..0b0918b8ff
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_escape_self.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 422698 to make sure searches with urls from the location bar
+ * correctly match itself when it contains escaped characters.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://unescapeduri/",
+ "http://escapeduri/%40/",
+];
+var kTitles = [
+ "title",
+];
+
+// Add unescaped and escaped uris
+addPageBook(0, 0);
+addPageBook(1, 0);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Unescaped location matches itself",
+ kURIs[0], [0]],
+ ["1: Escaped location matches itself",
+ kURIs[1], [1]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_ignore_protocol.js b/comm/suite/components/places/tests/autocomplete/test_ignore_protocol.js
new file mode 100644
index 0000000000..2ad63b735f
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_ignore_protocol.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 424509 to make sure searching for "h" doesn't match "http" of urls.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://site/",
+ "http://happytimes/",
+];
+var kTitles = [
+ "title",
+];
+
+// Add site without "h" and with "h"
+addPageBook(0, 0);
+addPageBook(1, 0);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Searching for h matches site and not http://",
+ "h", [1]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_keyword_search.js b/comm/suite/components/places/tests/autocomplete/test_keyword_search.js
new file mode 100644
index 0000000000..796f386bb8
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_keyword_search.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 392143 that puts keyword results into the autocomplete. Makes
+ * sure that multiple parameter queries get spaces converted to +, + converted
+ * to %2B, non-ascii become escaped, and pages in history that match the
+ * keyword uses the page's title.
+ *
+ * Also test for bug 249468 by making sure multiple keyword bookmarks with the
+ * same keyword appear in the list.
+ */
+
+// Details for the keyword bookmark
+var keyBase = "http://abc/?search=";
+var keyKey = "key";
+
+// A second keyword bookmark with the same keyword
+var otherBase = "http://xyz/?foo=";
+
+var unescaped = "ユニコード";
+var pageInHistory = "ThisPageIsInHistory";
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ keyBase + "%s",
+ keyBase + "term",
+ keyBase + "multi+word",
+ keyBase + "blocking%2B",
+ keyBase + unescaped,
+ keyBase + pageInHistory,
+ keyBase,
+ otherBase + "%s",
+ keyBase + "twoKey",
+ otherBase + "twoKey"
+];
+var kTitles = [
+ "Generic page title",
+ "Keyword title",
+ "abc",
+ "xyz"
+];
+
+// Add the keyword bookmark
+addPageBook(0, 0, 1, [], keyKey);
+// Add in the "fake pages" for keyword searches
+gPages[1] = [1, 2];
+gPages[2] = [2, 2];
+gPages[3] = [3, 2];
+gPages[4] = [4, 2];
+// Add a page into history
+addPageBook(5, 2);
+gPages[6] = [6, 2];
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Plain keyword query",
+ keyKey + " term", [1]],
+ ["1: Multi-word keyword query",
+ keyKey + " multi word", [2]],
+ ["2: Keyword query with +",
+ keyKey + " blocking+", [3]],
+ ["3: Unescaped term in query",
+ keyKey + " " + unescaped, [4]],
+ ["4: Keyword that happens to match a page",
+ keyKey + " " + pageInHistory, [5]],
+ ["5: Keyword without query (without space)",
+ keyKey, [6]],
+ ["6: Keyword without query (with space)",
+ keyKey + " ", [6]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_match_beginning.js b/comm/suite/components/places/tests/autocomplete/test_match_beginning.js
new file mode 100644
index 0000000000..b9ba3ab39b
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_match_beginning.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 451760 which allows matching only at the beginning of urls or
+ * titles to simulate Firefox 2 functionality.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://x.com/y",
+ "https://y.com/x",
+];
+var kTitles = [
+ "a b",
+ "b a",
+];
+
+addPageBook(0, 0);
+addPageBook(1, 1);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ // Tests after this one will match at the beginning
+ ["0: Match at the beginning of titles",
+ "a", [0],
+ () => setBehavior(3)],
+ ["1: Match at the beginning of titles",
+ "b", [1]],
+ ["2: Match at the beginning of urls",
+ "x", [0]],
+ ["3: Match at the beginning of urls",
+ "y", [1]],
+
+ // Tests after this one will match against word boundaries and anywhere
+ ["4: Sanity check that matching anywhere finds more",
+ "a", [0, 1],
+ () => setBehavior(1)],
+];
+
+function setBehavior(aType) {
+ prefs.setIntPref("browser.urlbar.matchBehavior", aType);
+}
diff --git a/comm/suite/components/places/tests/autocomplete/test_multi_word_search.js b/comm/suite/components/places/tests/autocomplete/test_multi_word_search.js
new file mode 100644
index 0000000000..c0b896c868
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_multi_word_search.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 401869 to allow multiple words separated by spaces to match in
+ * the page title, page url, or bookmark title to be considered a match. All
+ * terms must match but not all terms need to be in the title, etc.
+ *
+ * Test bug 424216 by making sure bookmark titles are always shown if one is
+ * available. Also bug 425056 makes sure matches aren't found partially in the
+ * page title and partially in the bookmark.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://a.b.c/d-e_f/h/t/p",
+ "http://d.e.f/g-h_i/h/t/p",
+ "http://g.h.i/j-k_l/h/t/p",
+ "http://j.k.l/m-n_o/h/t/p",
+];
+var kTitles = [
+ "f(o)o b<a>r",
+ "b(a)r b<a>z",
+];
+
+// Regular pages
+addPageBook(0, 0);
+addPageBook(1, 1);
+// Bookmarked pages
+addPageBook(2, 0, 0);
+addPageBook(3, 0, 1);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: Match 2 terms all in url",
+ "c d", [0]],
+ ["1: Match 1 term in url and 1 term in title",
+ "b e", [0, 1]],
+ ["2: Match 3 terms all in title; display bookmark title if matched",
+ "b a z", [1, 3]],
+ ["3: Match 2 terms in url and 1 in title; make sure bookmark title is used for search",
+ "k f t", [2]],
+ ["4: Match 3 terms in url and 1 in title",
+ "d i g z", [1]],
+ ["5: Match nothing",
+ "m o z i", []],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_special_search.js b/comm/suite/components/places/tests/autocomplete/test_special_search.js
new file mode 100644
index 0000000000..78bf5a7d64
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_special_search.js
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 395161 that allows special searches that restrict results to
+ * history/bookmark/tagged items and title/url matches.
+ *
+ * Test 485122 by making sure results don't have tags when restricting result
+ * to just history either by default behavior or dynamic query restrict.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://url/",
+ "http://url/2",
+ "http://foo.bar/",
+ "http://foo.bar/2",
+ "http://url/star",
+ "http://url/star/2",
+ "http://foo.bar/star",
+ "http://foo.bar/star/2",
+ "http://url/tag",
+ "http://url/tag/2",
+ "http://foo.bar/tag",
+ "http://foo.bar/tag/2",
+];
+var kTitles = [
+ "title",
+ "foo.bar",
+];
+
+// Plain page visits
+addPageBook(0, 0); // plain page
+addPageBook(1, 1); // title
+addPageBook(2, 0); // url
+addPageBook(3, 1); // title and url
+
+// Bookmarked pages (no tag)
+addPageBook(4, 0, 0); // bookmarked page
+addPageBook(5, 1, 1); // title
+addPageBook(6, 0, 0); // url
+addPageBook(7, 1, 1); // title and url
+
+// Tagged pages
+addPageBook(8, 0, 0, [1]); // tagged page
+addPageBook(9, 1, 1, [1]); // title
+addPageBook(10, 0, 0, [1]); // url
+addPageBook(11, 1, 1, [1]); // title and url
+
+// Remove pages from history to treat them as unvisited, so pages that do have
+// visits are 0,1,2,3,5,10
+removePages([4, 6, 7, 8, 9, 11]);
+// Set some pages as typed
+markTyped([0, 10], 0);
+markTyped([3], 1);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ // Test restricting searches
+ ["0: History restrict",
+ "^", [0, 1, 2, 3, 5, 10]],
+ ["1: Star restrict",
+ "*", [4, 5, 6, 7, 8, 9, 10, 11]],
+ ["2: Tag restrict",
+ "+", [8, 9, 10, 11]],
+
+ // Test specials as any word position
+ ["3: Special as first word",
+ "^ foo bar", [1, 2, 3, 5, 10]],
+ ["4: Special as middle word",
+ "foo ^ bar", [1, 2, 3, 5, 10]],
+ ["5: Special as last word",
+ "foo bar ^", [1, 2, 3, 5, 10]],
+
+ // Test restricting and matching searches with a term
+ ["6.1: foo ^ -> history",
+ "foo ^", [1, 2, 3, 5, 10]],
+ ["6.2: foo | -> history (change pref)",
+ "foo |", [1, 2, 3, 5, 10], () => changeRestrict("history", "|")],
+ ["7.1: foo * -> is star",
+ "foo *", [5, 6, 7, 8, 9, 10, 11], () => resetRestrict("history")],
+ ["7.2: foo | -> is star (change pref)",
+ "foo |", [5, 6, 7, 8, 9, 10, 11], () => changeRestrict("bookmark", "|")],
+ ["8.1: foo # -> in title",
+ "foo #", [1, 3, 5, 7, 8, 9, 10, 11], () => resetRestrict("bookmark")],
+ ["8.2: foo | -> in title (change pref)",
+ "foo |", [1, 3, 5, 7, 8, 9, 10, 11], () => changeRestrict("title", "|")],
+ ["9.1: foo @ -> in url",
+ "foo @", [2, 3, 6, 7, 10, 11], () => resetRestrict("title")],
+ ["9.2: foo | -> in url (change pref)",
+ "foo |", [2, 3, 6, 7, 10, 11], () => changeRestrict("url", "|")],
+ ["10: foo + -> is tag",
+ "foo +", [8, 9, 10, 11], () => resetRestrict("url")],
+ ["10.2: foo | -> is tag (change pref)",
+ "foo |", [8, 9, 10, 11], () => changeRestrict("tag", "|")],
+ ["10.3: foo ~ -> is typed",
+ "foo ~", [3, 10], () => resetRestrict("tag")],
+ ["10.4: foo | -> is typed (change pref)",
+ "foo |", [3, 10], () => changeRestrict("typed", "|")],
+
+ // Test various pairs of special searches
+ ["11: foo ^ * -> history, is star",
+ "foo ^ *", [5, 10], () => resetRestrict("typed")],
+ ["12: foo ^ # -> history, in title",
+ "foo ^ #", [1, 3, 5, 10]],
+ ["13: foo ^ @ -> history, in url",
+ "foo ^ @", [2, 3, 10]],
+ ["14: foo ^ + -> history, is tag",
+ "foo ^ +", [10]],
+ ["14.1: foo ^ ~ -> history, is typed",
+ "foo ^ ~", [3, 10]],
+ ["15: foo * # -> is star, in title",
+ "foo * #", [5, 7, 8, 9, 10, 11]],
+ ["16: foo * @ -> is star, in url",
+ "foo * @", [6, 7, 10, 11]],
+ ["17: foo * + -> same as +",
+ "foo * +", [8, 9, 10, 11]],
+ ["17.1: foo * ~ -> is star, is typed",
+ "foo * ~", [10]],
+ ["18: foo # @ -> in title, in url",
+ "foo # @", [3, 7, 10, 11]],
+ ["19: foo # + -> in title, is tag",
+ "foo # +", [8, 9, 10, 11]],
+ ["19.1: foo # ~ -> in title, is typed",
+ "foo # ~", [3, 10]],
+ ["20: foo @ + -> in url, is tag",
+ "foo @ +", [10, 11]],
+ ["20.1: foo @ ~ -> in url, is typed",
+ "foo @ ~", [3, 10]],
+ ["20.2: foo + ~ -> is tag, is typed",
+ "foo + ~", [10]],
+
+ // Test default usage by setting certain bits of default.behavior to 1
+ ["21: foo -> default history",
+ "foo", [1, 2, 3, 5, 10], function () { setPref({ history: true }); }],
+ ["22: foo -> default history or is star",
+ "foo", [1, 2, 3, 5, 6, 7, 8, 9, 10, 11], () => setPref({ history: true, bookmark: true })],
+ ["22.1: foo -> default history or is star, is typed",
+ "foo", [3, 10], () => setPref({ history: true, bookmark: true, "history.onlyTyped": true })],
+
+];
+
+function setPref(aTypes) {
+ clearSuggestPrefs();
+ for (let type in aTypes) {
+ prefs.setBoolPref("browser.urlbar.suggest." + type, aTypes[type]);
+ }
+}
+
+function clearSuggestPrefs() {
+ prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
+ prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+}
+
+function changeRestrict(aType, aChar)
+{
+ let branch = "browser.urlbar.";
+ // "title" and "url" are different from everything else, so special case them.
+ if (aType == "title" || aType == "url")
+ branch += "match.";
+ else
+ branch += "restrict.";
+
+ print("changing restrict for " + aType + " to '" + aChar + "'");
+ prefs.setCharPref(branch + aType, aChar);
+}
+
+function resetRestrict(aType)
+{
+ let branch = "browser.urlbar.";
+ // "title" and "url" are different from everything else, so special case them.
+ if (aType == "title" || aType == "url")
+ branch += "match.";
+ else
+ branch += "restrict.";
+
+ if (prefs.prefHasUserValue(branch + aType))
+ prefs.clearUserPref(branch + aType);
+}
diff --git a/comm/suite/components/places/tests/autocomplete/test_swap_protocol.js b/comm/suite/components/places/tests/autocomplete/test_swap_protocol.js
new file mode 100644
index 0000000000..860b722498
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_swap_protocol.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 424717 to make sure searching with an existing location like
+ * http://site/ also matches https://site/ or ftp://site/. Same thing for
+ * ftp://site/ and https://site/.
+ *
+ * Test bug 461483 to make sure a search for "w" doesn't match the "www." from
+ * site subdomains.
+ */
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://www.site/",
+ "http://site/",
+ "ftp://ftp.site/",
+ "ftp://site/",
+ "https://www.site/",
+ "https://site/",
+ "http://woohoo/",
+ "http://wwwwwwacko/",
+];
+var kTitles = [
+ "title",
+];
+
+// Add various protocols of site
+addPageBook(0, 0);
+addPageBook(1, 0);
+addPageBook(2, 0);
+addPageBook(3, 0);
+addPageBook(4, 0);
+addPageBook(5, 0);
+addPageBook(6, 0);
+addPageBook(7, 0);
+
+var allSite = [0, 1, 2, 3, 4, 5];
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ ["0: http://www.site matches all site", "http://www.site", allSite],
+ ["1: http://site matches all site", "http://site", allSite],
+ ["2: ftp://ftp.site matches itself", "ftp://ftp.site", [2]],
+ ["3: ftp://site matches all site", "ftp://site", allSite],
+ ["4: https://www.site matches all site", "https://www.site", allSite],
+ ["5: https://site matches all site", "https://site", allSite],
+ ["6: www.site matches all site", "www.site", allSite],
+
+ ["7: w matches none of www.", "w", [6, 7]],
+ ["8: http://w matches none of www.", "w", [6, 7]],
+ ["9: http://www.w matches none of www.", "w", [6, 7]],
+
+ ["10: ww matches none of www.", "ww", [7]],
+ ["11: http://ww matches none of www.", "http://ww", [7]],
+ ["12: http://www.ww matches none of www.", "http://www.ww", [7]],
+
+ ["13: www matches none of www.", "www", [7]],
+ ["14: http://www matches none of www.", "http://www", [7]],
+ ["15: http://www.www matches none of www.", "http://www.www", [7]],
+];
diff --git a/comm/suite/components/places/tests/autocomplete/test_tabmatches.js b/comm/suite/components/places/tests/autocomplete/test_tabmatches.js
new file mode 100644
index 0000000000..22f83a86e6
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_tabmatches.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTabRestrictChar = "%";
+prefs.setCharPref("browser.urlbar.restrict.openpage", gTabRestrictChar);
+registerCleanupFunction(() => {
+ prefs.clearUserPref("browser.urlbar.restrict.openpage");
+});
+
+var kSearchParam = "enable-actions";
+
+var kURIs = [
+ "http://abc.com/",
+ "moz-action:switchtab,http://abc.com/",
+ "http://xyz.net/",
+ "moz-action:switchtab,http://xyz.net/",
+ "about:mozilla",
+ "moz-action:switchtab,about:mozilla",
+ "data:text/html,test",
+ "moz-action:switchtab,data:text/html,test"
+];
+
+var kTitles = [
+ "ABC rocks",
+ "xyz.net - we're better than ABC",
+ "about:mozilla",
+ "data:text/html,test"
+];
+
+addPageBook(0, 0);
+gPages[1] = [1, 0];
+addPageBook(2, 1);
+gPages[3] = [3, 1];
+
+addOpenPages(0, 1);
+
+// PAges that cannot be registered in history.
+addOpenPages(4, 1);
+gPages[5] = [5, 2];
+addOpenPages(6, 1);
+gPages[7] = [7, 3];
+
+var gTests = [
+ ["0: single result, that is also a tab match",
+ "abc.com", [1]],
+ ["1: two results, one tab match",
+ "abc", [1, 2]],
+ ["2: two results, both tab matches",
+ "abc", [1, 3],
+ function() {
+ addOpenPages(2, 1);
+ }],
+ ["3: two results, both tab matches, one has multiple tabs",
+ "abc", [1, 3],
+ function() {
+ addOpenPages(2, 5);
+ }],
+ ["4: two results, no tab matches",
+ "abc", [0, 2],
+ function() {
+ removeOpenPages(0, 1);
+ removeOpenPages(2, 6);
+ }],
+ ["5: tab match search with restriction character",
+ gTabRestrictChar + " abc", [1],
+ function() {
+ addOpenPages(0, 1);
+ }],
+ ["6: tab match with not-addable pages",
+ "mozilla", [5]],
+ ["7: tab match with not-addable pages and restriction character",
+ gTabRestrictChar + " mozilla", [5]],
+ ["8: tab match with not-addable pages and only restriction character",
+ gTabRestrictChar, [1, 5, 7]],
+];
+
+
+function addOpenPages(aUri, aCount) {
+ let num = aCount || 1;
+ let acprovider = Cc["@mozilla.org/autocomplete/search;1?name=history"].
+ getService(Ci.mozIPlacesAutoComplete);
+ for (let i = 0; i < num; i++) {
+ acprovider.registerOpenPage(toURI(kURIs[aUri]));
+ }
+}
+
+function removeOpenPages(aUri, aCount) {
+ let num = aCount || 1;
+ let acprovider = Cc["@mozilla.org/autocomplete/search;1?name=history"].
+ getService(Ci.mozIPlacesAutoComplete);
+ for (let i = 0; i < num; i++) {
+ acprovider.unregisterOpenPage(toURI(kURIs[aUri]));
+ }
+}
diff --git a/comm/suite/components/places/tests/autocomplete/test_word_boundary_search.js b/comm/suite/components/places/tests/autocomplete/test_word_boundary_search.js
new file mode 100644
index 0000000000..b4ae368491
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/test_word_boundary_search.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 393678 to make sure matches against the url, title, tags are only
+ * made on word boundaries instead of in the middle of words.
+ *
+ * Make sure we don't try matching one after a CamelCase because the upper-case
+ * isn't really a word boundary. (bug 429498)
+ *
+ * Bug 429531 provides switching between "must match on word boundary" and "can
+ * match," so leverage "must match" pref for checking word boundary logic and
+ * make sure "can match" matches anywhere.
+ */
+
+var katakana = ["\u30a8", "\u30c9"]; // E, Do
+var ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do
+
+// Define some shared uris and titles (each page needs its own uri)
+var kURIs = [
+ "http://matchme/",
+ "http://dontmatchme/",
+ "http://title/1",
+ "http://title/2",
+ "http://tag/1",
+ "http://tag/2",
+ "http://crazytitle/",
+ "http://katakana/",
+ "http://ideograph/",
+ "http://camel/pleaseMatchMe/",
+];
+var kTitles = [
+ "title1",
+ "matchme2",
+ "dontmatchme3",
+ "!@#$%^&*()_+{}|:<>?word",
+ katakana.join(""),
+ ideograph.join(""),
+];
+
+// Boundaries on the url
+addPageBook(0, 0);
+addPageBook(1, 0);
+// Boundaries on the title
+addPageBook(2, 1);
+addPageBook(3, 2);
+// Boundaries on the tag
+addPageBook(4, 0, 0, [1]);
+addPageBook(5, 0, 0, [2]);
+// Lots of word boundaries before a word
+addPageBook(6, 3);
+// Katakana
+addPageBook(7, 4);
+// Ideograph
+addPageBook(8, 5);
+// CamelCase
+addPageBook(9, 0);
+
+// Provide for each test: description; search terms; array of gPages indices of
+// pages that should match; optional function to be run before the test
+var gTests = [
+ // Tests after this one will match only on word boundaries
+ ["0: Match 'match' at the beginning or after / or on a CamelCase",
+ "match", [0, 2, 4, 9],
+ () => setBehavior(2)],
+ ["1: Match 'dont' at the beginning or after /",
+ "dont", [1, 3, 5]],
+ ["2: Match '2' after the slash and after a word (in tags too)",
+ "2", [2, 3, 4, 5]],
+ ["3: Match 't' at the beginning or after /",
+ "t", [0, 1, 2, 3, 4, 5, 9]],
+ ["4: Match 'word' after many consecutive word boundaries",
+ "word", [6]],
+ ["5: Match a word boundary '/' for everything",
+ "/", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]],
+ ["6: Match word boundaries '()_+' that are among word boundaries",
+ "()_+", [6]],
+
+ ["7: Katakana characters form a string, so match the beginning",
+ katakana[0], [7]],
+ /*["8: Middle of a katakana word shouldn't be matched",
+ katakana[1], []],*/
+
+ ["9: Ideographs are treated as words so 'nin' is one word",
+ ideograph[0], [8]],
+ ["10: Ideographs are treated as words so 'ten' is another word",
+ ideograph[1], [8]],
+ ["11: Ideographs are treated as words so 'do' is yet another",
+ ideograph[2], [8]],
+
+ ["12: Extra negative assert that we don't match in the middle",
+ "ch", []],
+ ["13: Don't match one character after a camel-case word boundary (bug 429498)",
+ "atch", []],
+
+ // Tests after this one will match against word boundaries and anywhere
+ ["14: Match on word boundaries as well as anywhere (bug 429531)",
+ "tch", [0, 1, 2, 3, 4, 5, 9],
+ () => setBehavior(1)],
+];
+
+function setBehavior(aType) {
+ prefs.setIntPref("browser.urlbar.matchBehavior", aType);
+}
diff --git a/comm/suite/components/places/tests/autocomplete/xpcshell.ini b/comm/suite/components/places/tests/autocomplete/xpcshell.ini
new file mode 100644
index 0000000000..7c018dbcc7
--- /dev/null
+++ b/comm/suite/components/places/tests/autocomplete/xpcshell.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+head = head_autocomplete.js
+tail =
+skip-if = toolkit == 'android' || toolkit == 'gonk'
+
+[test_416211.js]
+[test_416214.js]
+[test_417798.js]
+[test_418257.js]
+[test_422277.js]
+[test_autocomplete_on_value_removed_479089.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_download_embed_bookmarks.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_empty_search.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_enabled.js]
+[test_escape_self.js]
+[test_ignore_protocol.js]
+[test_keyword_search.js]
+[test_match_beginning.js]
+[test_multi_word_search.js]
+[test_special_search.js]
+[test_swap_protocol.js]
+[test_tabmatches.js]
+[test_word_boundary_search.js]
diff --git a/comm/suite/components/places/tests/browser/browser.ini b/comm/suite/components/places/tests/browser/browser.ini
new file mode 100644
index 0000000000..f21d4b73a2
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_0_library_left_pane_migration.js]
+[browser_425884.js]
+[browser_drag_bookmarks_on_toolbar.js]
+[browser_library_infoBox.js]
+[browser_library_left_pane_commands.js]
+[browser_library_left_pane_fixnames.js]
+[browser_library_open_leak.js]
+[browser_library_views_liveupdate.js]
+[browser_sort_in_library.js]
diff --git a/comm/suite/components/places/tests/browser/browser_0_library_left_pane_migration.js b/comm/suite/components/places/tests/browser/browser_0_library_left_pane_migration.js
new file mode 100644
index 0000000000..1db96f5000
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_0_library_left_pane_migration.js
@@ -0,0 +1,93 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test we correctly migrate Library left pane to the latest version.
+ * Note: this test MUST be the first between browser chrome tests, or results
+ * of next tests could be unexpected due to PlacesUIUtils getters.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+function onLibraryReady(organizer) {
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0,
+ "Left pane folder correctly created");
+ var leftPaneItems =
+ PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1,
+ "We correctly have only 1 left pane folder");
+ var leftPaneRoot = leftPaneItems[0];
+ is(leftPaneRoot, PlacesUIUtils.leftPaneFolderId,
+ "leftPaneFolderId getter has correct value");
+ // Check version has been upgraded.
+ var version =
+ PlacesUtils.annotations.getItemAnnotation(leftPaneRoot,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
+ "Left pane version has been correctly upgraded");
+
+ // Check left pane is populated.
+ organizer.PlacesOrganizer.selectLeftPaneQuery('AllBookmarks');
+ is(organizer.PlacesOrganizer._places.selectedNode.itemId,
+ PlacesUIUtils.leftPaneQueries["AllBookmarks"],
+ "Library left pane is populated and working");
+
+ // Close Library window.
+ organizer.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Check if we have any left pane folder already set, remove it eventually.
+ var leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ if (leftPaneItems.length > 0) {
+ // The left pane has already been created, touching it now would cause
+ // next tests to rely on wrong values (and possibly crash)
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ var version = PlacesUtils.annotations.getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+ ok(true, "left pane has already been created, skipping test");
+ finish();
+ return;
+ }
+
+ // Create a fake left pane folder with an old version (current version - 1).
+ var fakeLeftPaneRoot =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, "",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(fakeLeftPaneRoot,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Check fake left pane root has been correctly created.
+ var leftPaneItems =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ is(leftPaneItems[0], fakeLeftPaneRoot, "left pane root itemId is correct");
+
+ // Check version.
+ var version = PlacesUtils.annotations.getItemAnnotation(fakeLeftPaneRoot,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1, "Left pane version correctly set");
+
+ // Open Library, this will upgrade our left pane version.
+ openLibrary(onLibraryReady);
+}
diff --git a/comm/suite/components/places/tests/browser/browser_425884.js b/comm/suite/components/places/tests/browser/browser_425884.js
new file mode 100644
index 0000000000..4140ac0234
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_425884.js
@@ -0,0 +1,103 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ // sanity check
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?");
+
+ /*
+ Deep copy of bookmark data, using the front-end codepath:
+
+ - create test folder A
+ - add a subfolder to folder A, and add items to it
+ - validate folder A (sanity check)
+ - copy folder A, creating new folder B, using the front-end path
+ - validate folder B
+ - undo copy transaction
+ - validate folder B (empty)
+ - redo copy transaction
+ - validate folder B's contents
+
+ */
+
+ var toolbarId = PlacesUtils.toolbarFolderId;
+ var toolbarNode = PlacesUtils.getFolderContents(toolbarId).root;
+
+ var oldCount = toolbarNode.childCount;
+ var testRootId = PlacesUtils.bookmarks.createFolder(toolbarId, "test root", -1);
+ is(toolbarNode.childCount, oldCount+1, "confirm test root node is a container, and is empty");
+ var testRootNode = toolbarNode.getChild(toolbarNode.childCount-1);
+ testRootNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ testRootNode.containerOpen = true;
+ is(testRootNode.childCount, 0, "confirm test root node is a container, and is empty");
+
+ // create folder A, fill it, validate its contents
+ var folderAId = PlacesUtils.bookmarks.createFolder(testRootId, "A", -1);
+ populate(folderAId);
+ var folderANode = PlacesUtils.getFolderContents(folderAId).root;
+ validate(folderANode);
+ is(testRootNode.childCount, 1, "create test folder");
+
+ // copy it, using the front-end helper functions
+ var serializedNode = PlacesUtils.wrapNode(folderANode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ var rawNode = PlacesUtils.unwrapNodes(serializedNode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER).shift();
+ // confirm serialization
+ ok(rawNode.type, "confirm json node");
+ folderANode.containerOpen = false;
+
+ var transaction = PlacesUIUtils.makeTransaction(rawNode,
+ PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ testRootId,
+ -1,
+ true);
+ ok(transaction, "create transaction");
+ PlacesUtils.transactionManager.doTransaction(transaction);
+ // confirm copy
+ is(testRootNode.childCount, 2, "create test folder via copy");
+
+ // validate the copy
+ var folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // undo the transaction, confirm the removal
+ PlacesUtils.transactionManager.undoTransaction();
+ is(testRootNode.childCount, 1, "confirm undo removed the copied folder");
+
+ // redo the transaction
+ PlacesUtils.transactionManager.redoTransaction();
+ is(testRootNode.childCount, 2, "confirm redo re-copied the folder");
+ folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // Close containers, cleaning up their observers.
+ testRootNode.containerOpen = false;
+ toolbarNode.containerOpen = false;
+
+ // clean up
+ PlacesUtils.transactionManager.undoTransaction();
+ PlacesUtils.bookmarks.removeItem(folderAId);
+}
+
+function populate(aFolderId) {
+ var folderId = PlacesUtils.bookmarks.createFolder(aFolderId, "test folder", -1);
+ PlacesUtils.bookmarks.insertBookmark(folderId, PlacesUtils._uri("http://foo"), -1, "test bookmark");
+ PlacesUtils.bookmarks.insertSeparator(folderId, -1);
+}
+
+function validate(aNode) {
+ PlacesUtils.asContainer(aNode);
+ aNode.containerOpen = true;
+ is(aNode.childCount, 1, "confirm child count match");
+ var folderNode = aNode.getChild(0);
+ is(folderNode.title, "test folder", "confirm folder title");
+ PlacesUtils.asContainer(folderNode);
+ folderNode.containerOpen = true;
+ is(folderNode.childCount, 2, "confirm child count match");
+ var bookmarkNode = folderNode.getChild(0);
+ var separatorNode = folderNode.getChild(1);
+ folderNode.containerOpen = false;
+ aNode.containerOpen = false;
+}
diff --git a/comm/suite/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/comm/suite/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
new file mode 100644
index 0000000000..62af31ccf7
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TEST_URL = "http://www.mozilla.org";
+const TEST_TITLE = "example_title";
+
+var gBookmarksToolbar = window.document.getElementById("PlacesToolbar");
+var dragDirections = { LEFT: 0, UP: 1, RIGHT: 2, DOWN: 3 };
+
+/**
+ * Tests dragging on toolbar.
+ *
+ * We must test these 2 cases:
+ * - Dragging toward left, top, right should start a drag.
+ * - Dragging toward down should should open the container if the item is a
+ * container, drag the item otherwise.
+ *
+ * @param aElement
+ * DOM node element we will drag
+ * @param aExpectedDragData
+ * Array of flavors and values in the form:
+ * [ ["text/plain: sometext", "text/html: <b>sometext</b>"], [...] ]
+ * Pass an empty array to check that drag even has been canceled.
+ * @param aDirection
+ * Direction for the dragging gesture, see dragDirections helper object.
+ */
+function synthesizeDragWithDirection(aElement, aExpectedDragData, aDirection) {
+ var trapped = false;
+
+ // Dragstart listener function.
+ var trapDrag = function(event) {
+ trapped = true;
+ var dataTransfer = event.dataTransfer;
+ is(dataTransfer.mozItemCount, aExpectedDragData.length,
+ "Number of dragged items should be the same.");
+
+ for (var t = 0; t < dataTransfer.mozItemCount; t++) {
+ var types = dataTransfer.mozTypesAt(t);
+ var expecteditem = aExpectedDragData[t];
+ is(types.length, expecteditem.length,
+ "Number of flavors for item " + t + " should be the same.");
+
+ for (var f = 0; f < types.length; f++) {
+ is(types[f], expecteditem[f].substring(0, types[f].length),
+ "Flavor " + types[f] + " for item " + t + " should be the same.");
+ is(dataTransfer.mozGetDataAt(types[f], t),
+ expecteditem[f].substring(types[f].length + 2),
+ "Contents for item " + t + " with flavor " + types[f] + " should be the same.");
+ }
+ }
+
+ if (!aExpectedDragData.length)
+ ok(event.defaultPrevented, "Drag has been canceled.");
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ var prevent = function(aEvent) {aEvent.preventDefault();}
+
+ var xIncrement = 0;
+ var yIncrement = 0;
+
+ switch (aDirection) {
+ case dragDirections.LEFT:
+ xIncrement = -1;
+ break;
+ case dragDirections.RIGHT:
+ xIncrement = +1;
+ break;
+ case dragDirections.UP:
+ yIncrement = -1;
+ break;
+ case dragDirections.DOWN:
+ yIncrement = +1;
+ break;
+ }
+
+ var rect = aElement.getBoundingClientRect();
+ var startingPoint = { x: (rect.right - rect.left)/2,
+ y: (rect.bottom - rect.top)/2 };
+
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x,
+ startingPoint.y,
+ { type: "mousedown" });
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 1,
+ startingPoint.y + yIncrement * 1,
+ { type: "mousemove" });
+ gBookmarksToolbar.addEventListener("dragstart", trapDrag);
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mousemove" });
+ ok(trapped, "A dragstart event has been trapped.");
+ gBookmarksToolbar.removeEventListener("dragstart", trapDrag);
+
+ // This is likely to cause a click event, and, in case we are dragging a
+ // bookmark, an unwanted page visit. Prevent the click event.
+ aElement.addEventListener("click", prevent);
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mouseup" });
+ aElement.removeEventListener("click", prevent);
+
+ // Cleanup eventually opened menus.
+ if (aElement.localName == "menu" && aElement.open)
+ aElement.open = false;
+}
+
+function getToolbarNodeForItemId(aItemId) {
+ var children = document.getElementById("PlacesToolbarItems").childNodes;
+ var node = null;
+ for (var i = 0; i < children.length; i++) {
+ if (aItemId == children[i]._placesNode.itemId) {
+ node = children[i];
+ break;
+ }
+ }
+ return node;
+}
+
+function getExpectedDataForPlacesNode(aNode) {
+ var wrappedNode = [];
+ var flavors = ["text/x-moz-place",
+ "text/x-moz-url",
+ "text/plain",
+ "text/html"];
+
+ flavors.forEach(function(aFlavor) {
+ var wrappedFlavor = aFlavor + ": " +
+ PlacesUtils.wrapNode(aNode, aFlavor);
+ wrappedNode.push(wrappedFlavor);
+ });
+
+ return [wrappedNode];
+}
+
+var gTests = [
+
+//------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a folder on toolbar",
+ run: function() {
+ // Create a test folder to be dragged.
+ var folderId = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.toolbarFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ var element = getToolbarNodeForItemId(folderId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ ok(true, "Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT);
+ ok(true, "Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT);
+ ok(true, "Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP);
+ ok(true, "Dragging down");
+ synthesizeDragWithDirection(element, new Array(), dragDirections.DOWN);
+
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(folderId);
+ }
+ },
+
+//------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a bookmark on toolbar",
+ run: function() {
+ // Create a test bookmark to be dragged.
+ var itemId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ var element = getToolbarNodeForItemId(itemId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ ok(true, "Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT);
+ ok(true, "Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT);
+ ok(true, "Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP);
+ ok(true, "Dragging down");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.DOWN);
+
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(itemId);
+ }
+ },
+];
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ info("Start of test: " + test.desc);
+ test.run();
+
+ setTimeout(nextTest, 0);
+ }
+ else {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed)
+ toolbar.collapsed = true;
+ finish();
+ }
+}
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+function test() {
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed)
+ toolbar.collapsed = false;
+
+ waitForExplicitFinish();
+ nextTest();
+}
+
diff --git a/comm/suite/components/places/tests/browser/browser_library_infoBox.js b/comm/suite/components/places/tests/browser/browser_library_infoBox.js
new file mode 100644
index 0000000000..03ee11574a
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_library_infoBox.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test appropriate visibility of infoBoxExpanderWrapper and
+ * additionalInfoFields in infoBox section of library
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+var gTests = [];
+var gLibrary;
+
+//------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Bug 430148 - Remove or hide the more/less button in details pane...",
+ run: function() {
+ var PO = gLibrary.PlacesOrganizer;
+ var infoBoxExpanderWrapper = getAndCheckElmtById("infoBoxExpanderWrapper");
+
+ function addVisitsCallback() {
+ // open all bookmarks node
+ PO.selectLeftPaneQuery("AllBookmarks");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected all bookmarks node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for all bookmarks node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open bookmarks menu node
+ PO.selectLeftPaneQuery("BookmarksMenu");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected bookmarks menu node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for bookmarks menu node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open recently bookmarked node
+ var menuNode = PO._places.selectedNode.
+ QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ menuNode.containerOpen = true;
+ var childNode = menuNode.getChild(0);
+ isnot(childNode, null, "Bookmarks menu child node exists.");
+ var recentlyBookmarkedTitle = PlacesUIUtils.
+ getString("recentlyBookmarkedTitle");
+ isnot(recentlyBookmarkedTitle, null,
+ "Correctly got the recently bookmarked title locale string.");
+ is(childNode.title, recentlyBookmarkedTitle,
+ "Correctly selected recently bookmarked node.");
+ PO._places.selectNode(childNode);
+ checkInfoBoxSelected(PO);
+ // Note: SeaMonkey differs from Firefox UI in this case.
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for recently bookmarked node.");
+ checkAddInfoFieldsNotCollapsed(PO);
+
+ // open first bookmark
+ PO._content.focus();
+ var view = PO._content.treeBoxObject.view;
+ ok(view.rowCount > 0, "Bookmark item exists.");
+ view.selection.select(0);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for bookmark item.");
+ checkAddInfoFieldsNotCollapsed(PO);
+ checkAddInfoFields(PO, "bookmark item");
+
+ // make sure additional fields are still hidden in second bookmark item
+ ok(view.rowCount > 1, "Second bookmark item exists.");
+ view.selection.select(1);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for second bookmark item.");
+ checkAddInfoFieldsNotCollapsed(PO);
+ checkAddInfoFields(PO, "second bookmark item");
+
+ menuNode.containerOpen = false;
+
+ waitForClearHistory(nextTest);
+ };
+
+ // Add a visit to browser history
+ addVisits(
+ { uri: PlacesUtils._uri(TEST_URI),
+ visitDate: Date.now()*1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED },
+ addVisitsCallback);
+ }
+});
+
+function checkInfoBoxSelected(PO) {
+ is(getAndCheckElmtById("detailsDeck").selectedIndex, 1,
+ "Selected element in detailsDeck is infoBox.");
+}
+
+function checkAddInfoFieldsCollapsed(PO) {
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).collapsed,
+ "Additional info field correctly collapsed: #" + id);
+ });
+}
+
+function checkAddInfoFieldsNotCollapsed(PO) {
+ ok(PO._additionalInfoFields.some(function (id) {
+ return !getAndCheckElmtById(id).collapsed;
+ }), "Some additional info field correctly not collapsed");
+}
+
+function checkAddInfoFields(PO, nodeName) {
+ ok(true, "Checking additional info fields visibiity for node: " + nodeName);
+ var expanderButton = getAndCheckElmtById("infoBoxExpander");
+
+ // make sure additional fields are hidden by default
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden by default: #" + id);
+ });
+
+ // toggle fields and make sure they are hidden/unhidden as expected
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(!getAndCheckElmtById(id).hidden,
+ "Additional info field correctly unhidden after toggle: #" + id);
+ });
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden after toggle: #" + id);
+ });
+}
+
+function getAndCheckElmtById(id) {
+ var elmt = gLibrary.document.getElementById(id);
+ isnot(elmt, null, "Correctly got element: #" + id);
+ return elmt;
+}
+
+//------------------------------------------------------------------------------
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ ok(true, "TEST: " + test.desc);
+ dump("TEST: " + test.desc + "\n");
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+
+ // Open Library.
+ openLibrary(function (library) {
+ gLibrary = library;
+ gLibrary.PlacesOrganizer._places.focus();
+ nextTest(gLibrary);
+ });
+}
diff --git a/comm/suite/components/places/tests/browser/browser_library_left_pane_commands.js b/comm/suite/components/places/tests/browser/browser_library_left_pane_commands.js
new file mode 100644
index 0000000000..bd4f089018
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_library_left_pane_commands.js
@@ -0,0 +1,100 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test enabled commands in the left pane folder of the Library.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+var gTests = [];
+var gLibrary;
+
+//------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Bug 490156 - Can't delete smart bookmark containers",
+ run: function() {
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ var PO = gLibrary.PlacesOrganizer;
+ PO.selectLeftPaneQuery('BookmarksToolbar');
+ isnot(PO._places.selectedNode, null, "We have a valid selection");
+ is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+ PlacesUtils.toolbarFolderId,
+ "We have correctly selected bookmarks toolbar node.");
+
+ // Check that both cut and delete commands are disabled.
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+
+ var toolbarNode = PO._places.selectedNode
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ toolbarNode.containerOpen = true;
+
+ // Add an History query to the toolbar.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri("place:sort=4"),
+ 0, // Insert at start.
+ "special_query");
+ // Get first child and check it is the "Most Visited" smart bookmark.
+ ok(toolbarNode.childCount > 0, "Toolbar node has children");
+ var queryNode = toolbarNode.getChild(0);
+ is(queryNode.title, "special_query", "Query node is correctly selected");
+
+ // Select query node.
+ PO._places.selectNode(queryNode);
+ is(PO._places.selectedNode, queryNode, "We correctly selected query node");
+
+ // Check that both cut and delete commands are enabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ // Execute the delete command and check bookmark has been removed.
+ PO._places.controller.doCommand("cmd_delete");
+ try {
+ PlacesUtils.bookmarks.getFolderIdForItem(queryNode.itemId);
+ ok(false, "Unable to remove query node bookmark");
+ } catch(ex) {
+ ok(true, "Query node bookmark has been correctly removed");
+ }
+
+ toolbarNode.containerOpen = false;
+ nextTest();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ info("Start of test: " + test.desc);
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+
+ // Open Library.
+ openLibrary(function (library) {
+ gLibrary = library;
+ nextTest();
+ });
+}
diff --git a/comm/suite/components/places/tests/browser/browser_library_left_pane_fixnames.js b/comm/suite/components/places/tests/browser/browser_library_left_pane_fixnames.js
new file mode 100644
index 0000000000..d46baf520a
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_library_left_pane_fixnames.js
@@ -0,0 +1,92 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test we correctly fix broken Library left pane queries names.
+ */
+
+// Array of left pane queries objects, each one has the following properties:
+// name: query's identifier got from annotations,
+// itemId: query's itemId,
+// correctTitle: original and correct query's title.
+var leftPaneQueries = [];
+
+function onLibraryReady(organizer) {
+ // Check titles have been fixed.
+ for (var i = 0; i < leftPaneQueries.length; i++) {
+ var query = leftPaneQueries[i];
+ if ("concreteId" in query) {
+ is(PlacesUtils.bookmarks.getItemTitle(query.concreteId),
+ query.concreteTitle, "Concrete title is correct for query " + query.name);
+ }
+ }
+
+ // Close Library window.
+ organizer.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Ensure left pane is initialized.
+ ok(PlacesUIUtils.leftPaneFolderId > 0, "left pane folder is initialized");
+
+ // Get the left pane folder.
+ var leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ var version = PlacesUtils.annotations
+ .getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+
+ // Get all left pane queries.
+ var items = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ // Get current queries names.
+ for (var i = 0; i < items.length; i++) {
+ var itemId = items[i];
+ var queryName = PlacesUtils.annotations
+ .getItemAnnotation(items[i],
+ PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ var query = { name: queryName,
+ itemId: itemId,
+ correctTitle: PlacesUtils.bookmarks.getItemTitle(itemId) }
+ switch (queryName) {
+ case "BookmarksToolbar":
+ query.concreteId = PlacesUtils.toolbarFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "BookmarksMenu":
+ query.concreteId = PlacesUtils.bookmarksMenuFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "UnfiledBookmarks":
+ query.concreteId = PlacesUtils.unfiledBookmarksFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ }
+ leftPaneQueries.push(query);
+ // Rename to a bad title.
+ PlacesUtils.bookmarks.setItemTitle(query.itemId, "badName");
+ if ("concreteId" in query)
+ PlacesUtils.bookmarks.setItemTitle(query.concreteId, "badName");
+ }
+
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Open Library, this will kick-off left pane code.
+ openLibrary(onLibraryReady);
+}
diff --git a/comm/suite/components/places/tests/browser/browser_library_open_leak.js b/comm/suite/components/places/tests/browser/browser_library_open_leak.js
new file mode 100644
index 0000000000..03aa3408f7
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_library_open_leak.js
@@ -0,0 +1,23 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 474831
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=474831
+ *
+ * Tests for leaks caused by simply opening and closing the Places Library
+ * window. Opens the Places Library window, waits for it to load, closes it,
+ * and finishes.
+ */
+
+function test() {
+ waitForExplicitFinish();
+ openLibrary(function (win) {
+ ok(true, "Library has been correctly opened");
+ win.close();
+ finish();
+ });
+}
diff --git a/comm/suite/components/places/tests/browser/browser_library_views_liveupdate.js b/comm/suite/components/places/tests/browser/browser_library_views_liveupdate.js
new file mode 100644
index 0000000000..33fe4f0b34
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_library_views_liveupdate.js
@@ -0,0 +1,303 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests Library Left pane view for liveupdate.
+ */
+
+var gLibrary = null;
+
+function test() {
+ waitForExplicitFinish();
+ // This test takes quite some time, and timeouts frequently, so we require
+ // more time to run.
+ // See Bug 525610.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Open Library, we will check the left pane.
+ openLibrary(function (library) {
+ gLibrary = library;
+ startTest();
+ });
+}
+
+/**
+ * Adds bookmarks observer, and executes a bunch of bookmarks operations.
+ */
+function startTest() {
+ var bs = PlacesUtils.bookmarks;
+ // Add observers.
+ bs.addObserver(bookmarksObserver);
+ PlacesUtils.annotations.addObserver(bookmarksObserver);
+ var addedBookmarks = [];
+
+ // MENU
+ ok(true, "*** Acting on menu bookmarks");
+ var id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("http://bm1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bm1");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "bm2");
+ bs.setItemTitle(id, "bm2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.bookmarksMenuFolder,
+ "bmf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "bmf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://bmf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.bookmarksMenuFolder, 0);
+
+ // TOOLBAR
+ ok(true, "*** Acting on toolbar bookmarks");
+ bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("http://tb1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tb1");
+ bs.setItemTitle(id, "tb1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "tb2");
+ bs.setItemTitle(id, "tb2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.toolbarFolder,
+ "tbf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "tbf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://tbf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.toolbarFolder, 0);
+
+ // UNSORTED
+ ok(true, "*** Acting on unsorted bookmarks");
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("http://ub1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ub1");
+ bs.setItemTitle(id, "ub1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "ub2");
+ bs.setItemTitle(id, "ub2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "ubf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "ubf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://ubf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ubf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.unfiledBookmarksFolder, 0);
+
+ // Remove all added bookmarks.
+ addedBookmarks.forEach(function (aItem) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ bs.removeItem(aItem);
+ } catch (ex) {}
+ });
+
+ // Remove observers.
+ bs.removeObserver(bookmarksObserver);
+ PlacesUtils.annotations.removeObserver(bookmarksObserver);
+ finishTest();
+}
+
+/**
+ * Restores browser state and calls finish.
+ */
+function finishTest() {
+ // Close Library window.
+ gLibrary.close();
+ finish();
+}
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ , Ci.nsIAnnotationObserver
+ ]),
+
+ // nsIAnnotationObserver
+ onItemAnnotationSet: function() {},
+ onItemAnnotationRemoved: function() {},
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+ // nsINavBookmarkObserver
+ onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
+ aURI) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ var type = PlacesUtils.bookmarks.getItemType(aItemId);
+ switch (type) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = PlacesUtils.bookmarks.getBookmarkURI(aItemId).spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onItemRemoved: function PSB_onItemRemoved(aItemId, aFolder, aIndex) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ is(node, null, "Places node not found in left pane");
+ },
+
+ onItemMoved: function(aItemId,
+ aOldFolderId, aOldIndex,
+ aNewFolderId, aNewIndex) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ var type = PlacesUtils.bookmarks.getItemType(aItemId);
+ switch (type) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = PlacesUtils.bookmarks.getBookmarkURI(aItemId).spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PSB_onEndUpdateBatch() {},
+ onItemVisited: function() {},
+ onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "title") {
+ let validator = function(aTreeRowIndex) {
+ let tree = gLibrary.PlacesOrganizer._places;
+ let cellText = tree.view.getCellText(aTreeRowIndex,
+ tree.columns.getColumnAt(0));
+ return cellText == aNewValue;
+ }
+ let [node, index, valid] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places, validator);
+ if (node) // Only visible nodes.
+ ok(valid, "Title cell value has been correctly updated");
+ }
+ }
+};
+
+
+/**
+ * Get places node and index for an itemId in a tree view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @param aTree
+ * Tree to search in.
+ * @param aValidator [optional]
+ * function to check row validity if found. Defaults to {return true;}.
+ * @returns [node, index, valid] or [null, null, false] if not found.
+ */
+function getNodeForTreeItem(aItemId, aTree, aValidator) {
+
+ function findNode(aContainerIndex) {
+ if (aTree.view.isContainerEmpty(aContainerIndex))
+ return [null, null, false];
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < aTree.view.rowCount; i++) {
+ var node = aTree.view.nodeForTreeIndex(i);
+
+ if (node.itemId == aItemId) {
+ // Minus one because we want relative index inside the container.
+ let valid = aValidator ? aValidator(i) : true;
+ return [node, i - aTree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+
+ // We have finished walking this container.
+ if (!aTree.view.hasNextSibling(aContainerIndex + 1, i))
+ break;
+ }
+ return [null, null, false]
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < aTree.view.rowCount; i++) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ return [null, null, false];
+}
diff --git a/comm/suite/components/places/tests/browser/browser_sort_in_library.js b/comm/suite/components/places/tests/browser/browser_sort_in_library.js
new file mode 100644
index 0000000000..6967be16aa
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/browser_sort_in_library.js
@@ -0,0 +1,254 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the following bugs:
+ *
+ * Bug 443745 - View>Sort>of "alpha" sort items is default to Z>A instead of A>Z
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=443745
+ *
+ * Bug 444179 - Library>Views>Sort>Sort by Tags does nothing
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=444179
+ *
+ * Basically, fully tests sorting the placeContent tree in the Places Library
+ * window. Sorting is verified by comparing the nsINavHistoryResult returned by
+ * placeContent.result to the expected sort values.
+ */
+
+// Two properties of nsINavHistoryResult control the sort of the tree:
+// sortingMode and sortingAnnotation. sortingMode's value is one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants. sortingAnnotation is the
+// annotation used to sort for SORT_BY_ANNOTATION_* mode.
+//
+// This lookup table maps the possible values of anonid's of the treecols to
+// objects that represent the treecols' correct state after the user sorts the
+// previously unsorted tree by selecting a column from the Views > Sort menu.
+// sortingMode is constructed from the key and dir properties (i.e.,
+// SORT_BY_<key>_<dir>) and sortingAnnotation is checked against anno. anno
+// may be undefined if key is not "ANNOTATION".
+const SORT_LOOKUP_TABLE = {
+ title: { key: "TITLE", dir: "ASCENDING" },
+ tags: { key: "TAGS", dir: "ASCENDING" },
+ url: { key: "URI", dir: "ASCENDING" },
+ date: { key: "DATE", dir: "DESCENDING" },
+ visitCount: { key: "VISITCOUNT", dir: "DESCENDING" },
+ keyword: { key: "KEYWORD", dir: "ASCENDING" },
+ dateAdded: { key: "DATEADDED", dir: "DESCENDING" },
+ lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" },
+ description: { key: "ANNOTATION",
+ dir: "ASCENDING",
+ anno: "bookmarkProperties/description" }
+};
+
+// This is the column that's sorted if one is not specified and the tree is
+// currently unsorted. Set it to a key substring in the name of one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants, e.g., "TITLE", "URI".
+// Method ViewMenu.setSortColumn in browser/components/places/content/places.js
+// determines this value.
+const DEFAULT_SORT_KEY = "TITLE";
+
+// Part of the test is checking that sorts stick, so each time we sort we need
+// to remember it.
+var prevSortDir = null;
+var prevSortKey = null;
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Ensures that the sort of aTree is aSortingMode and aSortingAnno.
+ *
+ * @param aTree
+ * the tree to check
+ * @param aSortingMode
+ * one of the Ci.nsINavHistoryQueryOptions.SORT_BY_* constants
+ * @param aSortingAnno
+ * checked only if sorting mode is one of the
+ * Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_* constants
+ */
+function checkSort(aTree, aSortingMode, aSortingAnno) {
+ // The placeContent tree's sort is determined by the nsINavHistoryResult it
+ // stores. Get it and check that the sort is what the caller expects.
+ let res = aTree.result;
+ isnot(res, null,
+ "sanity check: placeContent.result should not return null");
+
+ // Check sortingMode.
+ is(res.sortingMode, aSortingMode,
+ "column should now have sortingMode " + aSortingMode);
+
+ // Check sortingAnnotation, but only if sortingMode is ANNOTATION.
+ if ([Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING].
+ indexOf(aSortingMode) >= 0) {
+ is(res.sortingAnnotation, aSortingAnno,
+ "column should now have sorting annotation " + aSortingAnno);
+ }
+}
+
+/**
+ * Sets the sort of aTree.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aTree
+ * the tree to sort
+ * @param aUnsortFirst
+ * true if the sort should be set to SORT_BY_NONE before sorting by aCol
+ * and aDir
+ * @param aShouldFail
+ * true if setSortColumn should fail on aCol or aDir
+ * @param aCol
+ * the column of aTree by which to sort
+ * @param aDir
+ * either "ascending" or "descending"
+ */
+function setSort(aOrganizerWin, aTree, aUnsortFirst, aShouldFail, aCol, aDir) {
+ if (aUnsortFirst) {
+ aOrganizerWin.ViewMenu.setSortColumn();
+ checkSort(aTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+
+ // Remember the sort key and direction.
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+
+ let failed = false;
+ try {
+ aOrganizerWin.ViewMenu.setSortColumn(aCol, aDir);
+
+ // Remember the sort key and direction.
+ if (!aCol && !aDir) {
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+ else {
+ if (aCol)
+ prevSortKey = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].key;
+ else if (prevSortKey === null)
+ prevSortKey = DEFAULT_SORT_KEY;
+
+ if (aDir)
+ prevSortDir = aDir.toUpperCase();
+ else if (prevSortDir === null)
+ prevSortDir = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].dir;
+ }
+ } catch (exc) {
+ failed = true;
+ }
+
+ is(failed, !!aShouldFail,
+ "setSortColumn on column " +
+ (aCol ? aCol.getAttribute("anonid") : "(no column)") +
+ " with direction " + (aDir || "(no direction)") +
+ " and table previously " + (aUnsortFirst ? "unsorted" : "sorted") +
+ " should " + (aShouldFail ? "" : "not ") + "fail");
+}
+
+/**
+ * Tries sorting by an invalid column and sort direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ */
+function testInvalid(aOrganizerWin, aPlaceContentTree) {
+ // Invalid column should fail by throwing an exception.
+ let bogusCol = document.createElement("treecol");
+ bogusCol.setAttribute("anonid", "bogusColumn");
+ setSort(aOrganizerWin, aPlaceContentTree, true, true, bogusCol, "ascending");
+
+ // Invalid direction reverts to SORT_BY_NONE.
+ setSort(aOrganizerWin, aPlaceContentTree, false, false, null, "bogus dir");
+ checkSort(aPlaceContentTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+}
+
+/**
+ * Tests sorting aPlaceContentTree by column only and then by both column
+ * and direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByColAndDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ let cols = aPlaceContentTree.getElementsByTagName("treecol");
+ ok(cols.length > 0, "sanity check: placeContent should contain columns");
+
+ for (let i = 0; i < cols.length; i++) {
+ let col = cols.item(i);
+ ok(col.hasAttribute("anonid"),
+ "sanity check: column " + col.id + " should have anonid");
+
+ let colId = col.getAttribute("anonid");
+ ok(colId in SORT_LOOKUP_TABLE,
+ "sanity check: unexpected placeContent column anonid");
+
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" +
+ (aUnsortFirst ? SORT_LOOKUP_TABLE[colId].dir : prevSortDir);
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ let expectedAnno = SORT_LOOKUP_TABLE[colId].anno || "";
+
+ // Test sorting by only a column.
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+
+ // Test sorting by both a column and a direction.
+ ["ascending", "descending"].forEach(function (dir) {
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+ });
+ }
+}
+
+/**
+ * Tests sorting aPlaceContentTree by direction only.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ ["ascending", "descending"].forEach(function (dir) {
+ let key = (aUnsortFirst ? DEFAULT_SORT_KEY : prevSortKey);
+ let sortConst = "SORT_BY_" + key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, null, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, "");
+ });
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+function test() {
+ waitForExplicitFinish();
+
+ openLibrary(function (win) {
+ let tree = win.document.getElementById("placeContent");
+ isnot(tree, null, "sanity check: placeContent tree should exist");
+ // Run the tests.
+ testSortByColAndDir(win, tree, true);
+ testSortByColAndDir(win, tree, false);
+ testSortByDir(win, tree, true);
+ testSortByDir(win, tree, false);
+ testInvalid(win, tree);
+ // Reset the sort to SORT_BY_NONE.
+ setSort(win, tree, false, false);
+ // Close the window and finish.
+ win.close();
+ finish();
+ });
+}
diff --git a/comm/suite/components/places/tests/browser/head.js b/comm/suite/components/places/tests/browser/head.js
new file mode 100644
index 0000000000..d0fa1cdd49
--- /dev/null
+++ b/comm/suite/components/places/tests/browser/head.js
@@ -0,0 +1,95 @@
+
+// We need to cache this before test runs...
+var cachedLeftPaneFolderIdGetter;
+var getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function")
+ cachedLeftPaneFolderIdGetter = getter;
+
+// ...And restore it when test ends.
+registerCleanupFunction(function() {
+ let getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ if (cachedLeftPaneFolderIdGetter && typeof(getter) != "function")
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId",
+ cachedLeftPaneFolderIdGetter);
+});
+
+function openLibrary(callback) {
+ var library = window.openDialog(
+ "chrome://communicator/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable");
+ waitForFocus(function () {
+ callback(library);
+ }, library);
+}
+
+/**
+ * Waits for completion of a clear history operation, before
+ * proceeding with aCallback.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ */
+function waitForClearHistory(aCallback) {
+ Services.obs.addObserver(function observeCH(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observeCH, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ aCallback();
+ }, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ PlacesUtils.bhistory.removeAllPages();
+}
+
+/**
+ * Asynchronously adds visits to a page, invoking a callback function when done.
+ *
+ * @param aPlaceInfo
+ * Can be an nsIURI, in such a case a single LINK visit will be added.
+ * Otherwise can be an object describing the visit to add, or an array
+ * of these objects:
+ * { uri: nsIURI of the page,
+ * transition: one of the TRANSITION_* from nsINavHistoryService,
+ * [optional] title: title of the page,
+ * [optional] visitDate: visit date in microseconds from the epoch
+ * [optional] referrer: nsIURI of the referrer for this visit
+ * }
+ * @param [optional] aCallback
+ * Function to be invoked on completion.
+ */
+function addVisits(aPlaceInfo, aCallback) {
+ let places = [];
+ if (aPlaceInfo instanceof Ci.nsIURI) {
+ places.push({ uri: aPlaceInfo });
+ }
+ else if (Array.isArray(aPlaceInfo)) {
+ places = places.concat(aPlaceInfo);
+ } else {
+ places.push(aPlaceInfo)
+ }
+
+ // Create mozIVisitInfo for each entry.
+ let now = Date.now();
+ for (let i = 0; i < places.length; i++) {
+ if (!places[i].title) {
+ places[i].title = "test visit for " + places[i].uri.spec;
+ }
+ places[i].visits = [{
+ transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
+ : places[i].transition,
+ visitDate: places[i].visitDate || (now++) * 1000,
+ referrerURI: places[i].referrer
+ }];
+ }
+
+ PlacesUtils.asyncHistory.updatePlaces(
+ places,
+ {
+ handleError: function AAV_handleError() {
+ throw("Unexpected error in adding visit.");
+ },
+ handleResult: function () {},
+ handleCompletion: function UP_handleCompletion() {
+ if (aCallback)
+ aCallback();
+ }
+ }
+ );
+}
+
diff --git a/comm/suite/components/places/tests/chrome/chrome.ini b/comm/suite/components/places/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..38de538ec2
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/chrome.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = head.js
+
+[test_0_bug510634.xul]
+[test_0_multiple_left_pane.xul]
+[test_bug427633_no_newfolder_if_noip.xul]
+[test_bug485100-change-case-loses-tag.xul]
+[test_bug549192.xul]
+[test_bug549491.xul]
+[test_treeview_date.xul]
diff --git a/comm/suite/components/places/tests/chrome/head.js b/comm/suite/components/places/tests/chrome/head.js
new file mode 100644
index 0000000000..90d19c9def
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/head.js
@@ -0,0 +1,55 @@
+/**
+ * Asynchronously adds visits to a page, invoking a callback function when done.
+ *
+ * @param aPlaceInfo
+ * Can be an nsIURI, in such a case a single LINK visit will be added.
+ * Otherwise can be an object describing the visit to add, or an array
+ * of these objects:
+ * { uri: nsIURI of the page,
+ * transition: one of the TRANSITION_* from nsINavHistoryService,
+ * [optional] title: title of the page,
+ * [optional] visitDate: visit date in microseconds from the epoch
+ * [optional] referrer: nsIURI of the referrer for this visit
+ * }
+ * @param [optional] aCallback
+ * Function to be invoked on completion.
+ */
+function addVisits(aPlaceInfo, aCallback) {
+ let places = [];
+ if (aPlaceInfo instanceof Ci.nsIURI) {
+ places.push({ uri: aPlaceInfo });
+ }
+ else if (Array.isArray(aPlaceInfo)) {
+ places = places.concat(aPlaceInfo);
+ } else {
+ places.push(aPlaceInfo)
+ }
+
+ // Create mozIVisitInfo for each entry.
+ let now = Date.now();
+ for (let i = 0; i < places.length; i++) {
+ if (!places[i].title) {
+ places[i].title = "test visit for " + places[i].uri.spec;
+ }
+ places[i].visits = [{
+ transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
+ : places[i].transition,
+ visitDate: places[i].visitDate || (now++) * 1000,
+ referrerURI: places[i].referrer
+ }];
+ }
+
+ PlacesUtils.asyncHistory.updatePlaces(
+ places,
+ {
+ handleError: function AAV_handleError() {
+ throw("Unexpected error in adding visit.");
+ },
+ handleResult: function () {},
+ handleCompletion: function UP_handleCompletion() {
+ if (aCallback)
+ aCallback();
+ }
+ }
+ );
+}
diff --git a/comm/suite/components/places/tests/chrome/test_0_bug510634.xul b/comm/suite/components/places/tests/chrome/test_0_bug510634.xul
new file mode 100644
index 0000000000..95515b8e9f
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_0_bug510634.xul
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="510634: Wrong icons on bookmarks sidebar"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script>
+ <![CDATA[
+
+ /**
+ * Bug 510634 - Wrong icons on bookmarks sidebar
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=510634
+ *
+ * Ensures that properties for special queries are set on their tree nodes,
+ * even if PlacesUIUtils.leftPaneFolderId was not initialized.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // We need to cache and restore this getter in order to simulate
+ // Bug 510634
+ let cachedLeftPaneFolderIdGetter =
+ PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+
+ let leftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+
+ // restore the getter
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:queryType=1&folder=" + leftPaneFolderId;
+
+ // Open All Bookmarks
+ PlacesUtils.asContainer(tree.view.nodeForTreeIndex(1)).containerOpen = true;
+
+ // The query-property is set on the title column for each row.
+ let titleColumn = tree.treeBoxObject.columns.getColumnAt(0);
+
+ ["Tags", "AllBookmarks", "BookmarksToolbar",
+ "BookmarksMenu", "UnfiledBookmarks"].forEach(
+ function(aQueryName, aRow) {
+ let rowProperties = tree.view.getCellProperties(aRow, titleColumn).split(" ");
+ ok(rowProperties.includes("OrganizerQuery_" + aQueryName),
+ "OrganizerQuery_" + aQueryName + " is set");
+ }
+ );
+
+ // Close the root node
+ tree.result.root.containerOpen = false;
+
+ // Restore the getter for the next test.
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_0_multiple_left_pane.xul b/comm/suite/components/places/tests/chrome/test_0_multiple_left_pane.xul
new file mode 100644
index 0000000000..574bf3c1c6
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_0_multiple_left_pane.xul
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Bug 466422:
+ - Check that we replace the left pane with a correct one if it gets corrupted
+ - and we end up having more than one. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test handling of multiple left pane folders"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <script>
+ <![CDATA[
+
+ function runTest() {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, " +
+ "current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ var fakeLeftPanes = [];
+ var as = PlacesUtils.annotations;
+ var bs = PlacesUtils.bookmarks;
+
+ // We need 2 left pane folders to simulate a corrupt profile.
+ do {
+ let leftPaneItems = as.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ // Create a fake left pane folder.
+ let fakeLeftPaneRoot = bs.createFolder(PlacesUtils.placesRootId, "",
+ bs.DEFAULT_INDEX);
+ as.setItemAnnotation(fakeLeftPaneRoot, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 0,
+ as.EXPIRE_NEVER);
+ fakeLeftPanes.push(fakeLeftPaneRoot);
+ } while (fakeLeftPanes.length < 2);
+
+ // Initialize the left pane queries.
+ PlacesUIUtils.leftPaneFolderId;
+
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0,
+ "Left pane folder correctly created");
+ var leftPaneItems = as.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1,
+ "We correctly have only 1 left pane folder");
+
+ // Check that all old left pane items have been removed.
+ fakeLeftPanes.forEach(function(aItemId) {
+ try {
+ bs.getItemTitle(aItemId);
+ throw("This folder should have been removed");
+ } catch (ex) {}
+ });
+
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul b/comm/suite/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
new file mode 100644
index 0000000000..8c1d70ed2a
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if insertionPoint is invalid"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://communicator/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script>
+ <![CDATA[
+
+ /**
+ * Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if
+ * insertionPoint is invalid.
+ */
+
+ function runTest() {
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ function uri(spec) {
+ return Services.io.newURI(spec);
+ }
+
+ // Add a bookmark.
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "mozilla");
+
+ // Init panel.
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ gEditItemOverlay.initPanel(itemId);
+ ok(gEditItemOverlay._initialized, "gEditItemOverlay is initialized");
+ // We must be sure tree is initialized, so we wait for place to be set.
+ SimpleTest.waitForExplicitFinish();
+ var tree = gEditItemOverlay._element("folderTree");
+ tree.addEventListener("DOMAttrModified", function treeDOMAttrMod(event) {
+ if (event.attrName != "place")
+ return;
+ tree.removeEventListener("DOMAttrModified", treeDOMAttrMod, false);
+ SimpleTest.executeSoon(function() {
+ tree.view.selection.clearSelection();
+ ok(document.getElementById("editBMPanel_newFolderButton").disabled,
+ "New folder button is disabled if there's no selection");
+
+ // Cleanup.
+ bs.removeItem(itemId);
+ SimpleTest.finish();
+ });
+ }, false);
+ // Open the folder tree.
+ document.getElementById("editBMPanel_foldersExpander").doCommand();
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul b/comm/suite/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
new file mode 100644
index 0000000000..15c0ad4ad0
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://communicator/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="485100: Exchanging a letter of a tag name with its big/small equivalent removes tag from bookmark"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://communicator/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script>
+ <![CDATA[
+
+ function runTest() {
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var ts = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ function uri(spec) {
+ return Services.io.newURI(spec);
+ }
+
+ var testURI = uri("http://www.mozilla.org/");
+ var testTag = "foo";
+ var testTagUpper = "Foo";
+
+ // Add a bookmark
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ testURI,
+ bs.DEFAULT_INDEX,
+ "mozilla");
+
+ // Init panel
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ gEditItemOverlay.initPanel(itemId);
+
+ // add a tag
+ document.getElementById("editBMPanel_tagsField").value = testTag;
+ gEditItemOverlay.onTagsFieldBlur();
+
+ // test that the tag has been added in the backend
+ is(ts.getTagsForURI(testURI)[0], testTag, "tags match");
+
+ // change the tag
+ document.getElementById("editBMPanel_tagsField").value = testTagUpper;
+ gEditItemOverlay.onTagsFieldBlur();
+
+ // test that the tag has been added in the backend
+ is(ts.getTagsForURI(testURI)[0], testTagUpper, "tags match");
+
+ // Cleanup.
+ ts.untagURI(testURI, [testTag]);
+ bs.removeItem(itemId);
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_bug549192.xul b/comm/suite/components/places/tests/chrome/test_bug549192.xul
new file mode 100644
index 0000000000..55e5502764
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_bug549192.xul
@@ -0,0 +1,118 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script>
+ <![CDATA[
+ /**
+ * Bug 1388827 / Bug 874407
+ * Ensures that history views are updated properly after visits.
+ *
+ * Bug 549192
+ * Ensures that history views are updated after deleting entries.
+ */
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // The mochitest page is added to history.
+ waitForClearHistory(continue_test);
+ }
+
+ function continue_test() {
+ // Add some visits.
+ let vtime = Date.now() * 1000;
+ const ttype = PlacesUtils.history.TRANSITION_TYPED;
+ let places =
+ [{ uri: Services.io.newURI("http://example.tld/"),
+ visitDate: ++vtime, transition: ttype },
+ { uri: Services.io.newURI("http://example2.tld/"),
+ visitDate: ++vtime, transition: ttype },
+ { uri: Services.io.newURI("http://example3.tld/"),
+ visitDate: ++vtime, transition: ttype }];
+
+ addVisits(places, function() {
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ var tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check them.
+ let treeView = tree.view;
+ let selection = treeView.selection;
+ let rc = treeView.rowCount;
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ is(rc, 3, "Found expected number of rows.");
+
+ // First check live-update of the view when adding visits.
+ places.forEach(place => place.visitDate = ++vtime);
+ addVisits(places, function() {
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ // Now remove the pages and verify live-update again.
+ for (let i = 0; i < rc; i++) {
+ selection.select(0);
+ let node = tree.selectedNode;
+ tree.controller.remove("Removing page");
+ ok(treeView.treeIndexForNode(node) == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE,
+ node.uri + " removed.");
+ ok(treeView.rowCount == rc - i - 1, "Rows count decreased");
+ }
+
+ // Cleanup.
+ waitForClearHistory(SimpleTest.finish);
+ });
+ });
+ }
+
+ ]]></script>
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_bug549491.xul b/comm/suite/components/places/tests/chrome/test_bug549491.xul
new file mode 100644
index 0000000000..f211d62fd2
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_bug549491.xul
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549491: 'The root node is never visible' exception when details of the root node are modified "
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Date" anonid="date" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script>
+ <![CDATA[
+ /**
+ * Bug 549491
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=549491
+ *
+ * Ensures that changing the details of places tree's root-node doesn't
+ * throw.
+ */
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // The mochitest page is added to history.
+ waitForClearHistory(continue_test);
+ }
+
+ function continue_test() {
+ addVisits(
+ {uri: Services.io.newURI("http://example.tld/"),
+ visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ function() {
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ let rootNode = tree.result.root;
+ let obs = tree.view.QueryInterface(Ci.nsINavHistoryResultObserver);
+ obs.nodeHistoryDetailsChanged(rootNode, rootNode.time, rootNode.accessCount);
+ obs.nodeTitleChanged(rootNode, rootNode.title);
+ ok(true, "No exceptions thrown");
+
+ // Cleanup.
+ waitForClearHistory(SimpleTest.finish);
+ });
+ }
+
+ /**
+ * Clears history invoking callback when done.
+ */
+ function waitForClearHistory(aCallback) {
+ const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, TOPIC_EXPIRATION_FINISHED);
+ aCallback();
+ }
+ };
+ Services.obs.addObserver(observer, TOPIC_EXPIRATION_FINISHED);
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ hs.QueryInterface(Ci.nsIBrowserHistory).removeAllPages();
+ }
+
+ ]]></script>
+</window>
diff --git a/comm/suite/components/places/tests/chrome/test_treeview_date.xul b/comm/suite/components/places/tests/chrome/test_treeview_date.xul
new file mode 100644
index 0000000000..c390a66d2b
--- /dev/null
+++ b/comm/suite/components/places/tests/chrome/test_treeview_date.xul
@@ -0,0 +1,179 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://communicator/content/places/places.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/places/organizer.css"?>
+<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="435322: Places tree view's formatting"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Tags" id="tags" anonid="tags" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Url" id="url" anonid="url" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Date" id="date" anonid="date" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Count" id="visitCount" anonid="visitCount" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script>
+ <![CDATA[
+
+ /**
+ * Bug 435322
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=435322
+ *
+ * Ensures that date in places treeviews is correctly formatted.
+ */
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // The mochitest page is added to history.
+ waitForClearHistory(continue_test);
+ }
+
+ function continue_test() {
+
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ function uri(spec) {
+ return Services.io.newURI(spec);
+ }
+
+ var midnight = new Date();
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ function addVisitsCallback() {
+ // add a bookmark to the midnight visit
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri("http://at.midnight.com/"),
+ bs.DEFAULT_INDEX,
+ "A bookmark at midnight");
+ // Make a history query.
+ var query = hs.getNewQuery();
+ var opts = hs.getNewQueryOptions();
+ var queryURI = hs.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ var tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check formatting
+ var treeView = tree.view;
+ var rc = treeView.rowCount;
+ ok(rc >= 3, "Rows found");
+ var columns = tree.columns;
+ ok(columns.count > 0, "Columns found");
+ for (var r = 0; r < rc; r++) {
+ var node = treeView.nodeForTreeIndex(r);
+ ok(node, "Places node found");
+ for (var ci = 0; ci < columns.count; ci++) {
+ var c = columns.getColumnAt(ci);
+ var text = treeView.getCellText(r, c);
+ switch (c.element.getAttribute("anonid")) {
+ case "title":
+ // The title can differ, we did not set any title so we would
+ // expect null, but in such a case the view will generate a title
+ // through PlacesUIUtils.getBestTitle.
+ if (node.title)
+ is(text, node.title, "Title is correct");
+ break;
+ case "url":
+ is(text, node.uri, "Uri is correct");
+ break;
+ case "date":
+ var timeObj = new Date(node.time / 1000);
+ // Default is short date format.
+ let dtOptions = { dateStyle: "short", timeStyle: "short" };
+ // For today's visits we don't show date portion.
+ if (node.uri == "http://at.midnight.com/" ||
+ node.uri == "http://after.midnight.com/") {
+ dtOptions.dateStyle = undefined;
+ } else if (node.uri != "http://before.midnight.com/") {
+ // Avoid to test spurious uris, due to how the test works
+ // a redirecting uri could be put in the tree while we test.
+ break;
+ }
+ let timeStr = new Services.intl.DateTimeFormat(undefined, dtOptions).format(timeObj);
+ is(text, timeStr, "Date format is correct");
+ break;
+ case "visitCount":
+ is(text, 1, "Visit count is correct");
+ break;
+ }
+ }
+ }
+ // Cleanup.
+ bs.removeItem(itemId);
+ waitForClearHistory(SimpleTest.finish);
+ }
+
+ // Add a visit 1ms before midnight, a visit at midnight, and a visit 1ms
+ // after midnight.
+ addVisits(
+ [{uri: uri("http://before.midnight.com/"),
+ visitDate: (midnight.getTime() - 1) * 1000,
+ transition: hs.TRANSITION_TYPED},
+ {uri: uri("http://at.midnight.com/"),
+ visitDate: (midnight.getTime()) * 1000,
+ transition: hs.TRANSITION_TYPED},
+ {uri: uri("http://after.midnight.com/"),
+ visitDate: (midnight.getTime() + 1) * 1000,
+ transition: hs.TRANSITION_TYPED}],
+ addVisitsCallback);
+
+ }
+
+ /**
+ * Clears history invoking callback when done.
+ */
+ function waitForClearHistory(aCallback) {
+ const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, TOPIC_EXPIRATION_FINISHED);
+ aCallback();
+ }
+ };
+ Services.obs.addObserver(observer, TOPIC_EXPIRATION_FINISHED);
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ hs.QueryInterface(Ci.nsIBrowserHistory).removeAllPages();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/comm/suite/components/places/tests/head_common.js b/comm/suite/components/places/tests/head_common.js
new file mode 100644
index 0000000000..88ecb6d6ba
--- /dev/null
+++ b/comm/suite/components/places/tests/head_common.js
@@ -0,0 +1,868 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const CURRENT_SCHEMA_VERSION = 33;
+const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
+
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
+
+// Shortcuts to transitions type.
+const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
+const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
+const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
+const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
+const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
+const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT;
+const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY;
+const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
+const TRANSITION_RELOAD = Ci.nsINavHistoryService.TRANSITION_RELOAD;
+
+const TITLE_LENGTH_MAX = 4096;
+
+Cu.importGlobalProperties(["URL"]);
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+ChromeUtils.defineModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+ChromeUtils.defineModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "BookmarkHTMLUtils",
+ "resource://gre/modules/BookmarkHTMLUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesTransactions",
+ "resource://gre/modules/PlacesTransactions.jsm");
+ChromeUtils.defineModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+// This imports various other objects in addition to PlacesUtils.
+var {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
+ return NetUtil.newURI(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
+ "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
+});
+XPCOMUtils.defineLazyGetter(this, "SMALLSVG_DATA_URI", function() {
+ return NetUtil.newURI(
+ "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy5" +
+ "3My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBmaWxs" +
+ "PSIjNDI0ZTVhIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iN" +
+ "DQiIHN0cm9rZT0iIzQyNGU1YSIgc3Ryb2tlLXdpZHRoPSIxMSIgZmlsbD" +
+ "0ibm9uZSIvPg0KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjI0LjYiIHI9IjY" +
+ "uNCIvPg0KICA8cmVjdCB4PSI0NSIgeT0iMzkuOSIgd2lkdGg9IjEwLjEi" +
+ "IGhlaWdodD0iNDEuOCIvPg0KPC9zdmc%2BDQo%3D");
+});
+
+var gTestDir = do_get_cwd();
+
+// Initialize profile.
+var gProfD = do_get_profile();
+
+// Remove any old database.
+clearDB();
+
+/**
+ * Shortcut to create a nsIURI.
+ *
+ * @param aSpec
+ * URLString of the uri.
+ */
+function uri(aSpec) {
+ return NetUtil.newURI(aSpec);
+}
+
+
+/**
+ * Gets the database connection. If the Places connection is invalid it will
+ * try to create a new connection.
+ *
+ * @param [optional] aForceNewConnection
+ * Forces creation of a new connection to the database. When a
+ * connection is asyncClosed it cannot anymore schedule async statements,
+ * though connectionReady will keep returning true (Bug 726990).
+ *
+ * @return The database connection or null if unable to get one.
+ */
+var gDBConn;
+function DBConn(aForceNewConnection) {
+ if (!aForceNewConnection) {
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ if (db.connectionReady)
+ return db;
+ }
+
+ // If the Places database connection has been closed, create a new connection.
+ if (!gDBConn || aForceNewConnection) {
+ let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ file.append("places.sqlite");
+ let dbConn = gDBConn = Services.storage.openDatabase(file);
+
+ // Be sure to cleanly close this connection.
+ promiseTopicObserved("profile-before-change").then(() => dbConn.asyncClose());
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+/**
+ * Reads data from the provided inputstream.
+ *
+ * @return an array of bytes.
+ */
+function readInputStreamData(aStream) {
+ let bistream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ try {
+ bistream.setInputStream(aStream);
+ let expectedData = [];
+ let avail;
+ while ((avail = bistream.available())) {
+ expectedData = expectedData.concat(bistream.readByteArray(avail));
+ }
+ return expectedData;
+ } finally {
+ bistream.close();
+ }
+}
+
+/**
+ * Reads the data from the specified nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to read from.
+ * @return an array of bytes.
+ */
+function readFileData(aFile) {
+ let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ // init the stream as RD_ONLY, -1 == default permissions.
+ inputStream.init(aFile, 0x01, -1, null);
+
+ // Check the returned size versus the expected size.
+ let size = inputStream.available();
+ let bytes = readInputStreamData(inputStream);
+ if (size != bytes.length) {
+ throw "Didn't read expected number of bytes";
+ }
+ return bytes;
+}
+
+/**
+ * Reads the data from the named file, verifying the expected file length.
+ *
+ * @param aFileName
+ * This file should be located in the same folder as the test.
+ * @param aExpectedLength
+ * Expected length of the file.
+ *
+ * @return The array of bytes read from the file.
+ */
+function readFileOfLength(aFileName, aExpectedLength) {
+ let data = readFileData(do_get_file(aFileName));
+ Assert.equal(data.length, aExpectedLength);
+ return data;
+}
+
+
+/**
+ * Returns the base64-encoded version of the given string. This function is
+ * similar to window.btoa, but is available to xpcshell tests also.
+ *
+ * @param aString
+ * Each character in this string corresponds to a byte, and must be a
+ * code point in the range 0-255.
+ *
+ * @return The base64-encoded string.
+ */
+function base64EncodeString(aString) {
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(aString, aString.length);
+ var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
+ .createInstance(Ci.nsIScriptableBase64Encoder);
+ return encoder.encodeToString(stream, aString.length);
+}
+
+
+/**
+ * Compares two arrays, and returns true if they are equal.
+ *
+ * @param aArray1
+ * First array to compare.
+ * @param aArray2
+ * Second array to compare.
+ */
+function compareArrays(aArray1, aArray2) {
+ if (aArray1.length != aArray2.length) {
+ print("compareArrays: array lengths differ\n");
+ return false;
+ }
+
+ for (let i = 0; i < aArray1.length; i++) {
+ if (aArray1[i] != aArray2[i]) {
+ print("compareArrays: arrays differ at index " + i + ": " +
+ "(" + aArray1[i] + ") != (" + aArray2[i] +")\n");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+/**
+ * Deletes a previously created sqlite file from the profile folder.
+ */
+function clearDB() {
+ try {
+ let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ file.append("places.sqlite");
+ if (file.exists())
+ file.remove(false);
+ } catch (ex) { dump("Exception: " + ex); }
+}
+
+
+/**
+ * Dumps the rows of a table out to the console.
+ *
+ * @param aName
+ * The name of the table or view to output.
+ */
+function dump_table(aName)
+{
+ let stmt = DBConn().createStatement("SELECT * FROM " + aName);
+
+ print("\n*** Printing data from " + aName);
+ let count = 0;
+ while (stmt.executeStep()) {
+ let columns = stmt.numEntries;
+
+ if (count == 0) {
+ // Print the column names.
+ for (let i = 0; i < columns; i++)
+ dump(stmt.getColumnName(i) + "\t");
+ dump("\n");
+ }
+
+ // Print the rows.
+ for (let i = 0; i < columns; i++) {
+ switch (stmt.getTypeOfIndex(i)) {
+ case Ci.mozIStorageValueArray.VALUE_TYPE_NULL:
+ dump("NULL\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER:
+ dump(stmt.getInt64(i) + "\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT:
+ dump(stmt.getDouble(i) + "\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT:
+ dump(stmt.getString(i) + "\t");
+ break;
+ }
+ }
+ dump("\n");
+
+ count++;
+ }
+ print("*** There were a total of " + count + " rows of data.\n");
+
+ stmt.finalize();
+}
+
+
+/**
+ * Checks if an address is found in the database.
+ * @param aURI
+ * nsIURI or address to look for.
+ * @return place id of the page or 0 if not found
+ */
+function page_in_database(aURI)
+{
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url"
+ );
+ stmt.params.url = url;
+ try {
+ if (!stmt.executeStep())
+ return 0;
+ return stmt.getInt64(0);
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Checks how many visits exist for a specified page.
+ * @param aURI
+ * nsIURI or address to look for.
+ * @return number of visits found.
+ */
+function visits_in_database(aURI)
+{
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ `SELECT count(*) FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = url;
+ try {
+ if (!stmt.executeStep())
+ return 0;
+ return stmt.getInt64(0);
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Checks that we don't have any bookmark
+ */
+function check_no_bookmarks() {
+ let query = PlacesUtils.history.getNewQuery();
+ let folders = [
+ PlacesUtils.bookmarks.toolbarFolder,
+ PlacesUtils.bookmarks.bookmarksMenuFolder,
+ PlacesUtils.bookmarks.unfiledBookmarksFolder,
+ ];
+ query.setFolders(folders, 3);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ if (root.childCount != 0)
+ do_throw("Unable to remove all bookmarks");
+ root.containerOpen = false;
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [aSubject, aData] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic)
+{
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observe, aTopic);
+ resolve([aSubject, aData]);
+ }, aTopic);
+ });
+}
+
+/**
+ * Simulates a Places shutdown.
+ */
+var shutdownPlaces = function() {
+ info("shutdownPlaces: starting");
+ let promise = new Promise(resolve => {
+ Services.obs.addObserver(resolve, "places-connection-closed");
+ });
+ let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
+ hs.observe(null, "profile-change-teardown", null);
+ info("shutdownPlaces: sent profile-change-teardown");
+ hs.observe(null, "test-simulate-places-shutdown", null);
+ info("shutdownPlaces: sent test-simulate-places-shutdown");
+ return promise.then(() => {
+ info("shutdownPlaces: complete");
+ });
+};
+
+const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
+const FILENAME_BOOKMARKS_JSON = "bookmarks-" +
+ (PlacesBackups.toISODateString(new Date())) + ".json";
+
+/**
+ * Creates a bookmarks.html file in the profile folder from a given source file.
+ *
+ * @param aFilename
+ * Name of the file to copy to the profile folder. This file must
+ * exist in the directory that contains the test files.
+ *
+ * @return nsIFile object for the file.
+ */
+function create_bookmarks_html(aFilename) {
+ if (!aFilename)
+ do_throw("you must pass a filename to create_bookmarks_html function");
+ remove_bookmarks_html();
+ let bookmarksHTMLFile = gTestDir.clone();
+ bookmarksHTMLFile.append(aFilename);
+ Assert.ok(bookmarksHTMLFile.exists());
+ bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML);
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ Assert.ok(profileBookmarksHTMLFile.exists());
+ return profileBookmarksHTMLFile;
+}
+
+
+/**
+ * Remove bookmarks.html file from the profile folder.
+ */
+function remove_bookmarks_html() {
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ if (profileBookmarksHTMLFile.exists()) {
+ profileBookmarksHTMLFile.remove(false);
+ Assert.ok(!profileBookmarksHTMLFile.exists());
+ }
+}
+
+
+/**
+ * Check bookmarks.html file exists in the profile folder.
+ *
+ * @return nsIFile object for the file.
+ */
+function check_bookmarks_html() {
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ Assert.ok(profileBookmarksHTMLFile.exists());
+ return profileBookmarksHTMLFile;
+}
+
+
+/**
+ * Creates a JSON backup in the profile folder folder from a given source file.
+ *
+ * @param aFilename
+ * Name of the file to copy to the profile folder. This file must
+ * exist in the directory that contains the test files.
+ *
+ * @return nsIFile object for the file.
+ */
+function create_JSON_backup(aFilename) {
+ if (!aFilename)
+ do_throw("you must pass a filename to create_JSON_backup function");
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ if (!bookmarksBackupDir.exists()) {
+ bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ Assert.ok(bookmarksBackupDir.exists());
+ }
+ let profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ if (profileBookmarksJSONFile.exists()) {
+ profileBookmarksJSONFile.remove();
+ }
+ let bookmarksJSONFile = gTestDir.clone();
+ bookmarksJSONFile.append(aFilename);
+ Assert.ok(bookmarksJSONFile.exists());
+ bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
+ profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ Assert.ok(profileBookmarksJSONFile.exists());
+ return profileBookmarksJSONFile;
+}
+
+
+/**
+ * Remove bookmarksbackup dir and all backups from the profile folder.
+ */
+function remove_all_JSON_backups() {
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ if (bookmarksBackupDir.exists()) {
+ bookmarksBackupDir.remove(true);
+ Assert.ok(!bookmarksBackupDir.exists());
+ }
+}
+
+
+/**
+ * Check a JSON backup file for today exists in the profile folder.
+ *
+ * @param aIsAutomaticBackup The boolean indicates whether it's an automatic
+ * backup.
+ * @return nsIFile object for the file.
+ */
+function check_JSON_backup(aIsAutomaticBackup) {
+ let profileBookmarksJSONFile;
+ if (aIsAutomaticBackup) {
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ let files = bookmarksBackupDir.directoryEntries;
+ let backup_date = PlacesBackups.toISODateString(new Date());
+ while (files.hasMoreElements()) {
+ let entry = files.nextFile;
+ if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
+ profileBookmarksJSONFile = entry;
+ break;
+ }
+ }
+ } else {
+ profileBookmarksJSONFile = gProfD.clone();
+ profileBookmarksJSONFile.append("bookmarkbackups");
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ }
+ Assert.ok(profileBookmarksJSONFile.exists());
+ return profileBookmarksJSONFile;
+}
+
+/**
+ * Returns the frecency of a url.
+ *
+ * @param aURI
+ * The URI or spec to get frecency for.
+ * @return the frecency value.
+ */
+function frecencyForUrl(aURI)
+{
+ let url = aURI;
+ if (aURI instanceof Ci.nsIURI) {
+ url = aURI.spec;
+ } else if (aURI instanceof URL) {
+ url = aURI.href;
+ }
+ let stmt = DBConn().createStatement(
+ "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ try {
+ if (!stmt.executeStep()) {
+ throw new Error("No result for frecency.");
+ }
+ return stmt.getInt32(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Returns the hidden status of a url.
+ *
+ * @param aURI
+ * The URI or spec to get hidden for.
+ * @return @return true if the url is hidden, false otherwise.
+ */
+function isUrlHidden(aURI)
+{
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ "SELECT hidden FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ if (!stmt.executeStep())
+ throw new Error("No result for hidden.");
+ let hidden = stmt.getInt32(0);
+ stmt.finalize();
+
+ return !!hidden;
+}
+
+/**
+ * Compares two times in usecs, considering eventual platform timers skews.
+ *
+ * @param aTimeBefore
+ * The older time in usecs.
+ * @param aTimeAfter
+ * The newer time in usecs.
+ * @return true if times are ordered, false otherwise.
+ */
+function is_time_ordered(before, after) {
+ // Windows has an estimated 16ms timers precision, since Date.now() and
+ // PR_Now() use different code atm, the results can be unordered by this
+ // amount of time. See bug 558745 and bug 557406.
+ let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
+ // Just to be safe we consider 20ms.
+ let skew = isWindows ? 20000000 : 0;
+ return after - before > -skew;
+}
+
+/**
+ * Shutdowns Places, invoking the callback when the connection has been closed.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ */
+function waitForConnectionClosed(aCallback)
+{
+ promiseTopicObserved("places-connection-closed").then(aCallback);
+ shutdownPlaces();
+}
+
+/**
+ * Tests if a given guid is valid for use in Places or not.
+ *
+ * @param aGuid
+ * The guid to test.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ */
+function do_check_valid_places_guid(aGuid,
+ aStack)
+{
+ if (!aStack) {
+ aStack = Components.stack.caller;
+ }
+ Assert.ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack);
+}
+
+/**
+ * Retrieves the guid for a given uri.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ * @return the associated the guid.
+ */
+function do_get_guid_for_uri(aURI,
+ aStack)
+{
+ if (!aStack) {
+ aStack = Components.stack.caller;
+ }
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = aURI.spec;
+ Assert.ok(stmt.executeStep(), aStack);
+ let guid = stmt.row.guid;
+ stmt.finalize();
+ do_check_valid_places_guid(guid, aStack);
+ return guid;
+}
+
+/**
+ * Tests that a guid was set in moz_places for a given uri.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param [optional] aGUID
+ * The expected guid in the database.
+ */
+function do_check_guid_for_uri(aURI,
+ aGUID)
+{
+ let caller = Components.stack.caller;
+ let guid = do_get_guid_for_uri(aURI, caller);
+ if (aGUID) {
+ do_check_valid_places_guid(aGUID, caller);
+ Assert.equal(guid, aGUID, caller);
+ }
+}
+
+/**
+ * Retrieves the guid for a given bookmark.
+ *
+ * @param aId
+ * The bookmark id to check.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ * @return the associated the guid.
+ */
+function do_get_guid_for_bookmark(aId,
+ aStack)
+{
+ if (!aStack) {
+ aStack = Components.stack.caller;
+ }
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_bookmarks
+ WHERE id = :item_id`
+ );
+ stmt.params.item_id = aId;
+ Assert.ok(stmt.executeStep(), aStack);
+ let guid = stmt.row.guid;
+ stmt.finalize();
+ do_check_valid_places_guid(guid, aStack);
+ return guid;
+}
+
+/**
+ * Tests that a guid was set in moz_places for a given bookmark.
+ *
+ * @param aId
+ * The bookmark id to check.
+ * @param [optional] aGUID
+ * The expected guid in the database.
+ */
+function do_check_guid_for_bookmark(aId,
+ aGUID)
+{
+ let caller = Components.stack.caller;
+ let guid = do_get_guid_for_bookmark(aId, caller);
+ if (aGUID) {
+ do_check_valid_places_guid(aGUID, caller);
+ Assert.equal(guid, aGUID, caller);
+ }
+}
+
+/**
+ * Compares 2 arrays returning whether they contains the same elements.
+ *
+ * @param a1
+ * First array to compare.
+ * @param a2
+ * Second array to compare.
+ * @param [optional] sorted
+ * Whether the comparison should take in count position of the elements.
+ * @return true if the arrays contain the same elements, false otherwise.
+ */
+function do_compare_arrays(a1, a2, sorted)
+{
+ if (a1.length != a2.length)
+ return false;
+
+ if (sorted) {
+ return a1.every((e, i) => e == a2[i]);
+ }
+ return a1.filter(e => !a2.includes(e)).length == 0 &&
+ a2.filter(e => !a1.includes(e)).length == 0;
+}
+
+/**
+ * Generic nsINavBookmarkObserver that doesn't implement anything, but provides
+ * dummy methods to prevent errors about an object not having a certain method.
+ */
+function NavBookmarkObserver() {}
+
+NavBookmarkObserver.prototype = {
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemAdded: function () {},
+ onItemRemoved: function () {},
+ onItemChanged: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ])
+};
+
+/**
+ * Generic nsINavHistoryObserver that doesn't implement anything, but provides
+ * dummy methods to prevent errors about an object not having a certain method.
+ */
+function NavHistoryObserver() {}
+
+NavHistoryObserver.prototype = {
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function () {},
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavHistoryObserver,
+ ])
+};
+
+/**
+ * Generic nsINavHistoryResultObserver that doesn't implement anything, but
+ * provides dummy methods to prevent errors about an object not having a certain
+ * method.
+ */
+function NavHistoryResultObserver() {}
+
+NavHistoryResultObserver.prototype = {
+ batching: function () {},
+ containerStateChanged: function () {},
+ invalidateContainer: function () {},
+ nodeAnnotationChanged: function () {},
+ nodeDateAddedChanged: function () {},
+ nodeHistoryDetailsChanged: function () {},
+ nodeIconChanged: function () {},
+ nodeInserted: function () {},
+ nodeKeywordChanged: function () {},
+ nodeLastModifiedChanged: function () {},
+ nodeMoved: function () {},
+ nodeRemoved: function () {},
+ nodeTagsChanged: function () {},
+ nodeTitleChanged: function () {},
+ nodeURIChanged: function () {},
+ sortingChanged: function () {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavHistoryResultObserver,
+ ])
+};
+
+/**
+ * Asynchronously check a url is visited.
+ *
+ * @param aURI The URI.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Asynchronously set the favicon associated with a page.
+ * @param aPageURI
+ * The page's URI
+ * @param aIconURI
+ * The URI of the favicon to be set.
+ */
+function promiseSetIconForPage(aPageURI, aIconURI) {
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ aPageURI, aIconURI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ () => { deferred.resolve(); },
+ Services.scriptSecurityManager.getSystemPrincipal());
+ return deferred.promise;
+}
+
+function checkBookmarkObject(info) {
+ do_check_valid_places_guid(info.guid);
+ do_check_valid_places_guid(info.parentGuid);
+ Assert.ok(typeof info.index == "number", "index should be a number");
+ Assert.ok(info.dateAdded.constructor.name == "Date", "dateAdded should be a Date");
+ Assert.ok(info.lastModified.constructor.name == "Date", "lastModified should be a Date");
+ Assert.ok(info.lastModified >= info.dateAdded, "lastModified should never be smaller than dateAdded");
+ Assert.ok(typeof info.type == "number", "type should be a number");
+}
+
+/**
+ * Reads foreign_count value for a given url.
+ */
+async function foreign_count(url) {
+ if (url instanceof Ci.nsIURI)
+ url = url.spec;
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ `SELECT foreign_count FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url
+ `, { url });
+ return rows.length == 0 ? 0 : rows[0].getResultByName("foreign_count");
+}
diff --git a/comm/suite/components/places/tests/unit/bookmarks.glue.html b/comm/suite/components/places/tests/unit/bookmarks.glue.html
new file mode 100644
index 0000000000..07b22e9b3f
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/bookmarks.glue.html
@@ -0,0 +1,16 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ <DT><H3 ADD_DATE="1233157910" LAST_MODIFIED="1233157972" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ </DL><p>
+</DL><p>
diff --git a/comm/suite/components/places/tests/unit/bookmarks.glue.json b/comm/suite/components/places/tests/unit/bookmarks.glue.json
new file mode 100644
index 0000000000..8ca855ad42
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/bookmarks.glue.json
@@ -0,0 +1 @@
+{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]} \ No newline at end of file
diff --git a/comm/suite/components/places/tests/unit/corruptDB.sqlite b/comm/suite/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/comm/suite/components/places/tests/unit/distribution.ini b/comm/suite/components/places/tests/unit/distribution.ini
new file mode 100644
index 0000000000..f94a1be3c5
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/distribution.ini
@@ -0,0 +1,21 @@
+# Distribution Configuration File
+# Bug 516444 demo
+
+[Global]
+id=516444
+version=1.0
+about=Test distribution file
+
+[BookmarksToolbar]
+item.1.title=Toolbar Link Before
+item.1.link=http://mozilla.com/
+item.2.type=default
+item.3.title=Toolbar Link After
+item.3.link=http://mozilla.com/
+
+[BookmarksMenu]
+item.1.title=Menu Link Before
+item.1.link=http://mozilla.com/
+item.2.type=default
+item.3.title=Menu Link After
+item.3.link=http://mozilla.com/ \ No newline at end of file
diff --git a/comm/suite/components/places/tests/unit/head_bookmarks.js b/comm/suite/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 0000000000..0a795620fd
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+var commonFile = do_get_file("../head_common.js", false);
+var uri = Services.io.newFileURI(commonFile);
+Services.scriptloader.loadSubScript(uri.spec, this);
+
+// Put any other stuff relative to this test folder below.
+
+
+XPCOMUtils.defineLazyGetter(this, "PlacesUIUtils", function() {
+ const {PlacesUIUtils} = ChromeUtils.import("resource:///modules/PlacesUIUtils.jsm");
+ return PlacesUIUtils;
+});
+
+
+const ORGANIZER_FOLDER_ANNO = "PlacesOrganizer/OrganizerFolder";
+const ORGANIZER_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
+
+
+// Needed by some test that relies on having an app registered.
+ChromeUtils.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "PlacesTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+// Smart bookmarks constants.
+const SMART_BOOKMARKS_VERSION = 4;
+// 1 = "Most Visited".
+const SMART_BOOKMARKS_ON_TOOLBAR = 1;
+// 3 = "Recently Bookmarked", "Recent Tags", separator.
+const SMART_BOOKMARKS_ON_MENU = 3; // Takes in count the additional separator.
+
+// Default bookmarks constants.
+// 4 = "SeaMonkey", "mozilla.org", "mozillaZine".
+const DEFAULT_BOOKMARKS_ON_TOOLBAR = 3;
+// 2 = "SeaMonkey and Mozilla", "Search the Web".
+const DEFAULT_BOOKMARKS_ON_MENU = 3; // Takes in count the additional separator.
diff --git a/comm/suite/components/places/tests/unit/test_421483.js b/comm/suite/components/places/tests/unit/test_421483.js
new file mode 100644
index 0000000000..5315617e58
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_421483.js
@@ -0,0 +1,84 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Get bookmarks service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+} catch(ex) {
+ do_throw("Could not get Bookmarks service\n");
+}
+
+// Get annotation service
+try {
+ var annosvc = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+} catch(ex) {
+ do_throw("Could not get Annotation service\n");
+}
+
+// Get browser glue
+try {
+ var gluesvc = Cc["@mozilla.org/suite/suiteglue;1"].
+ getService(Ci.nsISuiteGlue);
+ // Avoid default bookmarks import.
+ gluesvc.QueryInterface(Ci.nsIObserver).observe(null, "initial-migration", null);
+} catch(ex) {
+ do_throw("Could not get SuiteGlue service\n");
+}
+
+const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
+const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
+
+// main
+function run_test() {
+ // TEST 1: smart bookmarks disabled
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+ gluesvc.ensurePlacesDefaultQueriesInitialized();
+ var smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, 0);
+ // check that pref has not been bumped up
+ Assert.equal(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
+
+ // TEST 2: create smart bookmarks
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ gluesvc.ensurePlacesDefaultQueriesInitialized();
+ smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.notEqual(smartBookmarkItemIds.length, 0);
+ // check that pref has been bumped up
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+
+ var smartBookmarksCount = smartBookmarkItemIds.length;
+
+ // TEST 3: smart bookmarks restore
+ // remove one smart bookmark and restore
+ bmsvc.removeItem(smartBookmarkItemIds[0]);
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ gluesvc.ensurePlacesDefaultQueriesInitialized();
+ smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+ // check that pref has been bumped up
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+
+ // TEST 4: move a smart bookmark, change its title, then restore
+ // smart bookmark should be restored in place
+ var parent = bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]);
+ var oldTitle = bmsvc.getItemTitle(smartBookmarkItemIds[0]);
+ // create a subfolder and move inside it
+ var newParent = bmsvc.createFolder(parent, "test", bmsvc.DEFAULT_INDEX);
+ bmsvc.moveItem(smartBookmarkItemIds[0], newParent, bmsvc.DEFAULT_INDEX);
+ // change title
+ bmsvc.setItemTitle(smartBookmarkItemIds[0], "new title");
+ // restore
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ gluesvc.ensurePlacesDefaultQueriesInitialized();
+ smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+ Assert.equal(bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
+ Assert.equal(bmsvc.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
+ // check that pref has been bumped up
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+}
diff --git a/comm/suite/components/places/tests/unit/test_PUIU_makeTransaction.js b/comm/suite/components/places/tests/unit/test_PUIU_makeTransaction.js
new file mode 100644
index 0000000000..ce13f4e2ee
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_PUIU_makeTransaction.js
@@ -0,0 +1,355 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function waitForBookmarkNotification(aNotification, aCallback, aProperty)
+{
+ PlacesUtils.bookmarks.addObserver({
+ validate: function (aMethodName, aData)
+ {
+ if (aMethodName == aNotification &&
+ (!aProperty || aProperty == aData.property)) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ aCallback(aData);
+ }
+ },
+
+ // nsINavBookmarkObserver
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+ onBeginUpdateBatch: function onBeginUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onEndUpdateBatch: function onEndUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI, aTitle)
+ {
+ return this.validate(arguments.callee.name, { id: aItemId,
+ index: aIndex,
+ type: aItemType,
+ url: aURI ? aURI.spec : null,
+ title: aTitle });
+ },
+ onItemRemoved: function onItemRemoved() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemChanged: function onItemChanged(aItemId, aProperty, aIsAnno,
+ aNewValue, aLastModified, aItemType)
+ {
+ return this.validate(arguments.callee.name,
+ { id: aItemId,
+ get index() { return PlacesUtils.bookmarks.getItemIndex(this.id); },
+ type: aItemType,
+ property: aProperty,
+ get url() { return aItemType == PlacesUtils.bookmarks.TYPE_BOOKMARK ?
+ PlacesUtils.bookmarks.getBookmarkURI(this.id).spec :
+ null; },
+ get title() { return PlacesUtils.bookmarks.getItemTitle(this.id); },
+ });
+ },
+ onItemVisited: function onItemVisited() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemMoved: function onItemMoved(aItemId, aOldParentId, aOldIndex,
+ aNewParentId, aNewIndex, aItemType)
+ {
+ this.validate(arguments.callee.name, { id: aItemId,
+ index: aNewIndex,
+ type: aItemType });
+ }
+ });
+}
+
+function wrapNodeByIdAndParent(aItemId, aParentId)
+{
+ let wrappedNode;
+ let root = PlacesUtils.getFolderContents(aParentId, false, false).root;
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ if (node.itemId == aItemId) {
+ let type;
+ if (PlacesUtils.nodeIsContainer(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+ }
+ else if (PlacesUtils.nodeIsURI(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ }
+ else if (PlacesUtils.nodeIsSeparator(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ }
+ else {
+ do_throw("Unknown node type");
+ }
+ wrappedNode = PlacesUtils.wrapNode(node, type);
+ }
+ }
+ root.containerOpen = false;
+ return JSON.parse(wrappedNode);
+}
+
+add_test(function test_text_paste()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.url, TEST_URL);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 0);
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ { title: TEST_TITLE, uri: TEST_URL },
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true // Unused for text.
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+});
+
+add_test(function test_container()
+{
+ const TEST_TITLE = "Places folder"
+
+ waitForBookmarkNotification("onItemChanged", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(aData.index, 1);
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(aData.index, 2);
+ let id = aData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aData)
+ {
+ Assert.equal(aData.id, id);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(aData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch(ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+
+add_test(function test_separator()
+{
+ waitForBookmarkNotification("onItemChanged", function(aData)
+ {
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.equal(aData.index, 3);
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.equal(aData.index, 4);
+ let id = aData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aData)
+ {
+ Assert.equal(aData.id, id);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.equal(aData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch(ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_bookmark()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemChanged", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.url, TEST_URL);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 5);
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.url, TEST_URL);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 6);
+ let id = aData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aData)
+ {
+ Assert.equal(aData.id, id);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch(ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_visit()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.url, TEST_URL);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 7);
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ Assert.equal(aData.title, TEST_TITLE);
+ Assert.equal(aData.url, TEST_URL);
+ Assert.equal(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(aData.index, 8);
+ run_next_test();
+ });
+
+ try {
+ let node = wrapNodeByIdAndParent(aData.id, PlacesUtils.unfiledBookmarksFolderId);
+ // Simulate a not-bookmarked node, will copy it to a new bookmark.
+ node.id = -1;
+ let txn = PlacesUIUtils.makeTransaction(
+ node,
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch(ex) {
+ do_throw(ex);
+ }
+ });
+
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+});
+
+add_test(function check_annotations() {
+ // As last step check how many items for each annotation exist.
+
+ // Copies should retain the description annotation.
+ let descriptions =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.DESCRIPTION_ANNO, {});
+ Assert.equal(descriptions.length, 4);
+
+ // Only the original bookmarks should have this annotation.
+ let others = PlacesUtils.annotations.getItemsWithAnnotation("random-anno", {});
+ Assert.equal(others.length, 3);
+ run_next_test();
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_corrupt.js b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt.js
new file mode 100644
index 0000000000..b0a3e6fb67
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt.js
@@ -0,0 +1,85 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue correctly restores bookmarks from a JSON backup if
+ * database is corrupt and one backup is available.
+ */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "bs",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+XPCOMUtils.defineLazyServiceGetter(this, "anno",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.notEqual(itemId, -1);
+ if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
+ continue_test();
+ },
+ onItemAdded: function() {},
+ onItemRemoved: function(id, folder, index, itemType) {},
+ onItemChanged: function() {},
+ onItemVisited: function(id, visitID, time) {},
+ onItemMoved: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Create our bookmarks.html copying bookmarks.glue.html to the profile
+ // folder. It should be ignored.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Create our JSON backup copying bookmarks.glue.json to the profile folder.
+ create_JSON_backup("bookmarks.glue.json");
+
+ // Remove current database file.
+ let db = gProfD.clone();
+ db.append("places.sqlite");
+ if (db.exists()) {
+ db.remove(false);
+ Assert.ok(!db.exists());
+ }
+ // Create a corrupt database.
+ let corruptDB = gTestDir.clone();
+ corruptDB.append("corruptDB.sqlite");
+ corruptDB.copyTo(gProfD, "places.sqlite");
+ Assert.ok(db.exists());
+
+ // Initialize nsSuiteGlue before Places.
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsISuiteGlue);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check the database was corrupt.
+ // nsSuiteGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ bs.addObserver(bookmarksObserver);
+}
+
+function continue_test() {
+ // Check that JSON backup has been restored.
+ // Notice restore from JSON notification is fired before smart bookmarks creation.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(bs.getItemTitle(itemId), "examplejson");
+
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ do_test_finished();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
new file mode 100644
index 0000000000..aa6ec0e716
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
@@ -0,0 +1,81 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue correctly imports from bookmarks.html if database
+ * is corrupt but a JSON backup is not available.
+ */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "bs",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+XPCOMUtils.defineLazyServiceGetter(this, "anno",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.notEqual(itemId, -1);
+ if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
+ continue_test();
+ },
+ onItemAdded: function() {},
+ onItemRemoved: function(id, folder, index, itemType) {},
+ onItemChanged: function() {},
+ onItemVisited: function(id, visitID, time) {},
+ onItemMoved: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Create bookmarks.html in the profile.
+ create_bookmarks_html("bookmarks.glue.html");
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ // Remove current database file.
+ let db = gProfD.clone();
+ db.append("places.sqlite");
+ if (db.exists()) {
+ db.remove(false);
+ Assert.ok(!db.exists());
+ }
+ // Create a corrupt database.
+ let corruptDB = gTestDir.clone();
+ corruptDB.append("corruptDB.sqlite");
+ corruptDB.copyTo(gProfD, "places.sqlite");
+ Assert.ok(db.exists());
+
+ // Initialize nsSuiteGlue before Places.
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsISuiteGlue);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check the database was corrupt.
+ // nsSuiteGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ bs.addObserver(bookmarksObserver);
+}
+
+function continue_test() {
+ // Check that bookmarks html has been restored.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(bs.getItemTitle(itemId), "example");
+
+ remove_bookmarks_html();
+
+ do_test_finished();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
new file mode 100644
index 0000000000..54a6bc829f
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
@@ -0,0 +1,80 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue correctly restores default bookmarks if database is
+ * corrupt, nor a JSON backup nor bookmarks.html are available.
+ */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "bs",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+XPCOMUtils.defineLazyServiceGetter(this, "anno",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.notEqual(itemId, -1);
+ if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
+ continue_test();
+ },
+ onItemAdded: function() {},
+ onItemRemoved: function(id, folder, index, itemType) {},
+ onItemChanged: function() {},
+ onItemVisited: function(id, visitID, time) {},
+ onItemMoved: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Remove bookmarks.html from profile.
+ remove_bookmarks_html();
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ // Remove current database file.
+ let db = gProfD.clone();
+ db.append("places.sqlite");
+ if (db.exists()) {
+ db.remove(false);
+ Assert.ok(!db.exists());
+ }
+ // Create a corrupt database.
+ let corruptDB = gTestDir.clone();
+ corruptDB.append("corruptDB.sqlite");
+ corruptDB.copyTo(gProfD, "places.sqlite");
+ Assert.ok(db.exists());
+
+ // Initialize nsSuiteGlue before Places.
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsISuiteGlue);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check the database was corrupt.
+ // nsSuiteGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ bs.addObserver(bookmarksObserver);
+}
+
+function continue_test() {
+ // Check that default bookmarks have been restored.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.ok(itemId > 0);
+ Assert.equal(bs.getItemTitle(itemId), "SeaMonkey");
+
+ do_test_finished();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_distribution.js b/comm/suite/components/places/tests/unit/test_browserGlue_distribution.js
new file mode 100644
index 0000000000..8c12711b6a
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_distribution.js
@@ -0,0 +1,124 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue does not overwrite bookmarks imported from the
+ * migrators. They usually run before nsSuiteGlue, so if we find any
+ * bookmark on init, we should not try to import.
+ */
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+
+const TOPIC_FINAL_UI_STARTUP = "final-ui-startup";
+const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete";
+
+function run_test() {
+ // This is needed but we still have to investigate the reason, could just be
+ // we try to act too late in the game, moving our shutdown earlier will help.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // TODO: re-enable when bug 523936 is fixed.
+ return;
+
+ do_test_pending();
+
+ // Copy distribution.ini file to our app dir.
+ let distroDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ distroDir.append("distribution");
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ print("distribution.ini already exists, did some test forget to cleanup?");
+ }
+
+ let testDistributionFile = gTestDir.clone();
+ testDistributionFile.append("distribution.ini");
+ testDistributionFile.copyTo(distroDir, "distribution.ini");
+ do_check_true(testDistributionFile.exists());
+
+ // Disable Smart Bookmarks creation.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
+ // Avoid migrateUI, we are just simulating a partial startup.
+ Services.prefs.setIntPref("browser.migration.version", 1);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check a new database has been created.
+ // nsSuiteGlue will use databaseStatus to manage initialization.
+ do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // Initialize nsSuiteGlue.
+ let bg = Cc["@mozilla.org/suite/suiteglue;1"].
+ getService(Ci.nsISuiteGlue);
+
+ let os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ os.removeObserver(this, PlacesUtils.TOPIC_INIT_COMPLETE);
+
+ // Simulate browser startup.
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ TOPIC_FINAL_UI_STARTUP,
+ null);
+ // Test will continue on customization complete notification.
+ let cObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ os.removeObserver(this, TOPIC_CUSTOMIZATION_COMPLETE);
+ do_execute_soon(continue_test);
+ }
+ }
+ os.addObserver(cObserver, TOPIC_CUSTOMIZATION_COMPLETE, false);
+ }
+ }
+ os.addObserver(observer, PlacesUtils.TOPIC_INIT_COMPLETE, false);
+}
+
+function continue_test() {
+ let bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ dump_table("moz_bookmarks");
+
+ // Check the custom bookmarks exist on menu.
+ let menuItemId = bs.getIdForItemAt(bs.bookmarksMenuFolder, 0);
+ do_check_neq(menuItemId, -1);
+ do_check_eq(bs.getItemTitle(menuItemId), "Menu Link Before");
+ menuItemId = bs.getIdForItemAt(bs.bookmarksMenuFolder, 1 + DEFAULT_BOOKMARKS_ON_MENU);
+ do_check_neq(menuItemId, -1);
+ do_check_eq(bs.getItemTitle(menuItemId), "Menu Link After");
+
+ // Check the custom bookmarks exist on toolbar.
+ let toolbarItemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ do_check_neq(toolbarItemId, -1);
+ do_check_eq(bs.getItemTitle(toolbarItemId), "Toolbar Link Before");
+ toolbarItemId = bs.getIdForItemAt(bs.toolbarFolder, 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ do_check_neq(toolbarItemId, -1);
+ do_check_eq(bs.getItemTitle(toolbarItemId), "Toolbar Link After");
+
+ // Check the bmprocessed pref has been created.
+ do_check_true(Services.prefs.getBoolPref(PREF_BMPROCESSED));
+
+ // Check distribution prefs have been created.
+ do_check_eq(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
+
+ do_test_finished();
+}
+
+do_register_cleanup(function() {
+ // Remove the distribution file, even if the test failed, otherwise all
+ // next tests will import it.
+ let iniFile = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ iniFile.append("distribution");
+ iniFile.append("distribution.ini");
+ if (iniFile.exists())
+ iniFile.remove(false);
+ do_check_false(iniFile.exists());
+});
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_migrate.js b/comm/suite/components/places/tests/unit/test_browserGlue_migrate.js
new file mode 100644
index 0000000000..8568454f04
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_migrate.js
@@ -0,0 +1,89 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue does not overwrite bookmarks imported from the
+ * migrators. They usually run before nsSuiteGlue, so if we find any
+ * bookmark on init, we should not try to import.
+ */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "bs",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+XPCOMUtils.defineLazyServiceGetter(this, "anno",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.notEqual(itemId, -1);
+ if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
+ continue_test();
+ },
+ onItemAdded: function() {},
+ onItemRemoved: function(id, folder, index, itemType) {},
+ onItemChanged: function() {},
+ onItemVisited: function(id, visitID, time) {},
+ onItemMoved: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+
+function run_test() {
+ do_test_pending();
+
+ // Create our bookmarks.html copying bookmarks.glue.html to the profile
+ // folder. It will be ignored.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove current database file.
+ let db = gProfD.clone();
+ db.append("places.sqlite");
+ if (db.exists()) {
+ db.remove(false);
+ Assert.ok(!db.exists());
+ }
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check a new database has been created.
+ // nsSuiteGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // A migrator would run before nsSuiteGlue Places initialization, so mimic
+ // that behavior adding a bookmark and notifying the migration.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://mozilla.org/"),
+ bs.DEFAULT_INDEX, "migrated");
+
+ // Initialize nsSuiteGlue.
+ let bg = Cc["@mozilla.org/suite/suiteglue;1"].
+ getService(Ci.nsIObserver);
+ bg.observe(null, "initial-migration", null)
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ bs.addObserver(bookmarksObserver);
+}
+
+function continue_test() {
+ // Check the created bookmarks still exist.
+ let itemId = bs.getIdForItemAt(bs.bookmarksMenuFolder, SMART_BOOKMARKS_ON_MENU);
+ Assert.equal(bs.getItemTitle(itemId), "migrated");
+
+ // Check that we have not imported any new bookmark.
+ Assert.equal(bs.getIdForItemAt(bs.bookmarksMenuFolder, SMART_BOOKMARKS_ON_MENU + 1), -1);
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_MENU), -1);
+
+ remove_bookmarks_html();
+
+ do_test_finished();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_prefs.js b/comm/suite/components/places/tests/unit/test_browserGlue_prefs.js
new file mode 100644
index 0000000000..49610d1476
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_prefs.js
@@ -0,0 +1,272 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+/** Bug 539067
+ * Test is disabled due to random failures and timeouts, see run_test.
+ * This is commented out to avoid leaks.
+// Initialize SuiteGlue.
+var bg = Cc["@mozilla.org/suite/suiteglue;1"].
+ getService(Ci.nsISuiteGlue);
+*/
+
+// Initialize Places through Bookmarks Service.
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+// Get other services.
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+
+function waitForImportAndSmartBookmarks(aCallback) {
+ Services.obs.addObserver(function waitImport() {
+ Services.obs.removeObserver(waitImport, "bookmarks-restore-success");
+ // Delay to test eventual smart bookmarks creation.
+ executeSoon(function () {
+ promiseAsyncUpdates().then(aCallback);
+ });
+ }, "bookmarks-restore-success");
+}
+
+var tests = [];
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Import from bookmarks.html if importBookmarksHTML is true.",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been imported, and a smart bookmark has been
+ // created.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder,
+ SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(bs.getItemTitle(itemId), "example");
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+ next_test();
+ });
+ // Force nsSuiteGlue::_initPlaces().
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "import from bookmarks.html, but don't create smart bookmarks if they are disabled",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.equal(bs.getItemTitle(itemId), "example");
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+ next_test();
+ });
+ // Force nsSuiteGlue::_initPlaces().
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Import from bookmarks.html, but don't create smart bookmarks if autoExportHTML is true and they are at latest version",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999);
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.equal(bs.getItemTitle(itemId), "example");
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+ // Check preferences have been reverted.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+
+ next_test();
+ });
+ // Force nsSuiteGlue::_initPlaces()
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Import from bookmarks.html, and create smart bookmarks if autoExportHTML is true and they are not at latest version.",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(bs.getItemTitle(itemId), "example");
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+ // Check preferences have been reverted.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+
+ next_test();
+ });
+ // Force nsSuiteGlue::_initPlaces()
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+tests.push({
+ description: "restore from default bookmarks.html if restore_default_bookmarks is true.",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been restored.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR + 1);
+ Assert.ok(itemId > 0);
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+
+ next_test();
+ });
+ // Force nsSuiteGlue::_initPlaces()
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "setting both importBookmarksHTML and restore_default_bookmarks should restore defaults.",
+ exec: function() {
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.equal(bs.getIdForItemAt(bs.toolbarFolder, 0), -1);
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ waitForImportAndSmartBookmarks(function () {
+ // Check bookmarks.html has been restored.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR + 1);
+ Assert.ok(itemId > 0);
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+ do_test_finished();
+ });
+ // Force nsSuiteGlue::_initPlaces()
+ do_log_info("Simulate Places init");
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_INIT_COMPLETE,
+ null);
+ }
+});
+
+//------------------------------------------------------------------------------
+
+function finish_test() {
+ // Clean up database from all bookmarks.
+ remove_all_bookmarks();
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ do_test_finished();
+}
+var testIndex = 0;
+function next_test() {
+ // Clean up database from all bookmarks.
+ remove_all_bookmarks();
+ // nsSuiteGlue stops observing topics after first notification,
+ // so we add back the observer to test additional runs.
+ Services.obs.addObserver(bg.QueryInterface(Ci.nsIObserver),
+ PlacesUtils.TOPIC_INIT_COMPLETE);
+ Services.obs.addObserver(bg.QueryInterface(Ci.nsIObserver),
+ PlacesUtils.TOPIC_DATABASE_LOCKED);
+ // Execute next test.
+ let test = tests.shift();
+ print("\nTEST " + (++testIndex) + ": " + test.description);
+ test.exec();
+}
+function run_test() {
+ // Bug 539067: disabled due to random failures and timeouts.
+ return;
+
+ do_test_pending();
+ // Enqueue test, so it will consume the default places-init-complete
+ // notification created at Places init.
+ do_timeout(0, start_tests);
+}
+
+function start_tests() {
+ // Clean up database from all bookmarks.
+ remove_all_bookmarks();
+
+ // Ensure preferences status.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+ try {
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+ do_throw("importBookmarksHTML pref should not exist");
+ }
+ catch(ex) {}
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+ // Kick-off tests.
+ next_test();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_restore.js b/comm/suite/components/places/tests/unit/test_browserGlue_restore.js
new file mode 100644
index 0000000000..c84bdb0b69
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_restore.js
@@ -0,0 +1,81 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue correctly restores bookmarks from a JSON backup if
+ * database has been created and one backup is available.
+ */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "bs",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+XPCOMUtils.defineLazyServiceGetter(this, "anno",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
+ Assert.notEqual(itemId, -1);
+ if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
+ continue_test();
+ },
+ onItemAdded: function() {},
+ onItemRemoved: function(id, folder, index, itemType) {},
+ onItemChanged: function() {},
+ onItemVisited: function(id, visitID, time) {},
+ onItemMoved: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Create our bookmarks.html copying bookmarks.glue.html to the profile
+ // folder. It will be ignored.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Create our JSON backup copying bookmarks.glue.json to the profile
+ // folder. It will be ignored.
+ create_JSON_backup("bookmarks.glue.json");
+
+ // Remove current database file.
+ let db = gProfD.clone();
+ db.append("places.sqlite");
+ if (db.exists()) {
+ db.remove(false);
+ Assert.ok(!db.exists());
+ }
+
+ // Initialize nsSuiteGlue before Places.
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsISuiteGlue);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ // Check a new database has been created.
+ // nsSuiteGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ bs.addObserver(bookmarksObserver);
+}
+
+function continue_test() {
+ // Check that JSON backup has been restored.
+ // Notice restore from JSON notification is fired before smart bookmarks creation.
+ let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(bs.getItemTitle(itemId), "examplejson");
+
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ do_test_finished();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_shutdown.js b/comm/suite/components/places/tests/unit/test_browserGlue_shutdown.js
new file mode 100644
index 0000000000..b4e5f17247
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_shutdown.js
@@ -0,0 +1,152 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue is correctly exporting based on preferences values,
+ * and creating bookmarks backup if one does not exist for today.
+ */
+
+// Initialize nsSuiteGlue after Places.
+var bg = Cc["@mozilla.org/suite/suiteglue;1"].
+ getService(Ci.nsISuiteGlue);
+
+// Initialize Places through Bookmarks Service.
+var bs = PlacesUtils.bookmarks;
+
+// Get other services.
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+
+var tests = [];
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Export to bookmarks.html if autoExportHTML is true.",
+ exec: function() {
+ // Sanity check: we should have bookmarks on the toolbar.
+ do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML);
+
+ // Force nsSuiteGlue::_shutdownPlaces().
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_SHUTDOWN,
+ null);
+
+ // Check bookmarks.html has been created.
+ check_bookmarks_html();
+ // Check JSON backup has been created.
+ check_JSON_backup();
+
+ // Check preferences have not been reverted.
+ do_check_true(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+ // Reset preferences.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Export to bookmarks.html if autoExportHTML is true and a bookmarks.html exists.",
+ exec: function() {
+ // Sanity check: we should have bookmarks on the toolbar.
+ do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+
+ // Create a bookmarks.html in the profile.
+ let profileBookmarksHTMLFile = create_bookmarks_html("bookmarks.glue.html");
+ // Get file lastModified and size.
+ let lastMod = profileBookmarksHTMLFile.lastModifiedTime;
+ let fileSize = profileBookmarksHTMLFile.fileSize;
+
+ // Force nsSuiteGlue::_shutdownPlaces().
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_SHUTDOWN,
+ null);
+
+ // Check a new bookmarks.html has been created.
+ let profileBookmarksHTMLFile = check_bookmarks_html();
+ //XXX not working on Linux unit boxes. Could be filestats caching issue.
+ //do_check_true(profileBookmarksHTMLFile.lastModifiedTime > lastMod);
+ do_check_neq(profileBookmarksHTMLFile.fileSize, fileSize);
+
+ // Check preferences have not been reverted.
+ do_check_true(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+ // Reset preferences.
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Backup to JSON should be a no-op if a backup for today already exists.",
+ exec: function() {
+ // Sanity check: we should have bookmarks on the toolbar.
+ do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
+
+ // Create a JSON backup in the profile.
+ let profileBookmarksJSONFile = create_JSON_backup("bookmarks.glue.json");
+ // Get file lastModified and size.
+ let lastMod = profileBookmarksJSONFile.lastModifiedTime;
+ let fileSize = profileBookmarksJSONFile.fileSize;
+
+ // Force nsSuiteGlue::_shutdownPlaces().
+ bg.QueryInterface(Ci.nsIObserver).observe(null,
+ PlacesUtils.TOPIC_SHUTDOWN,
+ null);
+
+ // Check a new JSON backup has not been created.
+ do_check_true(profileBookmarksJSONFile.exists());
+ do_check_eq(profileBookmarksJSONFile.lastModifiedTime, lastMod);
+ do_check_eq(profileBookmarksJSONFile.fileSize, fileSize);
+
+ do_test_finished();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+function finish_test() {
+ do_test_finished();
+}
+
+var testIndex = 0;
+function next_test() {
+ // Remove bookmarks.html from profile.
+ remove_bookmarks_html();
+ // Remove JSON backups from profile.
+ remove_all_JSON_backups();
+
+ // Execute next test.
+ let test = tests.shift();
+ dump("\nTEST " + (++testIndex) + ": " + test.description);
+ test.exec();
+}
+
+function run_test() {
+ do_test_pending();
+
+ // Clean up bookmarks.
+ remove_all_bookmarks();
+
+ // Create some bookmarks.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark-on-menu");
+ bs.insertBookmark(bs.toolbarFolder, uri("http://mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark-on-toolbar");
+
+ // Kick-off tests.
+ next_test();
+}
diff --git a/comm/suite/components/places/tests/unit/test_browserGlue_smartBookmarks.js b/comm/suite/components/places/tests/unit/test_browserGlue_smartBookmarks.js
new file mode 100644
index 0000000000..bf0b93b807
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -0,0 +1,351 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsSuiteGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
+
+const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
+
+/**
+ * Rebuilds smart bookmarks listening to console output to report any message or
+ * exception generated when calling ensurePlacesDefaultQueriesInitialized().
+ */
+function rebuildSmartBookmarks() {
+ let consoleListener = {
+ observe: function(aMsg) {
+ print("Got console message: " + aMsg.message);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIConsoleListener
+ ]),
+ };
+ Services.console.reset();
+ Services.console.registerListener(consoleListener);
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsISuiteGlue)
+ .ensurePlacesDefaultQueriesInitialized();
+ Services.console.unregisterListener(consoleListener);
+}
+
+
+var tests = [];
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "All smart bookmarks are created if smart bookmarks version is 0.",
+ exec: function() {
+ // Sanity check: we should have default bookmark.
+ Assert.notEqual(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0), -1);
+ Assert.notEqual(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0), -1);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "An existing smart bookmark is replaced when version changes.",
+ exec: function() {
+ // Sanity check: we have a smart bookmark on the toolbar.
+ let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
+ Assert.notEqual(itemId, -1);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+ // Change its title.
+ PlacesUtils.bookmarks.setItemTitle(itemId, "new title");
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(itemId), "new title");
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check smart bookmark has been replaced, itemId has changed.
+ itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
+ Assert.notEqual(itemId, -1);
+ Assert.notEqual(PlacesUtils.bookmarks.getItemTitle(itemId), "new title");
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "bookmarks position is retained when version changes.",
+ exec: function() {
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+ let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
+
+ itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+ let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check smart bookmarks are still in correct position.
+ itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(itemId), firstItemTitle);
+
+ itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(itemId), secondItemTitle);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "moved bookmarks position is retained when version changes.",
+ exec: function() {
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ let itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO));
+ let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId1);
+
+ let itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO));
+ let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId2);
+
+ // Move the first smart bookmark to the end of the menu.
+ PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ Assert.equal(itemId1, PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX));
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check smart bookmarks are still in correct position.
+ itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO));
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(itemId2), secondItemTitle);
+
+ itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO));
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(itemId1), firstItemTitle);
+
+ // Move back the smart bookmark to the original position.
+ PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId, 1);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "An explicitly removed smart bookmark should not be recreated.",
+ exec: function() {
+ // Remove toolbar's smart bookmarks
+ PlacesUtils.bookmarks.removeItem(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0));
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ // We should not have recreated the smart bookmark on toolbar.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+
+//------------------------------------------------------------------------------
+
+tests.push({
+ description: "Even if a smart bookmark has been removed recreate it if version is 0.",
+ exec: function() {
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+ rebuildSmartBookmarks();
+
+ // Count items.
+ // We should not have recreated the smart bookmark on toolbar.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+
+ next_test();
+ }
+});
+//------------------------------------------------------------------------------
+
+function countFolderChildren(aFolderItemId) {
+ let rootNode = PlacesUtils.getFolderContents(aFolderItemId).root;
+ let cc = rootNode.childCount;
+ // Dump contents.
+ for (let i = 0; i < cc ; i++) {
+ let node = rootNode.getChild(i);
+ let title = PlacesUtils.nodeIsSeparator(node) ? "---" : node.title;
+ print("Found child(" + i + "): " + title);
+ }
+ rootNode.containerOpen = false;
+ return cc;
+}
+
+function next_test() {
+ if (tests.length) {
+ // Execute next test.
+ let test = tests.shift();
+ print("\nTEST: " + test.description);
+ test.exec();
+ }
+ else {
+ // Clean up database from all bookmarks.
+ remove_all_bookmarks();
+ do_test_finished();
+ }
+}
+
+function run_test() {
+ do_test_pending();
+
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ // Initialize SuiteGlue, but remove its listener to places-init-complete.
+ let bg = Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsIObserver);
+ // Initialize Places.
+ PlacesUtils.history;
+ // Usually places init would async notify to glue, but we want to avoid
+ // randomness here, thus we fire the notification synchronously.
+ bg.observe(null, "places-init-complete", null);
+
+ // Ensure preferences status.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+ // XXXkairo: might get set due to the different logic of SeaMonkey imports
+ // but there could be some real bug so import is set and restore not
+ try {
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+ }
+ catch(ex) {}
+ try {
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+ // do_throw("importBookmarksHTML pref should not exist");
+ }
+ catch(ex) {}
+
+ waitForImportAndSmartBookmarks(next_test);
+}
+
+function waitForImportAndSmartBookmarks(aCallback) {
+ Services.obs.addObserver(function waitImport() {
+ Services.obs.removeObserver(waitImport, "bookmarks-restore-success");
+ // Delay to test eventual smart bookmarks creation.
+ executeSoon(function () {
+ promiseAsyncUpdates().then(aCallback);
+ });
+ }, "bookmarks-restore-success");
+}
diff --git a/comm/suite/components/places/tests/unit/test_clearHistory_shutdown.js b/comm/suite/components/places/tests/unit/test_clearHistory_shutdown.js
new file mode 100644
index 0000000000..2fa7a42372
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -0,0 +1,181 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that requesting clear history at shutdown will really clear history.
+ */
+
+const URIS = [
+ "http://a.example1.com/"
+, "http://b.example1.com/"
+, "http://b.example2.com/"
+, "http://c.example3.com/"
+];
+
+const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
+
+var EXPECTED_NOTIFICATIONS = [
+ "places-shutdown"
+, "places-will-close-connection"
+, "places-expiration-finished"
+, "places-connection-closed"
+];
+
+const UNEXPECTED_NOTIFICATIONS = [
+ "xpcom-shutdown"
+];
+
+const URL = "ftp://localhost/clearHistoryOnShutdown/";
+
+var notificationIndex = 0;
+
+var notificationsObserver = {
+ observe: function observe(aSubject, aTopic, aData) {
+ print("Received notification: " + aTopic);
+
+ // Note that some of these notifications could arrive multiple times, for
+ // example in case of sync, we allow that.
+ if (EXPECTED_NOTIFICATIONS[notificationIndex] != aTopic)
+ notificationIndex++;
+ Assert.equal(EXPECTED_NOTIFICATIONS[notificationIndex], aTopic);
+
+ if (aTopic != TOPIC_CONNECTION_CLOSED)
+ return;
+
+ getDistinctNotifications().forEach(
+ topic => Services.obs.removeObserver(notificationsObserver, topic)
+ );
+
+ print("Looking for uncleared stuff.");
+
+ let stmt = DBConn().createStatement(
+ "SELECT id FROM moz_places WHERE url = :page_url "
+ );
+
+ try {
+ URIS.forEach(function(aUrl) {
+ stmt.params.page_url = aUrl;
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ });
+ } finally {
+ stmt.finalize();
+ }
+
+ // Check cache.
+ checkCache(URL);
+ }
+}
+
+var timeInMicroseconds = Date.now() * 1000;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(async function test_execute() {
+ do_test_pending();
+
+ print("Initialize suiteglue before Places");
+ // Avoid default bookmarks import.
+ Cc["@mozilla.org/suite/suiteglue;1"].getService(Ci.nsIObserver)
+ .observe(null, "initial-migration", null);
+
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.urlbar", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.formdata", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.passwords", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
+
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+ // Unlike Firefox, SeaMonkey still supports the confirmation dialog
+ // which is called from Sanitizer's init method checkSettings().
+ Services.prefs.setBoolPref("privacy.sanitize.promptOnSanitize", false);
+
+ print("Add visits.");
+ for (let aUrl of URIS) {
+ await promiseAddVisits({uri: uri(aUrl), visitDate: timeInMicroseconds++,
+ transition: PlacesUtils.history.TRANSITION_TYPED})
+ }
+ print("Add cache.");
+ storeCache(URL, "testData");
+});
+
+function run_test_continue()
+{
+ print("Simulate and wait shutdown.");
+ getDistinctNotifications().forEach(
+ topic =>
+ Services.obs.addObserver(notificationsObserver, topic)
+ );
+
+ // Simulate an exit so that Sanitizer's init method checkSettings() is called.
+ print("Simulate 'quit-application-granted' too for SeaMonkey.");
+ Services.obs.notifyObservers(null, "quit-application-granted");
+
+ shutdownPlaces();
+
+ // Shutdown the download manager.
+ Services.obs.notifyObservers(null, "quit-application");
+}
+
+function getDistinctNotifications() {
+ let ar = EXPECTED_NOTIFICATIONS.concat(UNEXPECTED_NOTIFICATIONS);
+ return [...new Set(ar)];
+}
+
+function storeCache(aURL, aContent) {
+ let cache = Cc["@mozilla.org/network/cache-service;1"].
+ getService(Ci.nsICacheService);
+ let session = cache.createSession("FTP", Ci.nsICache.STORE_ANYWHERE,
+ Ci.nsICache.STREAM_BASED);
+
+
+ var storeCacheListener = {
+ onCacheEntryAvailable: function (entry, access, status) {
+ Assert.equal(status, Cr.NS_OK);
+
+ entry.setMetaDataElement("servertype", "0");
+ var os = entry.openOutputStream(0);
+
+ var written = os.write(aContent, aContent.length);
+ if (written != aContent.length) {
+ do_throw("os.write has not written all data!\n" +
+ " Expected: " + written + "\n" +
+ " Actual: " + aContent.length + "\n");
+ }
+ os.close();
+ entry.close();
+ executeSoon(run_test_continue);
+ }
+ };
+
+ session.asyncOpenCacheEntry(aURL,
+ Ci.nsICache.ACCESS_READ_WRITE,
+ storeCacheListener);
+}
+
+function checkCache(aURL) {
+ let cache = Cc["@mozilla.org/network/cache-service;1"].
+ getService(Ci.nsICacheService);
+ let session = cache.createSession("FTP", Ci.nsICache.STORE_ANYWHERE,
+ Ci.nsICache.STREAM_BASED);
+
+ var checkCacheListener = {
+ onCacheEntryAvailable: function (entry, access, status) {
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ do_test_finished();
+ }
+ };
+
+ session.asyncOpenCacheEntry(aURL,
+ Ci.nsICache.ACCESS_READ,
+ checkCacheListener);
+}
diff --git a/comm/suite/components/places/tests/unit/test_leftpane_corruption_handling.js b/comm/suite/components/places/tests/unit/test_leftpane_corruption_handling.js
new file mode 100644
index 0000000000..c8b275e543
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/test_leftpane_corruption_handling.js
@@ -0,0 +1,189 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that we build a working leftpane in various corruption situations.
+ */
+
+// Used to store the original leftPaneFolderId getter.
+var gLeftPaneFolderIdGetter;
+var gAllBookmarksFolderIdGetter;
+// Used to store the original left Pane status as a JSON string.
+var gReferenceJSON;
+var gLeftPaneFolderId;
+// Third party annotated folder.
+var gFolderId;
+
+// Corruption cases.
+var gTests = [
+
+ function test1() {
+ print("1. Do nothing, checks test calibration.");
+ },
+
+ function test2() {
+ print("2. Delete the left pane folder.");
+ PlacesUtils.bookmarks.removeItem(gLeftPaneFolderId);
+ },
+
+ function test3() {
+ print("3. Delete a child of the left pane folder.");
+ let id = PlacesUtils.bookmarks.getIdForItemAt(gLeftPaneFolderId, 0);
+ PlacesUtils.bookmarks.removeItem(id);
+ },
+
+ function test4() {
+ print("4. Delete AllBookmarks.");
+ PlacesUtils.bookmarks.removeItem(PlacesUIUtils.allBookmarksFolderId);
+ },
+
+ function test5() {
+ print("5. Create a duplicated left pane folder.");
+ let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "PlacesRoot",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_FOLDER_ANNO,
+ "PlacesRoot", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ },
+
+ function test6() {
+ print("6. Create a duplicated left pane query.");
+ let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "AllBookmarks",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_QUERY_ANNO,
+ "AllBookmarks", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ },
+
+ function test7() {
+ print("7. Remove the left pane folder annotation.");
+ PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId,
+ ORGANIZER_FOLDER_ANNO);
+ },
+
+ function test8() {
+ print("8. Remove a left pane query annotation.");
+ PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId,
+ ORGANIZER_QUERY_ANNO);
+ },
+
+ function test9() {
+ print("9. Remove a child of AllBookmarks.");
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUIUtils.allBookmarksFolderId, 0);
+ PlacesUtils.bookmarks.removeItem(id);
+ },
+
+];
+
+function run_test() {
+ // We want empty roots.
+ remove_all_bookmarks();
+
+ // Sanity check.
+ Assert.ok(!!PlacesUIUtils);
+
+ // Check getters.
+ gLeftPaneFolderIdGetter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ Assert.equal(typeof(gLeftPaneFolderIdGetter), "function");
+ gAllBookmarksFolderIdGetter = PlacesUIUtils.__lookupGetter__("allBookmarksFolderId");
+ Assert.equal(typeof(gAllBookmarksFolderIdGetter), "function");
+
+ // Add a third party bogus annotated item. Should not be removed.
+ gFolderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "test",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(gFolderId, ORGANIZER_QUERY_ANNO,
+ "test", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Create the left pane, and store its current status, it will be used
+ // as reference value.
+ gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+ gReferenceJSON = folderToJSON(gLeftPaneFolderId);
+
+ // Kick-off tests.
+ do_test_pending();
+ do_timeout(0, run_next_test);
+}
+
+function run_next_test() {
+ if (gTests.length) {
+ // Create corruption.
+ let test = gTests.shift();
+ test();
+ // Regenerate getters.
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", gLeftPaneFolderIdGetter);
+ gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+ PlacesUIUtils.__defineGetter__("allBookmarksFolderId", gAllBookmarksFolderIdGetter);
+ // Check the new left pane folder.
+ let leftPaneJSON = folderToJSON(gLeftPaneFolderId);
+ Assert.ok(compareJSON(gReferenceJSON, leftPaneJSON));
+ Assert.equal(PlacesUtils.bookmarks.getItemTitle(gFolderId), "test");
+ // Go to next test.
+ do_timeout(0, run_next_test);
+ }
+ else {
+ // All tests finished.
+ remove_all_bookmarks();
+ do_test_finished();
+ }
+}
+
+/**
+ * Convert a folder item id to a JSON representation of it and its contents.
+ */
+function folderToJSON(aItemId) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([aItemId], 1);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ let writer = {
+ value: "",
+ write: function PU_wrapNode__write(aStr, aLen) {
+ this.value += aStr;
+ }
+ };
+ PlacesUtils.serializeNodeAsJSONToOutputStream(root, writer, false, false);
+ Assert.ok(writer.value.length > 0);
+ return writer.value;
+}
+
+/**
+ * Compare the JSON representation of 2 nodes, skipping everchanging properties
+ * like dates.
+ */
+function compareJSON(aNodeJSON_1, aNodeJSON_2) {
+ let node1 = JSON.parse(aNodeJSON_1);
+ let node2 = JSON.parse(aNodeJSON_2);
+
+ // List of properties we should not compare (expected to be different).
+ const SKIP_PROPS = ["dateAdded", "lastModified", "id"];
+
+ function compareObjects(obj1, obj2) {
+ function count(o) { var n = 0; for (let p in o) n++; return n; }
+ Assert.equal(count(obj1), count(obj2));
+ for (let prop in obj1) {
+ // Skip everchanging values.
+ if (SKIP_PROPS.includes(prop))
+ continue;
+ // Skip undefined objects, otherwise we hang on them.
+ if (!obj1[prop])
+ continue;
+ if (typeof(obj1[prop]) == "object")
+ return compareObjects(obj1[prop], obj2[prop]);
+ if (obj1[prop] !== obj2[prop]) {
+ print(prop + ": " + obj1[prop] + "!=" + obj2[prop]);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return compareObjects(node1, node2);
+}
diff --git a/comm/suite/components/places/tests/unit/xpcshell.ini b/comm/suite/components/places/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..a2753df42b
--- /dev/null
+++ b/comm/suite/components/places/tests/unit/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+run-sequentially = Avoid bustage.
+support-files = distribution.ini corruptDB.sqlite bookmarks.glue.html bookmarks.glue.json
+
+[test_421483.js]
+[test_browserGlue_corrupt.js]
+[test_browserGlue_corrupt_nobackup.js]
+[test_browserGlue_corrupt_nobackup_default.js]
+[test_browserGlue_distribution.js]
+[test_browserGlue_migrate.js]
+[test_browserGlue_prefs.js]
+[test_browserGlue_restore.js]
+[test_browserGlue_shutdown.js]
+[test_browserGlue_smartBookmarks.js]
+[test_clearHistory_shutdown.js]
+[test_leftpane_corruption_handling.js]
+[test_PUIU_makeTransaction.js]
diff --git a/comm/suite/components/pref/content/pref-advanced.js b/comm/suite/components/pref/content/pref-advanced.js
new file mode 100644
index 0000000000..1f1c290329
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-advanced.js
@@ -0,0 +1,90 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {ShellService} = ChromeUtils.import("resource:///modules/ShellService.jsm");
+
+var defaultClient = 0;
+var defaultApps = 0;
+
+function Startup()
+{
+ InitPlatformIntegration();
+ CrashReportsCheck();
+}
+
+/**
+ * System preferences
+ */
+
+function InitPlatformIntegration() {
+ if (ShellService) {
+ try {
+ this.defaultApps = ShellService.shouldBeDefaultClientFor;
+ ["Browser", "Mail", "News", "Rss"].forEach(function(aType) {
+ let button = document.getElementById("setDefault" + aType);
+ try {
+ let client = Ci.nsIShellService[aType.toUpperCase()];
+ let isDefault = ShellService.isDefaultClient(false, client);
+ if (isDefault) {
+ this.defaultClient |= client;
+ }
+ button.disabled = isDefault;
+ document.getElementById("defaultClientGroup").hidden = false;
+ } catch (e) {
+ button.hidden = true;
+ }
+ });
+ } catch (e) {
+ }
+ }
+}
+
+function ApplySetAsDefaultClient() {
+ let pane = document.getElementById("advanced_pane");
+ ShellService.setDefaultClient(false, false, pane.defaultClient);
+ ShellService.shouldBeDefaultClientFor = pane.defaultApps;
+}
+
+function onSetDefault(aButton, aType) {
+ if (document.documentElement.instantApply) {
+ ShellService.setDefaultClient(false, false, Ci.nsIShellService[aType]);
+ ShellService.shouldBeDefaultClientFor |= Ci.nsIShellService[aType];
+ } else {
+ this.defaultClient |= Ci.nsIShellService[aType];
+ this.defaultApps |= Ci.nsIShellService[aType];
+ window.addEventListener("dialogaccept", this.ApplySetAsDefaultClient, true);
+ }
+
+ aButton.disabled = true;
+}
+
+function onNewsChange(aChecked) {
+ let snws = document.getElementById("network.protocol-handler.external.snews");
+ let nntp = document.getElementById("network.protocol-handler.external.nntp");
+
+ if (!snws.locked)
+ snws.value = aChecked;
+
+ if (!nntp.locked)
+ nntp.value = aChecked;
+}
+
+function CrashReportsCheck()
+{
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ var cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]
+ .getService(Ci.nsICrashReporter);
+ document.getElementById("crashReports").hidden = !cr.enabled;
+ document.getElementById("submitCrashes").checked = cr.submitReports;
+ }
+}
+
+function updateSubmitCrashes(aChecked)
+{
+ Cc["@mozilla.org/toolkit/crash-reporter;1"]
+ .getService(Ci.nsICrashReporter)
+ .submitReports = aChecked;
+}
diff --git a/comm/suite/components/pref/content/pref-advanced.xul b/comm/suite/components/pref/content/pref-advanced.xul
new file mode 100644
index 0000000000..6b77581e4c
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-advanced.xul
@@ -0,0 +1,174 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD;
+ <!ENTITY % prefAdvancedDTD SYSTEM "chrome://communicator/locale/pref/pref-advanced.dtd"> %prefAdvancedDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="advanced_pane"
+ label="&pref.advanced.title;"
+ script="chrome://communicator/content/pref/pref-advanced.js">
+
+ <preferences id="advanced_preferences">
+ <preference id="shell.checkDefaultClient"
+ name="shell.checkDefaultClient"
+ type="bool"/>
+ <preference id="pref.browser.disable_button.default_browser"
+ name="pref.browser.disable_button.default_browser"
+ type="bool"
+ readonly="true"/>
+ <preference id="system.windows.lock_ui.defaultMailClient"
+ name="system.windows.lock_ui.defaultMailClient"
+ type="bool"
+ readonly="true"/>
+ <preference id="system.windows.lock_ui.defaultNewsClient"
+ name="system.windows.lock_ui.defaultNewsClient"
+ type="bool"
+ readonly="true"/>
+ <preference id="system.windows.lock_ui.defaultFeedClient"
+ name="system.windows.lock_ui.defaultFeedClient"
+ type="bool"
+ readonly="true"/>
+ <preference id="network.protocol-handler.external.mailto"
+ name="network.protocol-handler.external.mailto"
+ type="bool"
+ inverted="true"/>
+ <preference id="network.protocol-handler.external.news"
+ name="network.protocol-handler.external.news"
+ type="bool"
+ inverted="true"/>
+ <preference id="network.protocol-handler.external.snews"
+ name="network.protocol-handler.external.snews"
+ type="bool"
+ inverted="true"/>
+ <preference id="network.protocol-handler.external.nntp"
+ name="network.protocol-handler.external.nntp"
+ type="bool"
+ inverted="true"/>
+ <preference id="print.use_native_print_dialog"
+ name="print.use_native_print_dialog"
+ type="bool"/>
+ <preference id="print.use_global_printsettings"
+ name="print.use_global_printsettings"
+ type="bool"/>
+ <preference id="devtools.debugger.remote-enabled"
+ name="devtools.debugger.remote-enabled"
+ type="bool"/>
+ <preference id="devtools.debugger.force-local"
+ name="devtools.debugger.force-local"
+ inverted="true"
+ type="bool"/>
+ <preference id="devtools.debugger.prompt-connection"
+ name="devtools.debugger.prompt-connection"
+ type="bool"/>
+ <preference id="devtools.debugger.remote-port"
+ name="devtools.debugger.remote-port"
+ type="int"/>
+ </preferences>
+
+ <groupbox id="defaultClientGroup" hidden="true">
+ <caption label="&prefCheckDefault.caption;"/>
+ <checkbox id="checkDefaultClient"
+ label="&prefCheckDefaultClient.label;"
+ accesskey="&prefCheckDefaultClient.accesskey;"
+ preference="shell.checkDefaultClient"/>
+ <vbox>
+ <separator class="thin"/>
+
+ <description>&defaultClientFor.description;</description>
+ <hbox class="indent" align="center">
+ <button id="setDefaultBrowser"
+ label="&setDefaultBrowser.label;"
+ accesskey="&setDefaultBrowser.accesskey;"
+ oncommand="onSetDefault(this, 'BROWSER');"
+ preference="pref.browser.disable_button.default_browser"/>
+ <button id="setDefaultMail"
+ label="&setDefaultMail.label;"
+ accesskey="&setDefaultMail.accesskey;"
+ oncommand="onSetDefault(this, 'MAIL');"
+ preference="system.windows.lock_ui.defaultMailClient"/>
+ <button id="setDefaultNews"
+ label="&setDefaultNews.label;"
+ accesskey="&setDefaultNews.accesskey;"
+ oncommand="onSetDefault(this, 'NEWS');"
+ preference="system.windows.lock_ui.defaultNewsClient"/>
+ <button id="setDefaultRss"
+ label="&setDefaultFeed.label;"
+ accesskey="&setDefaultFeed.accesskey;"
+ oncommand="onSetDefault(this, 'RSS');"
+ preference="system.windows.lock_ui.defaultFeedClient"/>
+ </hbox>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <description>&useInternalSettings.description;</description>
+ <hbox class="indent" align="center">
+ <checkbox id="useInternalMail"
+ label="&useInternalMail.label;"
+ accesskey="&useInternalMail.accesskey;"
+ preference="network.protocol-handler.external.mailto"/>
+ <checkbox id="useInternalNews"
+ label="&useInternalNews.label;"
+ accesskey="&useInternalNews.accesskey;"
+ oncommand="onNewsChange(this.checked);"
+ preference="network.protocol-handler.external.news"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox id="printing">
+ <caption label="&printing.label;"/>
+ <checkbox id="nglayoutUseNativePrintDialog"
+ label="&useNativePrintDialog.label;"
+ accesskey="&useNativePrintDialog.accesskey;"
+ preference="print.use_native_print_dialog"/>
+ <checkbox id="printUseGlobalPrintSettings"
+ label="&useGlobalPrintSettings.label;"
+ accesskey="&useGlobalPrintSettings.accesskey;"
+ preference="print.use_global_printsettings"/>
+ </groupbox>
+
+ <groupbox id="crashReports" hidden="true">
+ <caption id="crashReportsCaption" label="&crashReports.caption;"/>
+ <checkbox id="submitCrashes"
+ label="&submitCrashes.label;"
+ accesskey="&submitCrashes.accesskey;"
+ oncommand="updateSubmitCrashes(this.checked);"/>
+ </groupbox>
+
+ <groupbox id="devTools">
+ <caption id="devToolsCaption" label="&devTools.caption;"/>
+ <checkbox id="allowDebugger"
+ label="&allowDebugger.label;"
+ accesskey="&allowDebugger.accesskey;"
+ preference="devtools.debugger.remote-enabled"/>
+ <checkbox id="allowRemoteConnections"
+ label="&allowRemoteConnections.label;"
+ accesskey="&allowRemoteConnections.accesskey;"
+ preference="devtools.debugger.force-local"/>
+ <checkbox id="connectionPrompt"
+ label="&connectionPrompt.label;"
+ accesskey="&connectionPrompt.accesskey;"
+ preference="devtools.debugger.prompt-connection"/>
+
+ <hbox align="center">
+ <label id="remoteDebuggerPortBefore"
+ value="&remoteDebuggerPort.label;"
+ accesskey="&remoteDebuggerPort.accesskey;"
+ control="remoteDebuggerPort"/>
+ <textbox id="remoteDebuggerPort"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"
+ preference="devtools.debugger.remote-port"
+ aria-labelledby="remoteDebuggerPortBefore remoteDebuggerPort"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-appearance.js b/comm/suite/components/pref/content/pref-appearance.js
new file mode 100644
index 0000000000..a54b14786d
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-appearance.js
@@ -0,0 +1,102 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Load spell-checker module to properly determine language strings
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function Startup()
+{
+ SwitchLocales_Load();
+ NumberLocales_Load();
+}
+
+/**
+ * From locale switcher's switch.js:
+ * Load available locales into selection menu
+ */
+function SwitchLocales_Load() {
+ var menulist = document.getElementById("switchLocales");
+
+ var cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIToolkitChromeRegistry);
+
+ var langNames = document.getElementById("languageNamesBundle");
+ var regNames = document.getElementById("regionNamesBundle");
+
+ var matched = false;
+ var currentLocale = Services.locale.getRequestedLocale() || undefined;
+ var locales = cr.getLocalesForPackage("global");
+
+ while (locales.hasMore()) {
+ var locale = locales.getNext();
+
+ var parts = locale.split(/-/);
+
+ var displayName;
+ try {
+ displayName = langNames.getString(parts[0]);
+ if (parts.length > 1) {
+ try {
+ displayName += " (" + regNames.getString(parts[1].toLowerCase()) + ")";
+ }
+ catch (e) {
+ displayName += " (" + parts[1] + ")";
+ }
+ }
+ }
+ catch (e) {
+ displayName = locale;
+ }
+
+ var item = menulist.appendItem(displayName, locale);
+ if (!matched && currentLocale && currentLocale == locale) {
+ matched = true;
+ menulist.selectedItem = item;
+ }
+ }
+ // If somehow we have not found the current locale, select the first in list.
+ if (!matched) {
+ menulist.selectedIndex = 1;
+ }
+}
+
+/**
+ * Determine the appropriate value to set and set it.
+ */
+function SelectLocale(aElement) {
+ var locale = aElement.value;
+ var currentLocale = Services.locale.getRequestedLocale() || undefined;
+ if (!currentLocale || (currentLocale && currentLocale != locale)) {
+ Services.locale.setRequestedLocales([locale]);
+ }
+}
+
+/**
+ * When starting up, determine application and regional locale settings
+ * and add the respective strings to the prefpane labels.
+ */
+function NumberLocales_Load()
+{
+ const osprefs =
+ Cc["@mozilla.org/intl/ospreferences;1"]
+ .getService(Ci.mozIOSPreferences);
+
+ let appLocale = Services.locale.appLocalesAsBCP47[0];
+ let rsLocale = osprefs.regionalPrefsLocales[0];
+ let names = Services.intl.getLocaleDisplayNames(undefined, [appLocale, rsLocale]);
+
+ let appLocaleRadio = document.getElementById("appLocale");
+ let rsLocaleRadio = document.getElementById("rsLocale");
+ let prefutilitiesBundle = document.getElementById("bundle_prefutilities");
+
+ let appLocaleLabel = prefutilitiesBundle.getFormattedString("appLocale.label",
+ [names[0]]);
+ let rsLocaleLabel = prefutilitiesBundle.getFormattedString("rsLocale.label",
+ [names[1]]);
+ appLocaleRadio.setAttribute("label", appLocaleLabel);
+ rsLocaleRadio.setAttribute("label", rsLocaleLabel);
+ appLocaleRadio.accessKey = prefutilitiesBundle.getString("appLocale.accesskey");
+ rsLocaleRadio.accessKey = prefutilitiesBundle.getString("rsLocale.accesskey");
+}
diff --git a/comm/suite/components/pref/content/pref-appearance.xul b/comm/suite/components/pref/content/pref-appearance.xul
new file mode 100644
index 0000000000..132e0a614e
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-appearance.xul
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD;
+ <!ENTITY % prefAppearanceDTD SYSTEM "chrome://communicator/locale/pref/pref-appearance.dtd"> %prefAppearanceDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="appearance_pane"
+ label="&pref.appearance.title;"
+ script="chrome://communicator/content/pref/pref-appearance.js">
+
+ <preferences id="appearance_preferences">
+ <preference id="general.startup.browser"
+ name="general.startup.browser"
+ type="bool"/>
+ <preference id="browser.chrome.toolbar_style"
+ name="browser.chrome.toolbar_style"
+ type="int"/>
+ <preference id="browser.chrome.toolbar_tips"
+ name="browser.chrome.toolbar_tips"
+ type="bool"/>
+ <preference id="browser.toolbars.grippyhidden"
+ name="browser.toolbars.grippyhidden"
+ type="bool"/>
+ <preference id="intl.regional_prefs.use_os_locales"
+ name="intl.regional_prefs.use_os_locales"
+ type="bool"/>
+ </preferences>
+
+ <hbox>
+ <groupbox id="generalStartupPreferences" align="start" flex="1">
+ <caption label="&onStartLegend.label;"/>
+
+ <checkbox id="generalStartupBrowser"
+ label="&navCheck.label;"
+ accesskey="&navCheck.accesskey;"
+ preference="general.startup.browser"/>
+ </groupbox>
+
+ <groupbox id="toolbarStyleBox" align="start" flex="1">
+ <caption label="&showToolsLegend.label;"/>
+
+ <radiogroup id="toolbarStyle"
+ preference="browser.chrome.toolbar_style">
+ <radio value="2"
+ label="&picsNtextRadio.label;"
+ accesskey="&picsNtextRadio.accesskey;"/>
+ <radio value="0"
+ label="&picsOnlyRadio.label;"
+ accesskey="&picsOnlyRadio.accesskey;"/>
+ <radio value="1"
+ label="&textonlyRadio.label;"
+ accesskey="&textonlyRadio.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ </hbox>
+
+ <vbox class="box-padded" align="start">
+ <checkbox id="showHideTooltips"
+ label="&showHideTooltips.label;"
+ accesskey="&showHideTooltips.accesskey;"
+ preference="browser.chrome.toolbar_tips"/>
+ </vbox>
+#ifndef XP_MACOSX
+ <vbox class="box-padded"
+ align="start">
+ <checkbox id="showHideGrippies"
+ label="&showHideGrippies.label;"
+ accesskey="&showHideGrippies.accesskey;"
+ preference="browser.toolbars.grippyhidden"/>
+ </vbox>
+#endif
+ <groupbox id="switchLocaleBox" align="start">
+ <caption label="&pref.locales.title;"/>
+ <description>&selectLocale.label;</description>
+
+ <menulist id="switchLocales"
+ onselect="SelectLocale(this);"/>
+
+ </groupbox>
+
+ <groupbox id="dateTimeFormatting" align="start">
+ <caption label="&dateTimeFormatting.label;"/>
+ <radiogroup id="formatLocale"
+ preference="intl.regional_prefs.use_os_locales"
+ orient="vertical">
+ <radio id="appLocale"
+ value="false"/>
+ <!-- label and accesskey will be set dynamically -->
+ <radio id="rsLocale"
+ value="true"/>
+ <!-- label and accesskey will be set dynamically -->
+ </radiogroup>
+ </groupbox>
+
+ <description>&restartOnLocaleChange.label;</description>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-applicationManager.js b/comm/suite/components/pref/content/pref-applicationManager.js
new file mode 100644
index 0000000000..83d18953ef
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-applicationManager.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var gAppManagerDialog = {
+ _removed: [],
+
+ init: function appManager_init() {
+ this.handlerInfo = window.arguments[0];
+
+ var bundle = document.getElementById("appManagerBundle");
+ var contentText;
+ if (this.handlerInfo.type == TYPE_MAYBE_FEED)
+ contentText = bundle.getString("descriptionHandleWebFeeds");
+ else {
+ var description = gApplicationsPane._describeType(this.handlerInfo);
+ var key =
+ (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) ?
+ "descriptionHandleFile" :
+ "descriptionHandleProtocol";
+ contentText = bundle.getFormattedString(key, [description]);
+ }
+ document.getElementById("appDescription").textContent = contentText;
+
+ var list = document.getElementById("appList");
+ var apps = this.handlerInfo.possibleApplicationHandlers.enumerate();
+ while (apps.hasMoreElements()) {
+ let app = apps.getNext();
+ if (!gApplicationsPane.isValidHandlerApp(app))
+ continue;
+
+ app.QueryInterface(Ci.nsIHandlerApp);
+ var item = list.appendItem(app.name);
+ item.className = "listitem-iconic";
+ item.setAttribute("image",
+ gApplicationsPane._getIconURLForHandlerApp(app));
+ item.app = app;
+ }
+
+ list.selectedIndex = 0;
+ },
+
+ onOK: function appManager_onOK() {
+ if (!this._removed.length) {
+ // return early to avoid calling the |store| method.
+ return;
+ }
+
+ for (var i = 0; i < this._removed.length; ++i)
+ this.handlerInfo.removePossibleApplicationHandler(this._removed[i]);
+
+ this.handlerInfo.store();
+ },
+
+ onCancel: function appManager_onCancel() {
+ // do nothing
+ },
+
+ remove: function appManager_remove() {
+ var list = document.getElementById("appList");
+ this._removed.push(list.selectedItem.app);
+ var index = list.selectedIndex;
+ list.removeItemAt(index);
+ if (list.getRowCount() == 0) {
+ // The list is now empty, make the bottom part disappear
+ document.getElementById("appDetails").hidden = true;
+ }
+ else {
+ // Select the item at the same index, if we removed the last
+ // item of the list, select the previous item
+ if (index == list.getRowCount())
+ --index;
+ list.selectedIndex = index;
+ }
+ },
+
+ onSelect: function appManager_onSelect() {
+ var list = document.getElementById("appList");
+ if (!list.selectedItem) {
+ document.getElementById("cmd_delete").setAttribute("disabled", "true");
+ return;
+ }
+ document.getElementById("cmd_delete").removeAttribute("disabled");
+ var app = list.selectedItem.app;
+ var address = "";
+ if (app instanceof Ci.nsILocalHandlerApp)
+ address = app.executable.path;
+ else if (app instanceof Ci.nsIWebHandlerApp)
+ address = app.uriTemplate;
+ else if (app instanceof Ci.nsIWebContentHandlerInfo)
+ address = app.uri;
+ document.getElementById("appLocation").value = address;
+ var bundle = document.getElementById("appManagerBundle");
+ var appType = app instanceof Ci.nsILocalHandlerApp ? "descriptionLocalApp"
+ : "descriptionWebApp";
+ document.getElementById("appType").value = bundle.getString(appType);
+ }
+};
diff --git a/comm/suite/components/pref/content/pref-applicationManager.xul b/comm/suite/components/pref/content/pref-applicationManager.xul
new file mode 100644
index 0000000000..38988e1080
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-applicationManager.xul
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/pref/pref-applicationManager.dtd">
+
+<dialog id="appManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="accept,cancel"
+ onload="gAppManagerDialog.init();"
+ ondialogaccept="gAppManagerDialog.onOK();"
+ ondialogcancel="gAppManagerDialog.onCancel();"
+ title="&appManager.title;"
+ style="&appManager.style;"
+ persist="screenX screenY">
+
+ <script src="chrome://communicator/content/pref/pref-applications.js"/>
+ <script src="chrome://communicator/content/pref/pref-applicationManager.js"/>
+
+ <commandset id="appManagerCommandSet">
+ <command id="cmd_delete"
+ oncommand="gAppManagerDialog.remove();"
+ disabled="true"/>
+ </commandset>
+
+ <keyset id="appManagerKeyset">
+ <key id="delete" keycode="VK_DELETE" command="cmd_delete"/>
+ </keyset>
+
+ <stringbundleset id="appManagerBundleset">
+ <stringbundle id="appManagerBundle"
+ src="chrome://communicator/locale/pref/pref-applicationManager.properties"/>
+ </stringbundleset>
+
+ <description id="appDescription"/>
+ <separator class="thin"/>
+ <hbox flex="1">
+ <listbox id="appList" onselect="gAppManagerDialog.onSelect();" flex="1"/>
+ <vbox>
+ <button id="remove"
+ label="&remove.label;"
+ accesskey="&remove.accesskey;"
+ command="cmd_delete"/>
+ <spacer flex="1"/>
+ </vbox>
+ </hbox>
+ <vbox id="appDetails">
+ <separator class="thin"/>
+ <label id="appType"/>
+ <textbox id="appLocation" readonly="true" class="plain"/>
+ </vbox>
+</dialog>
diff --git a/comm/suite/components/pref/content/pref-applications.js b/comm/suite/components/pref/content/pref-applications.js
new file mode 100644
index 0000000000..b3ca0d71fd
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-applications.js
@@ -0,0 +1,1606 @@
+/* -*- Mode: Java; tab-width: 2; c-basic-offset: 2; -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {ShellService} = ChromeUtils.import("resource:///modules/ShellService.jsm");
+// Needed as this script is also loaded by pref-applicationManager.xul.
+const {XPCOMUtils} =
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function Startup()
+{
+ gApplicationsPane.init();
+}
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ gCategoryManager: ["@mozilla.org/categorymanager;1", "nsICategoryManager"],
+ gHandlerService: ["@mozilla.org/uriloader/handler-service;1", "nsIHandlerService"],
+ gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
+ gWebContentConverterService: ["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1", "nsIWebContentConverterService"],
+});
+
+//****************************************************************************//
+// Constants & Enumeration Values
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
+const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
+
+/*
+ * Preferences where we store handling information about the feed type.
+ *
+ * browser.feeds.handler
+ * - "messenger", "reader" (clarified further using the .default preference),
+ * or "ask" -- indicates the default handler being used to process feeds;
+ * "messenger" is obsolete, use "reader" instead; to specify that the
+ * handler is messenger, set browser.feeds.handler.default to "messenger";
+ *
+ * browser.feeds.handler.default
+ * - "messenger", "client" or "web" -- indicates the chosen feed reader used
+ * to display feeds, either transiently (i.e., when the "use as default"
+ * checkbox is unchecked, corresponds to when browser.feeds.handler=="ask")
+ * or more permanently (i.e., the item displayed in the dropdown in Feeds
+ * preferences)
+ *
+ * browser.feeds.handler.webservice
+ * - the URL of the currently selected web service used to read feeds
+ *
+ * browser.feeds.handlers.application
+ * - nsIFile, stores the current client-side feed reading app if one has
+ * been chosen
+ */
+const PREF_FEED_SELECTED_APP = "browser.feeds.handlers.application";
+const PREF_FEED_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_FEED_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_FEED_SELECTED_READER = "browser.feeds.handler.default";
+
+const PREF_VIDEO_FEED_SELECTED_APP = "browser.videoFeeds.handlers.application";
+const PREF_VIDEO_FEED_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
+const PREF_VIDEO_FEED_SELECTED_ACTION = "browser.videoFeeds.handler";
+const PREF_VIDEO_FEED_SELECTED_READER = "browser.videoFeeds.handler.default";
+
+const PREF_AUDIO_FEED_SELECTED_APP = "browser.audioFeeds.handlers.application";
+const PREF_AUDIO_FEED_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
+const PREF_AUDIO_FEED_SELECTED_ACTION = "browser.audioFeeds.handler";
+const PREF_AUDIO_FEED_SELECTED_READER = "browser.audioFeeds.handler.default";
+
+// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
+// the actions the application can take with content of various types.
+const kActionChooseApp = -2;
+const kActionManageApp = -1;
+
+//****************************************************************************//
+// Utilities
+
+function getFileDisplayName(aFile) {
+ if (AppConstants.platform == "win" &&
+ aFile instanceof Ci.nsILocalFileWin) {
+ try {
+ return aFile.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ } else if (AppConstants.platform == "macosx" &&
+ aFile instanceof Ci.nsILocalFileMac) {
+ try {
+ return aFile.bundleDisplayName;
+ } catch (e) {}
+ }
+ return aFile.leafName;
+}
+
+function getLocalHandlerApp(aFile) {
+ var localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.name = getFileDisplayName(aFile);
+ localHandlerApp.executable = aFile;
+
+ return localHandlerApp;
+}
+
+/**
+ * An enumeration of items in a JS array.
+ *
+ * FIXME: use ArrayConverter once it lands (bug 380839).
+ *
+ * @constructor
+ */
+function ArrayEnumerator(aItems) {
+ this._index = 0;
+ this._contents = aItems;
+}
+
+ArrayEnumerator.prototype = {
+ _index: 0,
+
+ hasMoreElements: function() {
+ return this._index < this._contents.length;
+ },
+
+ getNext: function() {
+ return this._contents[this._index++];
+ }
+};
+
+function isFeedType(t) {
+ return t == TYPE_MAYBE_FEED || t == TYPE_MAYBE_VIDEO_FEED || t == TYPE_MAYBE_AUDIO_FEED;
+}
+
+//****************************************************************************//
+// HandlerInfoWrapper
+
+/**
+ * This object wraps nsIHandlerInfo with some additional functionality
+ * the Applications prefpane needs to display and allow modification of
+ * the list of handled types.
+ *
+ * We create an instance of this wrapper for each entry we might display
+ * in the prefpane, and we compose the instances from various sources,
+ * including the handler service.
+ *
+ * We don't implement all the original nsIHandlerInfo functionality,
+ * just the stuff that the prefpane needs.
+ *
+ * In theory, all of the custom functionality in this wrapper should get
+ * pushed down into nsIHandlerInfo eventually.
+ */
+function HandlerInfoWrapper(aType, aHandlerInfo) {
+ this.type = aType;
+ this.wrappedHandlerInfo = aHandlerInfo;
+}
+
+HandlerInfoWrapper.prototype = {
+ // The wrapped nsIHandlerInfo object. In general, this object is private,
+ // but there are a couple cases where callers access it directly for things
+ // we haven't (yet?) implemented, so we make it a public property.
+ wrappedHandlerInfo: null,
+
+
+ //**************************************************************************//
+ // nsIHandlerInfo
+
+ // The MIME type or protocol scheme.
+ type: null,
+
+ get description() {
+ if (this.wrappedHandlerInfo.description)
+ return this.wrappedHandlerInfo.description;
+
+ if (this.primaryExtension) {
+ var extension = this.primaryExtension.toUpperCase();
+ return gApplicationsPane._prefsBundle.getFormattedString("fileEnding",
+ [extension]);
+ }
+
+ return this.type;
+ },
+
+ get preferredApplicationHandler() {
+ return this.wrappedHandlerInfo.preferredApplicationHandler;
+ },
+
+ set preferredApplicationHandler(aNewValue) {
+ this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue;
+
+ // Make sure the preferred handler is in the set of possible handlers.
+ if (aNewValue)
+ this.addPossibleApplicationHandler(aNewValue);
+ },
+
+ get possibleApplicationHandlers() {
+ return this.wrappedHandlerInfo.possibleApplicationHandlers;
+ },
+
+ addPossibleApplicationHandler(aNewHandler) {
+ var possibleApps = this.possibleApplicationHandlers.enumerate();
+ while (possibleApps.hasMoreElements()) {
+ if (possibleApps.getNext().equals(aNewHandler))
+ return;
+ }
+ this.possibleApplicationHandlers.appendElement(aNewHandler);
+ },
+
+ removePossibleApplicationHandler(aHandler) {
+ var defaultApp = this.preferredApplicationHandler;
+ if (defaultApp && aHandler.equals(defaultApp)) {
+ // If the app we remove was the default app, we must make sure
+ // it won't be used anymore
+ this.alwaysAskBeforeHandling = true;
+ this.preferredApplicationHandler = null;
+ }
+
+ var handlers = this.possibleApplicationHandlers;
+ for (var i = 0; i < handlers.length; ++i) {
+ var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (handler.equals(aHandler)) {
+ handlers.removeElementAt(i);
+ break;
+ }
+ }
+ },
+
+ get hasDefaultHandler() {
+ return this.wrappedHandlerInfo.hasDefaultHandler;
+ },
+
+ get defaultDescription() {
+ return this.wrappedHandlerInfo.defaultDescription;
+ },
+
+ // What to do with content of this type.
+ get preferredAction() {
+ // If the action is to use a helper app, but we don't have a preferred
+ // handler app, then switch to using the system default, if any; otherwise
+ // fall back to saving to disk, which is the default action in nsMIMEInfo.
+ // Note: "save to disk" is an invalid value for protocol info objects,
+ // but the alwaysAskBeforeHandling getter will detect that situation
+ // and always return true in that case to override this invalid value.
+ if (this.wrappedHandlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
+ !gApplicationsPane.isValidHandlerApp(this.preferredApplicationHandler)) {
+ return this.wrappedHandlerInfo.hasDefaultHandler ?
+ Ci.nsIHandlerInfo.useSystemDefault :
+ Ci.nsIHandlerInfo.saveToDisk;
+ }
+
+ return this.wrappedHandlerInfo.preferredAction;
+ },
+
+ set preferredAction(aNewValue) {
+ this.wrappedHandlerInfo.preferredAction = aNewValue;
+ },
+
+ get alwaysAskBeforeHandling() {
+ // If this is a protocol type and the preferred action is "save to disk",
+ // which is invalid for such types, then return true here to override that
+ // action. This could happen when the preferred action is to use a helper
+ // app, but the preferredApplicationHandler is invalid, and there isn't
+ // a default handler, so the preferredAction getter returns save to disk
+ // instead.
+ if (!(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
+ this.preferredAction == Ci.nsIHandlerInfo.saveToDisk)
+ return true;
+
+ return this.wrappedHandlerInfo.alwaysAskBeforeHandling;
+ },
+
+ set alwaysAskBeforeHandling(aNewValue) {
+ this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue;
+ },
+
+
+ //**************************************************************************//
+ // nsIMIMEInfo
+
+ // The primary file extension associated with this type, if any.
+ get primaryExtension() {
+ try {
+ if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo.primaryExtension)
+ return this.wrappedHandlerInfo.primaryExtension;
+ } catch(ex) {}
+
+ return null;
+ },
+
+ //**************************************************************************//
+ // Storage
+
+ store() {
+ gHandlerService.store(this.wrappedHandlerInfo);
+ },
+
+
+ //**************************************************************************//
+ // Icons
+
+ get smallIcon() {
+ return this._getIcon(16);
+ },
+
+ get largeIcon() {
+ return this._getIcon(32);
+ },
+
+ _getIcon(aSize) {
+ if (this.primaryExtension)
+ return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize;
+
+ if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo)
+ return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type;
+
+ // We're falling back to a generic icon when we can't get a URL for one
+ // (for example in the case of protocol schemes).
+ return null;
+ },
+
+ // The type class is used for setting icons through CSS for types that don't
+ // explicitly set their icons.
+ typeClass: "unknown"
+
+};
+
+
+//****************************************************************************//
+// Feed Handler Info
+
+/**
+ * This object implements nsIHandlerInfo for the feed types. It's a separate
+ * object because we currently store handling information for the feed type
+ * in a set of preferences rather than the nsIHandlerService-managed datastore.
+ *
+ * This object inherits from HandlerInfoWrapper in order to get functionality
+ * that isn't special to the feed type.
+ *
+ * XXX Should we inherit from HandlerInfoWrapper? After all, we override
+ * most of that wrapper's properties and methods, and we have to dance around
+ * the fact that the wrapper expects to have a wrappedHandlerInfo, which we
+ * don't provide.
+ */
+
+function FeedHandlerInfo(aMIMEType) {
+ HandlerInfoWrapper.call(this, aMIMEType, null);
+}
+
+FeedHandlerInfo.prototype = {
+ __proto__: HandlerInfoWrapper.prototype,
+
+ //**************************************************************************//
+ // nsIHandlerInfo
+
+ get description() {
+ return gApplicationsPane._prefsBundle.getString(this.typeClass);
+ },
+
+ get preferredApplicationHandler() {
+ switch (document.getElementById(this._prefSelectedReader).value) {
+ case "client":
+ var file = document.getElementById(this._prefSelectedApp).value;
+ if (file)
+ return getLocalHandlerApp(file);
+
+ return null;
+
+ case "web":
+ var uri = document.getElementById(this._prefSelectedWeb).value;
+ if (!uri)
+ return null;
+ return gWebContentConverterService.getWebContentHandlerByURI(this.type, uri);
+
+ case "messenger":
+ default:
+ // When the pref is set to messenger, we handle feeds internally,
+ // we don't forward them to a local or web handler app, so there is
+ // no preferred handler.
+ return null;
+ }
+ },
+
+ set preferredApplicationHandler(aNewValue) {
+ if (aNewValue instanceof Ci.nsILocalHandlerApp) {
+ document.getElementById(this._prefSelectedApp).value = aNewValue.executable;
+ document.getElementById(this._prefSelectedReader).value = "client";
+ }
+ else if (aNewValue instanceof Ci.nsIWebContentHandlerInfo) {
+ document.getElementById(this._prefSelectedWeb).value = aNewValue.uri;
+ document.getElementById(this._prefSelectedReader).value = "web";
+ // Make the web handler be the new "auto handler" for feeds.
+ // Note: we don't have to unregister the auto handler when the user picks
+ // a non-web handler (local app, RSS News & Blogs, etc.) because the service
+ // only uses the "auto handler" when the selected reader is a web handler.
+ // We also don't have to unregister it when the user turns on "always ask"
+ // (i.e. preview in browser), since that also overrides the auto handler.
+ gWebContentConverterService.setAutoHandler(this.type, aNewValue);
+ }
+ },
+
+ _possibleApplicationHandlers: null,
+
+ get possibleApplicationHandlers() {
+ if (this._possibleApplicationHandlers)
+ return this._possibleApplicationHandlers;
+
+ // A minimal implementation of nsIMutableArray. It only supports the two
+ // methods its callers invoke, namely appendElement, nsIArray::enumerate
+ // and nsIArray::indexOf.
+ this._possibleApplicationHandlers = {
+ _inner: [],
+ _removed: [],
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMutableArray, Ci.nsIArray]),
+
+ get length() {
+ return this._inner.length;
+ },
+
+ enumerate: function() {
+ return new ArrayEnumerator(this._inner);
+ },
+
+ indexOf: function indexOf(startIndex, element) {
+ return this._inner.indexOf(element, startIndex);
+ },
+
+ appendElement: function(aHandlerApp) {
+ this._inner.push(aHandlerApp);
+ },
+
+ removeElementAt: function(aIndex) {
+ this._removed.push(this._inner[aIndex]);
+ this._inner.splice(aIndex, 1);
+ },
+
+ queryElementAt: function(aIndex, aInterface) {
+ return this._inner[aIndex].QueryInterface(aInterface);
+ },
+ };
+
+ // Add the selected local app if it's different from the OS default handler.
+ // Unlike for other types, we can store only one local app at a time for the
+ // feed type, since we store it in a preference that historically stores
+ // only a single path. But we display all the local apps the user chooses
+ // while the prefpane is open, only dropping the list when the user closes
+ // the prefpane, for maximum usability and consistency with other types.
+ var preferredAppFile = document.getElementById(this._prefSelectedApp).value;
+ if (preferredAppFile) {
+ let preferredApp = getLocalHandlerApp(preferredAppFile);
+ let defaultApp = this._defaultApplicationHandler;
+ if (!defaultApp || !defaultApp.equals(preferredApp))
+ this._possibleApplicationHandlers.appendElement(preferredApp);
+ }
+
+ // Add the registered web handlers. There can be any number of these.
+ var webHandlers = gWebContentConverterService.getContentHandlers(this.type);
+ for (let webHandler of webHandlers)
+ this._possibleApplicationHandlers.appendElement(webHandler);
+
+ return this._possibleApplicationHandlers;
+ },
+
+ __defaultApplicationHandler: undefined,
+ get _defaultApplicationHandler() {
+ if (this.__defaultApplicationHandler !== undefined)
+ return this.__defaultApplicationHandler;
+
+ var defaultFeedReader = null;
+ try {
+ defaultFeedReader = ShellService.defaultFeedReader;
+ }
+ catch(ex) {
+ // no default reader
+ }
+
+ if (defaultFeedReader) {
+ let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsIHandlerApp);
+ handlerApp.name = getFileDisplayName(defaultFeedReader);
+ handlerApp.QueryInterface(Ci.nsILocalHandlerApp);
+ handlerApp.executable = defaultFeedReader;
+
+ this.__defaultApplicationHandler = handlerApp;
+ }
+ else {
+ this.__defaultApplicationHandler = null;
+ }
+
+ return this.__defaultApplicationHandler;
+ },
+
+ get hasDefaultHandler() {
+ try {
+ if (ShellService.defaultFeedReader)
+ return true;
+ }
+ catch(ex) {
+ // no default reader
+ }
+
+ return false;
+ },
+
+ get defaultDescription() {
+ if (this.hasDefaultHandler)
+ return this._defaultApplicationHandler.name;
+
+ // Should we instead return null?
+ return "";
+ },
+
+ // What to do with content of this type.
+ get preferredAction() {
+ switch (document.getElementById(this._prefSelectedAction).value) {
+
+ case "reader": {
+ let preferredApp = this.preferredApplicationHandler;
+ let defaultApp = this._defaultApplicationHandler;
+
+ // If we have a valid preferred app, return useSystemDefault if it's
+ // the default app; otherwise return useHelperApp.
+ if (gApplicationsPane.isValidHandlerApp(preferredApp)) {
+ if (defaultApp && defaultApp.equals(preferredApp))
+ return Ci.nsIHandlerInfo.useSystemDefault;
+
+ return Ci.nsIHandlerInfo.useHelperApp;
+ }
+
+ // The pref is set to "reader", but we don't have a valid preferred app.
+ // What do we do now? Not sure this is the best option (perhaps we
+ // should direct the user to the default app, if any), but for now let's
+ // direct the user to live bookmarks.
+ return Ci.nsIHandlerInfo.handleInternally;
+ }
+
+ // If the action is "ask", then alwaysAskBeforeHandling will override
+ // the action, so it doesn't matter what we say it is, it just has to be
+ // something that doesn't cause the controller to hide the type.
+ case "ask":
+ case "messenger":
+ default:
+ return Ci.nsIHandlerInfo.handleInternally;
+ }
+ },
+
+ set preferredAction(aNewValue) {
+ switch (aNewValue) {
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ document.getElementById(this._prefSelectedReader).value = "messenger";
+ break;
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ document.getElementById(this._prefSelectedAction).value = "reader";
+ // The controller has already set preferredApplicationHandler
+ // to the new helper app.
+ break;
+
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ document.getElementById(this._prefSelectedAction).value = "reader";
+ this.preferredApplicationHandler = this._defaultApplicationHandler;
+ break;
+ }
+ },
+
+ get alwaysAskBeforeHandling() {
+ return document.getElementById(this._prefSelectedAction).value == "ask";
+ },
+
+ set alwaysAskBeforeHandling(aNewValue) {
+ if (aNewValue)
+ document.getElementById(this._prefSelectedAction).value = "ask";
+ else
+ document.getElementById(this._prefSelectedAction).value = "reader";
+ },
+
+ // Whether or not we are currently storing the action selected by the user.
+ // We use this to suppress notification-triggered updates to the list when
+ // we make changes that may spawn such updates, specifically when we change
+ // the action for the feed type, which results in feed preference updates,
+ // which spawn "pref changed" notifications that would otherwise cause us
+ // to rebuild the view unnecessarily.
+ _storingAction: false,
+
+
+ //**************************************************************************//
+ // nsIMIMEInfo
+
+ primaryExtension: "xml",
+
+
+ //**************************************************************************//
+ // Storage
+
+ // Changes to the preferred action and handler take effect immediately
+ // (we write them out to the preferences right as they happen),
+ // so we when the controller calls store() after modifying the handlers,
+ // the only thing we need to store is the removal of possible handlers
+ // XXX Should we hold off on making the changes until this method gets called?
+ store() {
+ for (let app of this._possibleApplicationHandlers._removed) {
+ if (app instanceof Ci.nsILocalHandlerApp) {
+ let pref = document.getElementById(PREF_FEED_SELECTED_APP);
+ var preferredAppFile = pref.value;
+ if (preferredAppFile) {
+ let preferredApp = getLocalHandlerApp(preferredAppFile);
+ if (app.equals(preferredApp))
+ pref.reset();
+ }
+ }
+ else {
+ app.QueryInterface(Ci.nsIWebContentHandlerInfo);
+ gWebContentConverterService.removeContentHandler(app.contentType,
+ app.uri);
+ }
+ }
+ this._possibleApplicationHandlers._removed = [];
+ },
+
+
+ //**************************************************************************//
+ // Icons
+
+ smallIcon: null,
+
+ largeIcon: null,
+
+ // The type class is used for setting icons through CSS for types that don't
+ // explicitly set their icons.
+ typeClass: "webFeed",
+};
+
+var feedHandlerInfo = {
+ __proto__: new FeedHandlerInfo(TYPE_MAYBE_FEED),
+ _prefSelectedApp: PREF_FEED_SELECTED_APP,
+ _prefSelectedWeb: PREF_FEED_SELECTED_WEB,
+ _prefSelectedAction: PREF_FEED_SELECTED_ACTION,
+ _prefSelectedReader: PREF_FEED_SELECTED_READER,
+ typeClass: "webFeed",
+};
+
+var videoFeedHandlerInfo = {
+ __proto__: new FeedHandlerInfo(TYPE_MAYBE_VIDEO_FEED),
+ _prefSelectedApp: PREF_VIDEO_FEED_SELECTED_APP,
+ _prefSelectedWeb: PREF_VIDEO_FEED_SELECTED_WEB,
+ _prefSelectedAction: PREF_VIDEO_FEED_SELECTED_ACTION,
+ _prefSelectedReader: PREF_VIDEO_FEED_SELECTED_READER,
+ typeClass: "videoPodcastFeed",
+};
+
+var audioFeedHandlerInfo = {
+ __proto__: new FeedHandlerInfo(TYPE_MAYBE_AUDIO_FEED),
+ _prefSelectedApp: PREF_AUDIO_FEED_SELECTED_APP,
+ _prefSelectedWeb: PREF_AUDIO_FEED_SELECTED_WEB,
+ _prefSelectedAction: PREF_AUDIO_FEED_SELECTED_ACTION,
+ _prefSelectedReader: PREF_AUDIO_FEED_SELECTED_READER,
+ typeClass: "audioPodcastFeed",
+};
+
+
+//****************************************************************************//
+// Prefpane Controller
+
+var gApplicationsPane = {
+ // The set of types the app knows how to handle. A hash of HandlerInfoWrapper
+ // objects, indexed by type.
+ _handledTypes: {},
+
+ // The list of types we can show, sorted by the sort column/direction.
+ // An array of HandlerInfoWrapper objects. We build this list when we first
+ // load the data and then rebuild it when users change a pref that affects
+ // what types we can show or change the sort column/direction.
+ // Note: this isn't necessarily the list of types we *will* show; if the user
+ // provides a filter string, we'll only show the subset of types in this list
+ // that match that string.
+ _visibleTypes: [],
+
+ // A count of the number of times each visible type description appears.
+ // We use these counts to determine whether or not to annotate descriptions
+ // with their types to distinguish duplicate descriptions from each other.
+ // A hash of integer counts, indexed by string description.
+ _visibleTypeDescriptionCount: {},
+
+
+ //**************************************************************************//
+ // Convenience & Performance Shortcuts
+
+ // These get defined by init().
+ _brandShortName : null,
+ _prefsBundle : null,
+ _list : null,
+ _filter : null,
+
+
+ //**************************************************************************//
+ // Initialization & Destruction
+
+ init() {
+ // Initialize shortcuts to some commonly accessed elements & values.
+ this._brandShortName =
+ document.getElementById("bundleBrand").getString("brandShortName");
+ this._prefsBundle = document.getElementById("bundlePrefApplications");
+ this._list = document.getElementById("handlersView");
+ this._filter = document.getElementById("filter");
+
+ // Observe preferences that influence what we display so we can rebuild
+ // the view when they change.
+ Services.prefs.addObserver(PREF_FEED_SELECTED_APP, this);
+ Services.prefs.addObserver(PREF_FEED_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_FEED_SELECTED_ACTION, this);
+
+ Services.prefs.addObserver(PREF_VIDEO_FEED_SELECTED_APP, this);
+ Services.prefs.addObserver(PREF_VIDEO_FEED_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this);
+
+ Services.prefs.addObserver(PREF_AUDIO_FEED_SELECTED_APP, this);
+ Services.prefs.addObserver(PREF_AUDIO_FEED_SELECTED_WEB, this);
+ Services.prefs.addObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this);
+
+ // Listen for window unload so we can remove our preference observers.
+ window.addEventListener("unload", this);
+
+ // Listen for user events on the listbox and its children
+ this._list.addEventListener("select", this);
+ this._list.addEventListener("command", this);
+
+ // Figure out how we should be sorting the list. We persist sort settings
+ // across sessions, so we can't assume the default sort column/direction.
+ this._sortColumn = document.getElementById("typeColumn");
+ if (document.getElementById("actionColumn").hasAttribute("sortDirection")) {
+ this._sortColumn = document.getElementById("actionColumn");
+ // The typeColumn element always has a sortDirection attribute,
+ // either because it was persisted or because the default value
+ // from the xul file was used. If we are sorting on the other
+ // column, we should remove it.
+ document.getElementById("typeColumn").removeAttribute("sortDirection");
+ }
+
+ // Load the data and build the list of handlers.
+ this._loadData();
+ this._rebuildVisibleTypes();
+ this._sortVisibleTypes();
+ this._rebuildView();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "app-handler-pane-loaded");
+ },
+
+ destroy() {
+ this._list.removeEventListener("command", this);
+ this._list.removeEventListener("select", this);
+ window.removeEventListener("unload", this);
+ Services.prefs.removeObserver(PREF_FEED_SELECTED_APP, this);
+ Services.prefs.removeObserver(PREF_FEED_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_FEED_SELECTED_ACTION, this);
+
+ Services.prefs.removeObserver(PREF_VIDEO_FEED_SELECTED_APP, this);
+ Services.prefs.removeObserver(PREF_VIDEO_FEED_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this);
+
+ Services.prefs.removeObserver(PREF_AUDIO_FEED_SELECTED_APP, this);
+ Services.prefs.removeObserver(PREF_AUDIO_FEED_SELECTED_WEB, this);
+ Services.prefs.removeObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this);
+ },
+
+
+ //**************************************************************************//
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIDOMEventListener]),
+
+ //**************************************************************************//
+ // nsIObserver
+
+ observe(aSubject, aTopic, aData) {
+ // Rebuild the list when there are changes to preferences that influence
+ // whether or not to show certain entries in the list.
+ if (aTopic == "nsPref:changed" && !this._storingAction) {
+ // All the prefs we observe can affect what we display, so we rebuild
+ // the view when any of them changes.
+ this._rebuildView();
+ }
+ },
+
+
+ //**************************************************************************//
+ // nsIDOMEventListener
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.destroy();
+ break;
+ case "select":
+ if (this._list.selectedItem)
+ this._list.setAttribute("lastSelectedType",
+ this._list.selectedItem.type);
+ break;
+ case "command":
+ var target = aEvent.originalTarget;
+ switch (target.localName) {
+ case "listitem":
+ if (!this._list.disabled &&
+ target.type == this._list.getAttribute("lastSelectedType"))
+ this._list.selectedItem = target;
+ break;
+ case "listcell":
+ this.rebuildActionsMenu();
+ break;
+ case "menuitem":
+ switch (parseInt(target.value)) {
+ case kActionChooseApp:
+ this.chooseApp();
+ break;
+ case kActionManageApp:
+ this.manageApp();
+ break;
+ default:
+ this.onSelectAction(target);
+ break;
+ }
+ break;
+ }
+ }
+ },
+
+
+ //**************************************************************************//
+ // Composed Model Construction
+
+ _loadData() {
+ this._loadFeedHandler();
+ this._loadApplicationHandlers();
+ },
+
+ _loadFeedHandler() {
+ this._handledTypes[TYPE_MAYBE_FEED] = feedHandlerInfo;
+
+ this._handledTypes[TYPE_MAYBE_VIDEO_FEED] = videoFeedHandlerInfo;
+
+ this._handledTypes[TYPE_MAYBE_AUDIO_FEED] = audioFeedHandlerInfo;
+ },
+
+ /**
+ * Load the set of handlers defined by the application datastore.
+ */
+ _loadApplicationHandlers() {
+ var wrappedHandlerInfos = gHandlerService.enumerate();
+ while (wrappedHandlerInfos.hasMoreElements()) {
+ let wrappedHandlerInfo =
+ wrappedHandlerInfos.getNext().QueryInterface(Ci.nsIHandlerInfo);
+ let type = wrappedHandlerInfo.type;
+
+ let handlerInfoWrapper;
+ if (type in this._handledTypes)
+ handlerInfoWrapper = this._handledTypes[type];
+ else {
+ handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
+ this._handledTypes[type] = handlerInfoWrapper;
+ }
+ }
+ },
+
+
+ //**************************************************************************//
+ // View Construction
+
+ _rebuildVisibleTypes() {
+ // Reset the list of visible types and the visible type description counts.
+ this._visibleTypes = [];
+ this._visibleTypeDescriptionCount = {};
+
+ for (let type in this._handledTypes) {
+ let handlerInfo = this._handledTypes[type];
+
+ // We couldn't find any reason to exclude the type, so include it.
+ this._visibleTypes.push(handlerInfo);
+
+ if (handlerInfo.description in this._visibleTypeDescriptionCount)
+ this._visibleTypeDescriptionCount[handlerInfo.description]++;
+ else
+ this._visibleTypeDescriptionCount[handlerInfo.description] = 1;
+ }
+ },
+
+ _rebuildView() {
+ // Clear the list of entries (the first 2 elements are <listcols> and
+ // <listhead>, they should never get removed).
+ while (this._list.childNodes.length > 2)
+ this._list.lastChild.remove();
+
+ var visibleTypes = this._visibleTypes;
+
+ // If the user is filtering the list, then only show matching types.
+ if (this._filter.value)
+ visibleTypes = visibleTypes.filter(this._matchesFilter, this);
+
+ for (let visibleType of visibleTypes) {
+ let item = document.createElement("listitem");
+ item.setAttribute("allowevents", "true");
+ item.setAttribute("type", visibleType.type);
+ item.setAttribute("typeDescription", this._describeType(visibleType));
+ if (visibleType.smallIcon)
+ item.setAttribute("typeIcon", visibleType.smallIcon);
+ else
+ item.setAttribute("typeClass", visibleType.typeClass);
+ item.setAttribute("actionDescription",
+ this._describePreferredAction(visibleType));
+
+ if (!this._setIconClassForPreferredAction(visibleType, item)) {
+ var sysIcon = this._getIconURLForPreferredAction(visibleType);
+ if (sysIcon)
+ item.setAttribute("actionIcon", sysIcon);
+ else
+ item.setAttribute("appHandlerIcon", "app");
+ }
+
+ this._list.appendChild(item);
+ }
+ },
+
+ _matchesFilter(aType) {
+ var filterValue = this._filter.value.toLowerCase();
+ return this._describeType(aType).toLowerCase().includes(filterValue) ||
+ this._describePreferredAction(aType).toLowerCase().includes(filterValue);
+ },
+
+ /**
+ * Describe, in a human-readable fashion, the type represented by the given
+ * handler info object. Normally this is just the description provided by
+ * the info object, but if more than one object presents the same description,
+ * then we annotate the duplicate descriptions with the type itself to help
+ * users distinguish between those types.
+ *
+ * @param aHandlerInfo {nsIHandlerInfo} the type being described
+ * @returns {string} a description of the type
+ */
+ _describeType(aHandlerInfo) {
+ if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 1)
+ return this._prefsBundle.getFormattedString("typeDescriptionWithType",
+ [aHandlerInfo.description,
+ aHandlerInfo.type]);
+
+ return aHandlerInfo.description;
+ },
+
+ /**
+ * Describe, in a human-readable fashion, the preferred action to take on
+ * the type represented by the given handler info object.
+ *
+ * XXX Should this be part of the HandlerInfoWrapper interface? It would
+ * violate the separation of model and view, but it might make more sense
+ * nonetheless (f.e. it would make sortTypes easier).
+ *
+ * @param aHandlerInfo {nsIHandlerInfo} the type whose preferred action
+ * is being described
+ * @returns {string} a description of the action
+ */
+ _describePreferredAction(aHandlerInfo) {
+ // alwaysAskBeforeHandling overrides the preferred action, so if that flag
+ // is set, then describe that behavior instead. For most types, this is
+ // the "alwaysAsk" string, but for the feed type we show something special.
+ if (aHandlerInfo.alwaysAskBeforeHandling) {
+ if (isFeedType(aHandlerInfo.type))
+ return this._prefsBundle.getFormattedString("previewInApp",
+ [this._brandShortName]);
+ return this._prefsBundle.getString("alwaysAsk");
+ }
+
+ // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
+ // the actions the application can take with content of various types.
+ // But since we've stopped support for plugins, there's no value
+ // identifying the "use plugin" action, so we use this constant instead.
+ const kActionUsePlugin = -3;
+
+ switch (aHandlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ return this._prefsBundle.getString("saveFile");
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ var preferredApp = aHandlerInfo.preferredApplicationHandler;
+ var name = (preferredApp instanceof Ci.nsILocalHandlerApp) ?
+ getFileDisplayName(preferredApp.executable) :
+ preferredApp.name;
+ return this._prefsBundle.getFormattedString("useApp", [name]);
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ // For the feed type, handleInternally means News & Blogs.
+ if (isFeedType(aHandlerInfo.type))
+ return this._prefsBundle.getFormattedString("addNewsBlogsInApp",
+ [this._brandShortName]);
+
+ // For other types, handleInternally looks like either useHelperApp
+ // or useSystemDefault depending on whether or not there's a preferred
+ // handler app.
+ return (this.isValidHandlerApp(aHandlerInfo.preferredApplicationHandler)) ?
+ aHandlerInfo.preferredApplicationHandler.name :
+ aHandlerInfo.defaultDescription;
+
+ // XXX Why don't we say the app will handle the type internally?
+ // Is it because the app can't actually do that? But if that's true,
+ // then why would a preferredAction ever get set to this value
+ // in the first place?
+
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return this._prefsBundle.getFormattedString("useDefault",
+ [aHandlerInfo.defaultDescription]);
+
+ // We no longer support plugins, select "ask" instead:
+ case kActionUsePlugin:
+ return this._prefsBundle.getString("alwaysAsk");
+ }
+ // we should never end up here but do a return to end up with a value
+ return null;
+ },
+
+ /**
+ * Whether or not the given handler app is valid.
+ *
+ * @param aHandlerApp {nsIHandlerApp} the handler app in question
+ *
+ * @returns {boolean} whether or not it's valid
+ */
+ isValidHandlerApp(aHandlerApp) {
+ if (!aHandlerApp)
+ return false;
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp)
+ return this._isValidHandlerExecutable(aHandlerApp.executable);
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp)
+ return aHandlerApp.uriTemplate;
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo)
+ return aHandlerApp.uri;
+
+ if (aHandlerApp instanceof Ci.nsIGIOMimeApp)
+ return aHandlerApp.command;
+
+ return false;
+ },
+
+ _isValidHandlerExecutable(aExecutable) {
+ var file = Services.dirsvc.get("XREExeF",
+ Ci.nsIFile);
+ return aExecutable &&
+ aExecutable.exists() &&
+ aExecutable.isExecutable() &&
+ aExecutable.leafName != file.leafName;
+ },
+
+ /**
+ * Rebuild the actions menu for the selected entry. Gets called by
+ * the listcell constructor when an entry in the list gets selected.
+ * Note that this would not work from onselect on the listbox because
+ * the XBL needs to be applied _before_ calling this function!
+ */
+ rebuildActionsMenu() {
+ var typeItem = this._list.selectedItem;
+ var handlerInfo = this._handledTypes[typeItem.type];
+ var cell =
+ document.getAnonymousElementByAttribute(typeItem, "anonid", "action-cell");
+ var menu =
+ document.getAnonymousElementByAttribute(cell, "anonid", "action-menu");
+ var menuPopup = menu.menupopup;
+
+ // Clear out existing items.
+ while (menuPopup.hasChildNodes())
+ menuPopup.lastChild.remove();
+
+ {
+ let askMenuItem = document.createElement("menuitem");
+ askMenuItem.setAttribute("class", "handler-action");
+ askMenuItem.setAttribute("value", Ci.nsIHandlerInfo.alwaysAsk);
+ let label;
+ if (isFeedType(handlerInfo.type))
+ label = this._prefsBundle.getFormattedString("previewInApp",
+ [this._brandShortName]);
+ else
+ label = this._prefsBundle.getString("alwaysAsk");
+ askMenuItem.setAttribute("label", label);
+ askMenuItem.setAttribute("tooltiptext", label);
+ askMenuItem.setAttribute("appHandlerIcon", "ask");
+ menuPopup.appendChild(askMenuItem);
+ }
+
+ // Create a menu item for saving to disk.
+ // Note: this option isn't available to protocol types, since we don't know
+ // what it means to save a URL having a certain scheme to disk, nor is it
+ // available to feeds, since the feed code doesn't implement the capability.
+ if ((handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
+ !isFeedType(handlerInfo.type)) {
+ let saveMenuItem = document.createElement("menuitem");
+ saveMenuItem.setAttribute("class", "handler-action");
+ saveMenuItem.setAttribute("value", Ci.nsIHandlerInfo.saveToDisk);
+ let label = this._prefsBundle.getString("saveFile");
+ saveMenuItem.setAttribute("label", label);
+ saveMenuItem.setAttribute("tooltiptext", label);
+ saveMenuItem.setAttribute("appHandlerIcon", "save");
+ menuPopup.appendChild(saveMenuItem);
+ }
+
+ // If this is the feed type, add a News & Blogs item.
+ if (isFeedType(handlerInfo.type)) {
+ let internalMenuItem = document.createElement("menuitem");
+ internalMenuItem.setAttribute("class", "handler-action");
+ internalMenuItem.setAttribute("value", Ci.nsIHandlerInfo.handleInternally);
+ let label = this._prefsBundle.getFormattedString("addNewsBlogsInApp",
+ [this._brandShortName]);
+ internalMenuItem.setAttribute("label", label);
+ internalMenuItem.setAttribute("tooltiptext", label);
+ internalMenuItem.setAttribute("appHandlerIcon", "feed");
+ menuPopup.appendChild(internalMenuItem);
+ }
+
+ // Add a separator to distinguish these items from the helper app items
+ // that follow them.
+ let menuSeparator = document.createElement("menuseparator");
+ menuPopup.appendChild(menuSeparator);
+
+ // Create a menu item for the OS default application, if any.
+ if (handlerInfo.hasDefaultHandler) {
+ let defaultMenuItem = document.createElement("menuitem");
+ defaultMenuItem.setAttribute("class", "handler-action");
+ defaultMenuItem.setAttribute("value", Ci.nsIHandlerInfo.useSystemDefault);
+ let label = this._prefsBundle.getFormattedString("useDefault",
+ [handlerInfo.defaultDescription]);
+ defaultMenuItem.setAttribute("label", label);
+ defaultMenuItem.setAttribute("tooltiptext", handlerInfo.defaultDescription);
+ let sysIcon = this._getIconURLForSystemDefault(handlerInfo);
+ if (sysIcon)
+ defaultMenuItem.setAttribute("image", sysIcon);
+ else
+ defaultMenuItem.setAttribute("appHandlerIcon", "app");
+
+ menuPopup.appendChild(defaultMenuItem);
+ }
+
+ // Create menu items for possible handlers.
+ let preferredApp = handlerInfo.preferredApplicationHandler;
+ let possibleApps = handlerInfo.possibleApplicationHandlers.enumerate();
+ var possibleAppMenuItems = [];
+ while (possibleApps.hasMoreElements()) {
+ let possibleApp = possibleApps.getNext();
+ if (!this.isValidHandlerApp(possibleApp))
+ continue;
+
+ let menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("class", "handler-action");
+ menuItem.setAttribute("value", Ci.nsIHandlerInfo.useHelperApp);
+ let label;
+ if (possibleApp instanceof Ci.nsILocalHandlerApp)
+ label = getFileDisplayName(possibleApp.executable);
+ else
+ label = possibleApp.name;
+ label = this._prefsBundle.getFormattedString("useApp", [label]);
+ menuItem.setAttribute("label", label);
+ menuItem.setAttribute("tooltiptext", label);
+ let sysIcon = this._getIconURLForHandlerApp(possibleApp);
+ if (sysIcon)
+ menuItem.setAttribute("image", sysIcon);
+ else
+ menuItem.setAttribute("appHandlerIcon", "app");
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = possibleApp;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+
+// Add gio handlers
+ if (Cc["@mozilla.org/gio-service;1"]) {
+ let gIOSvc = Cc["@mozilla.org/gio-service;1"]
+ .getService(Ci.nsIGIOService);
+ var gioApps = gIOSvc.getAppsForURIScheme(typeItem.type);
+ let enumerator = gioApps.enumerate();
+ let possibleHandlers = handlerInfo.possibleApplicationHandlers;
+ while (enumerator.hasMoreElements()) {
+ let handler = enumerator.getNext().QueryInterface(Ci.nsIHandlerApp);
+ // OS handler share the same name, it's most likely the same app, skipping...
+ if (handler.name == handlerInfo.defaultDescription) {
+ continue;
+ }
+ // Check if the handler is already in possibleHandlers
+ let appAlreadyInHandlers = false;
+ for (let i = possibleHandlers.length - 1; i >= 0; --i) {
+ let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ // nsGIOMimeApp::Equals is able to compare with Ci.nsILocalHandlerApp
+ if (handler.equals(app)) {
+ appAlreadyInHandlers = true;
+ break;
+ }
+ }
+ if (!appAlreadyInHandlers) {
+ let menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ let label = this._prefsBundle.getFormattedString("useApp", [handler.name]);
+ menuItem.setAttribute("label", label);
+ menuItem.setAttribute("tooltiptext", label);
+ menuItem.setAttribute("image", this._getIconURLForHandlerApp(handler));
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = handler;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+ }
+ }
+
+ // Create a menu item for selecting a local application.
+ let canOpenWithOtherApp = true;
+ if (AppConstants.platform == "win") {
+ // On Windows, selecting an application to open another application
+ // would be meaningless so we special case executables.
+ let executableType = gMIMEService.getTypeFromExtension("exe");
+ canOpenWithOtherApp = handlerInfo.type != executableType;
+ }
+ if (canOpenWithOtherApp)
+ {
+ let menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("class", "handler-action");
+ menuItem.setAttribute("value", kActionChooseApp);
+ let label = this._prefsBundle.getString("useOtherApp");
+ menuItem.setAttribute("label", label);
+ menuItem.setAttribute("tooltiptext", label);
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Create a menu item for managing applications.
+ if (possibleAppMenuItems.length) {
+ let menuItem = document.createElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+ menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("class", "handler-action");
+ menuItem.setAttribute("value", kActionManageApp);
+ menuItem.setAttribute("label", this._prefsBundle.getString("manageApp"));
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Select the item corresponding to the preferred action. If the always
+ // ask flag is set, it overrides the preferred action. Otherwise we pick
+ // the item identified by the preferred action (when the preferred action
+ // is to use a helper app, we have to pick the specific helper app item).
+ if (handlerInfo.alwaysAskBeforeHandling)
+ menu.value = Ci.nsIHandlerInfo.alwaysAsk;
+ else if (handlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
+ preferredApp)
+ menu.selectedItem =
+ possibleAppMenuItems.filter(v => v.handlerApp.equals(preferredApp))[0];
+ else
+ menu.value = handlerInfo.preferredAction;
+ },
+
+
+ //**************************************************************************//
+ // Sorting & Filtering
+
+ _sortColumn: null,
+
+ /**
+ * Sort the list when the user clicks on a column header.
+ */
+ sort(event) {
+ var column = event.target;
+
+ // If the user clicked on a new sort column, remove the direction indicator
+ // from the old column.
+ if (this._sortColumn && this._sortColumn != column)
+ this._sortColumn.removeAttribute("sortDirection");
+
+ this._sortColumn = column;
+
+ // Set (or switch) the sort direction indicator.
+ if (column.getAttribute("sortDirection") == "ascending")
+ column.setAttribute("sortDirection", "descending");
+ else
+ column.setAttribute("sortDirection", "ascending");
+
+ this._sortVisibleTypes();
+ this._rebuildView();
+ },
+
+ /**
+ * Sort the list of visible types by the current sort column/direction.
+ */
+ _sortVisibleTypes() {
+ if (!this._sortColumn)
+ return;
+
+ var t = this;
+
+ function sortByType(a, b) {
+ return t._describeType(a).toLowerCase()
+ .localeCompare(t._describeType(b).toLowerCase());
+ }
+
+ function sortByAction(a, b) {
+ return t._describePreferredAction(a).toLowerCase()
+ .localeCompare(t._describePreferredAction(b).toLowerCase());
+ }
+
+ switch (this._sortColumn.getAttribute("value")) {
+ case "type":
+ this._visibleTypes.sort(sortByType);
+ break;
+ case "action":
+ this._visibleTypes.sort(sortByAction);
+ break;
+ }
+
+ if (this._sortColumn.getAttribute("sortDirection") == "descending")
+ this._visibleTypes.reverse();
+ },
+
+
+ //**************************************************************************//
+ // Changes
+
+ onSelectAction(aActionItem) {
+ this._storingAction = true;
+
+ try {
+ this._storeAction(aActionItem);
+ }
+ finally {
+ this._storingAction = false;
+ }
+ },
+
+ _storeAction(aActionItem) {
+ var typeItem = this._list.selectedItem;
+ var handlerInfo = this._handledTypes[typeItem.type];
+
+ let action = parseInt(aActionItem.getAttribute("value"));
+
+ // Set the preferred application handler.
+ // We leave the existing preferred app in the list when we set
+ // the preferred action to something other than useHelperApp so that
+ // legacy datastores that don't have the preferred app in the list
+ // of possible apps still include the preferred app in the list of apps
+ // the user can choose to handle the type.
+ if (action == Ci.nsIHandlerInfo.useHelperApp)
+ handlerInfo.preferredApplicationHandler = aActionItem.handlerApp;
+
+ // Set the preferred action.
+ handlerInfo.preferredAction = action;
+
+ // Set the "always ask" flag.
+ handlerInfo.alwaysAskBeforeHandling = action == Ci.nsIHandlerInfo.alwaysAsk;
+
+ handlerInfo.store();
+
+ // Make sure the handler info object is flagged to indicate that there is
+ // now some user configuration for the type.
+
+ // Update the action label and image to reflect the new preferred action.
+ typeItem.setAttribute("actionDescription",
+ this._describePreferredAction(handlerInfo));
+ if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) {
+ var sysIcon = this._getIconURLForPreferredAction(handlerInfo);
+ if (sysIcon)
+ typeItem.setAttribute("actionIcon", sysIcon);
+ else
+ typeItem.setAttribute("appHandlerIcon", "app");
+ }
+ },
+
+ manageApp() {
+ var typeItem = this._list.selectedItem;
+ var handlerInfo = this._handledTypes[typeItem.type];
+
+ document.documentElement.openSubDialog("chrome://communicator/content/pref/pref-applicationManager.xul",
+ "", handlerInfo);
+
+ // Rebuild the actions menu so that we revert to the previous selection,
+ // or "Always ask" if the previous default application has been removed
+ this.rebuildActionsMenu();
+
+ // update the listitem too. Will be visible when selecting another row
+ typeItem.setAttribute("actionDescription",
+ this._describePreferredAction(handlerInfo));
+ if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) {
+ var sysIcon = this._getIconURLForPreferredAction(handlerInfo);
+ if (sysIcon)
+ typeItem.setAttribute("actionIcon", sysIcon);
+ else
+ typeItem.setAttribute("appHandlerIcon", "app");
+ }
+ },
+
+ handlerApp: null,
+
+ finishChooseApp() {
+ if (this.handlerApp) {
+ // Add the app to the type's list of possible handlers.
+ let handlerInfo = this._handledTypes[this._list.selectedItem.type];
+ handlerInfo.addPossibleApplicationHandler(this.handlerApp);
+ }
+
+ // Rebuild the actions menu whether the user picked an app or canceled.
+ // If they picked an app, we want to add the app to the menu and select it.
+ // If they canceled, we want to go back to their previous selection.
+ this.rebuildActionsMenu();
+
+ // If the user picked a new app from the menu, select it.
+ if (this.handlerApp) {
+ var actionsCell =
+ document.getAnonymousElementByAttribute(this._list.selectedItem,
+ "anonid", "action-cell");
+ var actionsMenu =
+ document.getAnonymousElementByAttribute(actionsCell,
+ "anonid", "action-menu");
+ let menuItems = actionsMenu.menupopup.childNodes;
+ for (let i = 0; i < menuItems.length; i++) {
+ let menuItem = menuItems[i];
+ if (menuItem.handlerApp &&
+ menuItem.handlerApp.equals(this.handlerApp)) {
+ actionsMenu.selectedIndex = i;
+ this.onSelectAction(menuItem);
+ break;
+ }
+ }
+ }
+ },
+
+ chooseApp() {
+ this.handlerApp = null;
+
+ if (AppConstants.platform == "win") {
+ let params = {};
+ let handlerInfo = this._handledTypes[this._list.selectedItem.type];
+
+ if (isFeedType(handlerInfo.type)) {
+ // MIME info will be null, create a temp object.
+ params.mimeInfo =
+ gMIMEService.getFromTypeAndExtension(handlerInfo.type,
+ handlerInfo.primaryExtension);
+ } else {
+ params.mimeInfo = handlerInfo.wrappedHandlerInfo;
+ }
+
+ params.title = this._prefsBundle.getString("fpTitleChooseApp");
+ params.description = handlerInfo.description;
+ params.filename = null;
+ params.handlerApp = null;
+
+ window.openDialog("chrome://global/content/appPicker.xul", null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes",
+ params);
+
+ if (this.isValidHandlerApp(params.handlerApp)) {
+ this.handlerApp = params.handlerApp;
+ }
+ this.finishChooseApp();
+ } else if (Services.prefs.getBoolPref("browser.download.useAppChooser", true) && ("@mozilla.org/applicationchooser;1" in Cc)) {
+ let mimeInfo;
+ let handlerInfo = this._handledTypes[this._list.selectedItem.type];
+ if (isFeedType(handlerInfo.type)) {
+ // MIME info will be null, create a temp object.
+ mimeInfo =
+ gMIMEService.getFromTypeAndExtension(handlerInfo.type,
+ handlerInfo.primaryExtension);
+ } else {
+ mimeInfo = handlerInfo.wrappedHandlerInfo;
+ }
+
+ var appChooser = Cc["@mozilla.org/applicationchooser;1"]
+ .createInstance(Ci.nsIApplicationChooser);
+ appChooser.init(window, this._prefsBundle.getString("fpTitleChooseApp"));
+ var contentTypeDialogObj = this;
+ let appChooserCallback = function appChooserCallback_done(aResult) {
+ if (aResult) {
+ contentTypeDialogObj.handlerApp = aResult.QueryInterface(Ci.nsILocalHandlerApp);
+ }
+ contentTypeDialogObj.finishChooseApp();
+ };
+ appChooser.open(mimeInfo.MIMEType, appChooserCallback);
+ // The finishChooseApp is called from appChooserCallback
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ let winTitle = this._prefsBundle.getString("fpTitleChooseApp");
+ fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+
+ // Prompt the user to pick an app. If they pick one, and it's a valid
+ // selection, then add it to the list of possible handlers.
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK && fp.file &&
+ this._isValidHandlerExecutable(fp.file)) {
+ let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.name = getFileDisplayName(fp.file);
+ handlerApp.executable = fp.file;
+ this.handlerApp = handlerApp;
+ }
+ this.finishChooseApp();
+ });
+ }
+ },
+
+ _setIconClassForPreferredAction(aHandlerInfo, aElement) {
+ // If this returns true, the attribute that CSS sniffs for was set to something
+ // so you shouldn't manually set an icon URI.
+ // This removes the existing actionIcon attribute if any, even if returning false.
+ aElement.removeAttribute("actionIcon");
+
+ if (aHandlerInfo.alwaysAskBeforeHandling) {
+ aElement.setAttribute("appHandlerIcon", "ask");
+ return true;
+ }
+
+ switch (aHandlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ aElement.setAttribute("appHandlerIcon", "save");
+ return true;
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (isFeedType(aHandlerInfo.type)) {
+ aElement.setAttribute("appHandlerIcon", "feed");
+ return true;
+ }
+ break;
+ }
+ aElement.removeAttribute("appHandlerIcon");
+ return false;
+ },
+
+ _getIconURLForPreferredAction(aHandlerInfo) {
+ switch (aHandlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return this._getIconURLForSystemDefault(aHandlerInfo);
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ let preferredApp = aHandlerInfo.preferredApplicationHandler;
+ if (this.isValidHandlerApp(preferredApp))
+ return this._getIconURLForHandlerApp(preferredApp);
+ break;
+ }
+ // This should never happen, but if preferredAction is set to some weird
+ // value, then fall back to the generic application icon.
+ return null;
+ },
+
+ _getIconURLForHandlerApp(aHandlerApp) {
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp)
+ return this._getIconURLForFile(aHandlerApp.executable);
+
+ if (Services.prefs.getBoolPref("browser.chrome.favicons")) { // q.v. Bug 514671
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp)
+ return this._getIconURLForWebApp(aHandlerApp.uriTemplate);
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo)
+ return this._getIconURLForWebApp(aHandlerApp.uri);
+ }
+
+ // We know nothing about other kinds of handler apps.
+ return "";
+ },
+
+ _getIconURLForFile(aFile) {
+ var fph = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ var urlSpec = fph.getURLSpecFromFile(aFile);
+
+ return "moz-icon://" + urlSpec + "?size=16";
+ },
+
+ _getIconURLForWebApp(aWebAppURITemplate) {
+ var uri = Services.io.newURI(aWebAppURITemplate);
+
+ // Unfortunately we need to use favicon.ico here, but we don't know
+ // about any other possibility to retrieve an icon for the web app/site
+ // without loading a specific full URL and parsing it for a possible
+ // shortcut icon.
+
+ return /^https?/.test(uri.scheme) ? uri.resolve("/favicon.ico") : "";
+ },
+
+ _getIconURLForSystemDefault(aHandlerInfo) {
+ // Handler info objects for MIME types on some OSes implement a property bag
+ // interface from which we can get an icon for the default app, so if we're
+ // dealing with a MIME type on one of those OSes, then try to get the icon.
+ if ("wrappedHandlerInfo" in aHandlerInfo) {
+ let wrappedHandlerInfo = aHandlerInfo.wrappedHandlerInfo;
+
+ if (wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ wrappedHandlerInfo instanceof Ci.nsIPropertyBag) {
+ try {
+ let url = wrappedHandlerInfo.getProperty("defaultApplicationIconURL");
+ if (url)
+ return url + "?size=16";
+ }
+ catch(ex) {}
+ }
+ }
+
+ // If this isn't a MIME type object on an OS that supports retrieving
+ // the icon, or if we couldn't retrieve the icon for some other reason,
+ // then use a generic icon.
+ return null;
+ }
+
+};
diff --git a/comm/suite/components/pref/content/pref-applications.xul b/comm/suite/components/pref/content/pref-applications.xul
new file mode 100644
index 0000000000..351c254252
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-applications.xul
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD;
+ <!ENTITY % prefApplicationsDTD SYSTEM "chrome://communicator/locale/pref/pref-applications.dtd"> %prefApplicationsDTD;
+]>
+
+<overlay id="ApplicationsPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="applications_pane"
+ label="&pref.applications.title;"
+ script="chrome://communicator/content/pref/pref-applications.js">
+
+ <preferences id="feedsPreferences">
+ <preference id="browser.feeds.handler"
+ name="browser.feeds.handler"
+ type="string"/>
+ <preference id="browser.feeds.handler.default"
+ name="browser.feeds.handler.default"
+ type="string"/>
+ <preference id="browser.feeds.handlers.application"
+ name="browser.feeds.handlers.application"
+ type="file"/>
+ <preference id="browser.feeds.handlers.webservice"
+ name="browser.feeds.handlers.webservice"
+ type="string"/>
+
+ <preference id="browser.videoFeeds.handler"
+ name="browser.videoFeeds.handler"
+ type="string"/>
+ <preference id="browser.videoFeeds.handler.default"
+ name="browser.videoFeeds.handler.default"
+ type="string"/>
+ <preference id="browser.videoFeeds.handlers.application"
+ name="browser.videoFeeds.handlers.application"
+ type="file"/>
+ <preference id="browser.videoFeeds.handlers.webservice"
+ name="browser.videoFeeds.handlers.webservice"
+ type="string"/>
+
+ <preference id="browser.audioFeeds.handler"
+ name="browser.audioFeeds.handler"
+ type="string"/>
+ <preference id="browser.audioFeeds.handler.default"
+ name="browser.audioFeeds.handler.default"
+ type="string"/>
+ <preference id="browser.audioFeeds.handlers.application"
+ name="browser.audioFeeds.handlers.application"
+ type="file"/>
+ <preference id="browser.audioFeeds.handlers.webservice"
+ name="browser.audioFeeds.handlers.webservice"
+ type="string"/>
+
+ <preference id="pref.downloads.disable_button.edit_actions"
+ name="pref.downloads.disable_button.edit_actions"
+ type="bool"/>
+ <preference id="browser.download.useAppChooser"
+ name="browser.download.useAppChooser"
+ type="bool"/>
+ </preferences>
+
+ <stringbundleset id="appBundleset">
+ <stringbundle id="bundleBrand"
+ src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundlePrefApplications"
+ src="chrome://communicator/locale/pref/pref-applications.properties"/>
+ </stringbundleset>
+
+ <hbox align="center">
+ <textbox id="filter"
+ flex="1"
+ type="search"
+ placeholder="&search.placeholder;"
+ clickSelectsAll="true"
+ aria-controls="handlersView"
+ oncommand="gApplicationsPane._rebuildView();"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <listbox id="handlersView" persist="lastSelectedType" flex="1"
+ preference="pref.downloads.disable_button.edit_actions">
+ <listcols>
+ <listcol width="1" flex="1"/>
+ <listcol width="1" flex="1"/>
+ </listcols>
+ <listhead>
+ <listheader id="typeColumn" label="&typeColumn.label;" value="type"
+ accesskey="&typeColumn.accesskey;" persist="sortDirection"
+ onclick="gApplicationsPane.sort(event);"
+ sortDirection="ascending"/>
+ <listheader id="actionColumn" label="&actionColumn2.label;" value="action"
+ accesskey="&actionColumn2.accesskey;" persist="sortDirection"
+ onclick="gApplicationsPane.sort(event);"/>
+ </listhead>
+ </listbox>
+#ifdef XP_LINUX
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox id="downloadUseAppChooser"
+ label="&useAppChooser.label;"
+ accesskey="&useAppChooser.accesskey;"
+ preference="browser.download.useAppChooser"/>
+ </hbox>
+#endif
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-cache.js b/comm/suite/components/pref/content/pref-cache.js
new file mode 100644
index 0000000000..be2245b98b
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-cache.js
@@ -0,0 +1,113 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+function Startup()
+{
+ updateActualCacheSize();
+}
+
+// Needs to be global because the cache service only keeps a weak reference.
+var CacheObserver = {
+ /* nsICacheStorageConsumptionObserver */
+ onNetworkCacheDiskConsumption: function(aConsumption) {
+ var actualSizeLabel = document.getElementById("cacheSizeInfo");
+ var sizeStrings = DownloadUtils.convertByteUnits(aConsumption);
+ var prefStrBundle = document.getElementById("bundle_prefutilities");
+ var sizeStr = prefStrBundle.getFormattedString("cacheSizeInfo",
+ sizeStrings);
+ actualSizeLabel.textContent = sizeStr;
+ },
+
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsICacheStorageConsumptionObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+// because the cache is in kilobytes, and the UI is in megabytes.
+function ReadCacheDiskCapacity()
+{
+ var pref = document.getElementById("browser.cache.disk.capacity");
+ return pref.value >> 10;
+}
+
+function WriteCacheDiskCapacity(aField)
+{
+ return aField.value << 10;
+}
+
+function ReadCacheFolder(aField)
+{
+ var pref = document.getElementById("browser.cache.disk.parent_directory");
+ var file = pref.value;
+
+ if (!file)
+ {
+ try
+ {
+ // no disk cache folder pref set; default to profile directory
+ file = GetSpecialDirectory(Services.dirsvc.has("ProfLD") ? "ProfLD"
+ : "ProfD");
+ }
+ catch (ex) {}
+ }
+
+ if (file) {
+ aField.file = file;
+ aField.label = AppConstants.platform == "macosx" ? file.leafName : file.path;
+ }
+}
+
+function CacheSelectFolder()
+{
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ let title = document.getElementById("bundle_prefutilities")
+ .getString("cachefolder");
+
+ fp.init(window, title, Ci.nsIFilePicker.modeGetFolder);
+ fp.displayDirectory =
+ document.getElementById("browser.cache.disk.parent_directory").value;
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ document.getElementById("browser.cache.disk.parent_directory").value = fp.file;
+ });
+}
+
+function ClearDiskAndMemCache()
+{
+ Services.cache2.clear();
+ updateActualCacheSize();
+}
+
+function updateCacheSizeUI(cacheSizeEnabled)
+{
+ document.getElementById("browserCacheDiskCacheBefore").disabled = cacheSizeEnabled;
+ document.getElementById("browserCacheDiskCache").disabled = cacheSizeEnabled;
+ document.getElementById("browserCacheDiskCacheAfter").disabled = cacheSizeEnabled;
+}
+
+function ReadSmartSizeEnabled()
+{
+ var enabled = document.getElementById("browser.cache.disk.smart_size.enabled").value;
+ updateCacheSizeUI(enabled);
+ return enabled;
+}
+
+function updateActualCacheSize()
+{
+ Services.cache2.asyncGetDiskConsumption(CacheObserver);
+}
diff --git a/comm/suite/components/pref/content/pref-cache.xul b/comm/suite/components/pref/content/pref-cache.xul
new file mode 100644
index 0000000000..6b60ddef2b
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-cache.xul
@@ -0,0 +1,142 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefCacheDTD SYSTEM "chrome://communicator/locale/pref/pref-cache.dtd">
+%prefCacheDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="cache_pane"
+ label="&pref.cache.title;"
+ script="chrome://communicator/content/pref/pref-cache.js">
+
+ <preferences>
+ <preference id="browser.cache.disk.capacity"
+ name="browser.cache.disk.capacity"
+ type="int"/>
+ <preference id="browser.cache.disk.smart_size.enabled"
+ name="browser.cache.disk.smart_size.enabled"
+ type="bool"/>
+ <preference id="pref.advanced.cache.disable_button.clear_disk"
+ name="pref.advanced.cache.disable_button.clear_disk"
+ type="bool"/>
+ <preference id="browser.cache.check_doc_frequency"
+ name="browser.cache.check_doc_frequency"
+ type="int"/>
+ <preference id="network.prefetch-next"
+ name="network.prefetch-next"
+ type="bool"/>
+ <preference id="browser.cache.disk.parent_directory"
+ name="browser.cache.disk.parent_directory"
+ type="file"/>
+ <preference id="browser.cache.disk.enable"
+ name="browser.cache.disk.enable"
+ type="bool"/>
+ <preference id="browser.cache.memory.enable"
+ name="browser.cache.memory.enable"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.cache.caption;"/>
+
+ <description>&cachePara;</description>
+
+ <vbox align="start">
+ <label id="cacheSizeInfo"/>
+ <checkbox id="allowSmartSize"
+ label="&cacheCheck.label;"
+ accesskey="&cacheCheck.accesskey;"
+ onsyncfrompreference="return document.getElementById('cache_pane').ReadSmartSizeEnabled();"
+ preference="browser.cache.disk.smart_size.enabled"/>
+ </vbox>
+ <hbox align="center">
+ <label id="browserCacheDiskCacheBefore"
+ value="&diskCacheUpTo.label;"
+ accesskey="&diskCacheUpTo.accesskey;"
+ control="browserCacheDiskCache"/>
+ <textbox id="browserCacheDiskCache"
+ size="5"
+ type="number"
+ aria-labelledby="browserCacheDiskCacheBefore browserCacheDiskCache browserCacheDiskCacheAfter"
+ preference="browser.cache.disk.capacity"
+ onsyncfrompreference="return document.getElementById('cache_pane').ReadCacheDiskCapacity();"
+ onsynctopreference="return document.getElementById('cache_pane').WriteCacheDiskCapacity(this);"/>
+ <label id="browserCacheDiskCacheAfter"
+ value="&spaceMbytes;"/>
+ <button label="&clearDiskCache.label;"
+ accesskey="&clearDiskCache.accesskey;"
+ oncommand="ClearDiskAndMemCache();"
+ id="clearDiskCache"
+ preference="pref.advanced.cache.disable_button.clear_disk"/>
+ </hbox>
+
+ <vbox>
+ <label value="&diskCacheFolder.label;"/>
+ <hbox align="center">
+ <filefield id="browserCacheDiskCacheFolder"
+ flex="1"
+ preference="browser.cache.disk.parent_directory"
+ preference-editable="true"
+ onsyncfrompreference="return document.getElementById('cache_pane').ReadCacheFolder(this);"/>
+ <button label="&chooseDiskCacheFolder.label;"
+ accesskey="&chooseDiskCacheFolder.accesskey;"
+ oncommand="CacheSelectFolder();"
+ id="chooseDiskCacheFolder">
+ <observes element="browserCacheDiskCacheFolder"
+ attribute="disabled"/>
+ </button>
+ </hbox>
+ </vbox>
+ <description>&diskCacheFolderExplanation;</description>
+
+ <separator class="thin"/>
+
+ <label control="browserCacheCheckDocFrequency"
+ value="&docCache.label;"
+ accesskey="&docCache.accesskey;"/>
+ <hbox align="start">
+ <menulist id="browserCacheCheckDocFrequency"
+ class="indent"
+ preference="browser.cache.check_doc_frequency">
+ <menupopup>
+ <menuitem value="1" label="&checkEveryTime.label;"/>
+ <menuitem value="3" label="&checkAutomatically.label;"/>
+ <menuitem value="0" label="&checkOncePerSession.label;"/>
+ <menuitem value="2" label="&checkNever.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ </groupbox>
+
+ <groupbox id="prefetch">
+ <caption id="prefetchLabel" label="&prefetchTitle.label;"/>
+ <vbox id="prefetchBox" align="start">
+ <checkbox id="enablePrefetch"
+ label="&enablePrefetch.label;"
+ accesskey="&enablePrefetch.accesskey;"
+ preference="network.prefetch-next"/>
+ </vbox>
+ </groupbox>
+
+ <groupbox id="debugCache">
+ <caption label="&debugCache.label;"/>
+ <hbox align="center">
+ <checkbox id="browserEnableDiskCache"
+ label="&debugEnableDiskCache.label;"
+ accesskey="&debugEnableDiskCache.accesskey;"
+ preference="browser.cache.disk.enable"/>
+ <checkbox id="browserEnableCache"
+ label="&debugEnableMemCache.label;"
+ accesskey="&debugEnableMemCache.accesskey;"
+ preference="browser.cache.memory.enable"/>
+ </hbox>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-colors.js b/comm/suite/components/pref/content/pref-colors.js
new file mode 100644
index 0000000000..619af1bd5e
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-colors.js
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ ToggleCustomColorPickers(document.getElementById("browser.display.use_system_colors").value);
+}
+
+function ToggleCustomColorPickers(aChecked)
+{
+ TogglePickerDisability(aChecked, "browserForegroundColor");
+ TogglePickerDisability(aChecked, "browserBackgroundColor");
+}
+
+function TogglePickerDisability(aDisable, aPicker)
+{
+ var element = document.getElementById(aPicker);
+ aDisable = aDisable ||
+ document.getElementById(element.getAttribute("preference")).locked;
+
+ element.disabled = aDisable;
+ element = document.getElementById(aPicker + "Label");
+ element.disabled = aDisable;
+}
diff --git a/comm/suite/components/pref/content/pref-colors.xul b/comm/suite/components/pref/content/pref-colors.xul
new file mode 100644
index 0000000000..97c5d0c631
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-colors.xul
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-colors.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="colors_pane"
+ label="&pref.colors.title;"
+ script="chrome://communicator/content/pref/pref-colors.js">
+
+ <preferences id="colors_preferences">
+ <preference id="browser.display.foreground_color"
+ name="browser.display.foreground_color"
+ type="string"/>
+ <preference id="browser.display.background_color"
+ name="browser.display.background_color"
+ type="string"/>
+ <preference id="browser.display.use_system_colors"
+ name="browser.display.use_system_colors"
+ type="bool"
+ onchange="ToggleCustomColorPickers(this.value);"/>
+ <preference id="browser.anchor_color"
+ name="browser.anchor_color"
+ type="string"/>
+ <preference id="browser.active_color"
+ name="browser.active_color"
+ type="string"/>
+ <preference id="browser.visited_color"
+ name="browser.visited_color"
+ type="string"/>
+ <preference id="browser.underline_anchors"
+ name="browser.underline_anchors"
+ type="bool"/>
+ <preference id="browser.display.document_color_use"
+ name="browser.display.document_color_use"
+ type="int"/>
+ </preferences>
+ <hbox>
+ <groupbox flex="1" id="pageColours">
+ <caption label="&color;"/>
+ <hbox align="center">
+ <label id="browserForegroundColorLabel"
+ value="&textColor.label;"
+ accesskey="&textColor.accesskey;"
+ flex="1"
+ control="browserForegroundColor"/>
+ <colorpicker id="browserForegroundColor"
+ type="button"
+ palettename="standard"
+ preference="browser.display.foreground_color"/>
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label id="browserBackgroundColorLabel"
+ value="&backgroundColor.label;"
+ accesskey="&backgroundColor.accesskey;"
+ flex="1"
+ control="browserBackgroundColor"/>
+ <colorpicker id="browserBackgroundColor"
+ type="button"
+ palettename="standard"
+ preference="browser.display.background_color"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="center">
+ <checkbox id="browserUseSystemColors"
+ label="&useSystemColors.label;"
+ accesskey="&useSystemColors.accesskey;"
+ preference="browser.display.use_system_colors"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox flex="1">
+ <caption label="&links;"/>
+ <hbox align="center">
+ <label value="&linkColor.label;"
+ accesskey="&linkColor.accesskey;"
+ flex="1"
+ control="browserAnchorColor"/>
+ <colorpicker id="browserAnchorColor"
+ type="button"
+ palettename="standard"
+ preference="browser.anchor_color"/>
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label value="&activeLinkColor.label;"
+ accesskey="&activeLinkColor.accesskey;"
+ flex="1"
+ control="browserActiveColor"/>
+ <colorpicker id="browserActiveColor"
+ type="button"
+ palettename="standard"
+ preference="browser.active_color"/>
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label value="&visitedLinkColor.label;"
+ accesskey="&visitedLinkColor.accesskey;"
+ flex="1"
+ control="browserVisitedColor"/>
+ <colorpicker id="browserVisitedColor"
+ type="button"
+ palettename="standard"
+ preference="browser.visited_color"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="center">
+ <checkbox id="browserUnderlineAnchors"
+ label="&underlineLinks.label;"
+ accesskey="&underlineLinks.accesskey;"
+ preference="browser.underline_anchors"/>
+ </hbox>
+ </groupbox>
+ </hbox>
+
+ <groupbox>
+ <caption label="&someProvColors;"/>
+
+ <radiogroup id="browserDocumentColorUse"
+ preference="browser.display.document_color_use">
+ <radio value="1" label="&alwaysUseDocumentColors.label;"
+ accesskey="&alwaysUseDocumentColors.accesskey;"/>
+ <radio value="2" label="&useMyColors.label;"
+ accesskey="&useMyColors.accesskey;"/>
+ <radio value="0" label="&automaticColors.label;"
+ accesskey="&automaticColors.accesskey;"/>
+ </radiogroup>
+
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-content.js b/comm/suite/components/pref/content/pref-content.js
new file mode 100644
index 0000000000..964c216a28
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-content.js
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+var minMinValue;
+var maxMinValue;
+
+/**
+ * When starting up, obtain min and max values for the zoom-range controls
+ * from the first and last values of the zoom-levels array.
+ */
+function Startup()
+{
+ let minElement = document.getElementById("minZoom");
+ let maxElement = document.getElementById("maxZoom");
+ let minMaxLimit = 200;
+ let maxMinLimit = 50; // allow reasonable amounts of overlap
+ let zoomValues = Services.prefs.getCharPref("toolkit.zoomManager.zoomValues")
+ .split(",").map(parseFloat);
+ zoomValues.sort((a, b) => a - b);
+
+ let firstValue = Math.round(100 * zoomValues[0]);
+ let lastValue = Math.round(100 * zoomValues[zoomValues.length - 1]);
+
+ minMinValue = firstValue;
+ minElement.min = minMinValue;
+ minElement.max = lastValue > minMaxLimit ? minMaxLimit : lastValue;
+
+ maxMinValue = firstValue < maxMinLimit ? maxMinLimit : firstValue;
+ maxElement.min = maxMinValue;
+ maxElement.max = lastValue;
+
+ /* defaultZoom stuff */
+
+ let defaultElement = document.getElementById("defaultZoom");
+
+ defaultElement.min = Services.prefs.getIntPref("zoom.minPercent");
+ defaultElement.max = Services.prefs.getIntPref("zoom.maxPercent");
+
+ var zoomValue = Services.contentPrefs2
+ .getCachedGlobal("browser.content.full-zoom", null);
+ if (zoomValue && zoomValue.value) {
+ defaultElement.value = Math.round(zoomValue.value * 100);
+ return;
+ }
+
+ defaultElement.value = 100;
+ Services.contentPrefs2.getGlobal("browser.content.full-zoom", null, {
+ handleResult(pref) {
+ defaultElement.value = Math.round(pref.value * 100);
+ },
+ handleCompletion(reason) {}
+ });
+}
+
+/**
+ * Suspend "min" value while manually typing in a number.
+ */
+function DisableMinCheck(element)
+{
+ element.min = 0;
+}
+
+/**
+ * Modify the maxZoom setting if minZoom was chosen to be larger than it.
+ */
+function AdjustMaxZoom()
+{
+ let minElement = document.getElementById("minZoom");
+ let maxElement = document.getElementById("maxZoom");
+ let maxPref = document.getElementById("zoom.maxPercent");
+
+ if(minElement.valueNumber > maxElement.valueNumber)
+ maxPref.value = minElement.value;
+
+ minElement.min = minMinValue;
+
+ let defaultElement = document.getElementById("defaultZoom");
+ if (defaultElement.valueNumber < minElement.valueNumber) {
+ defaultElement.valueNumber = minElement.valueNumber;
+ SetDefaultZoom();
+ }
+ defaultElement.min = minElement.valueNumber;
+}
+
+/**
+ * Modify the minZoom setting if maxZoom was chosen to be smaller than it,
+ * adjusting maxZoom first if it's below maxMinValue.
+ */
+function AdjustMinZoom()
+{
+ let minElement = document.getElementById("minZoom");
+ let maxElement = document.getElementById("maxZoom");
+ let minPref = document.getElementById("zoom.minPercent");
+ let maxValue = maxElement.valueNumber < maxMinValue ?
+ maxMinValue : maxElement.valueNumber;
+
+ if(maxValue < minElement.valueNumber)
+ minPref.value = maxValue;
+
+ maxElement.min = maxMinValue;
+
+ let defaultElement = document.getElementById("defaultZoom");
+ if (defaultElement.valueNumber > maxElement.valueNumber) {
+ defaultElement.valueNumber = maxElement.valueNumber;
+ SetDefaultZoom();
+ }
+ defaultElement.max = maxElement.valueNumber;
+}
+
+/**
+ * Set default zoom.
+ */
+function SetDefaultZoom()
+{
+ let defaultElement = document.getElementById("defaultZoom");
+
+ if (defaultElement.valueNumber == 100) {
+ Services.contentPrefs2.removeGlobal("browser.content.full-zoom", null);
+ return;
+ }
+
+ let new_value = defaultElement.valueNumber / 100.;
+ Services.contentPrefs2.setGlobal("browser.content.full-zoom", new_value,
+ null);
+}
+
+/**
+ * When the user toggles the layers.acceleration.disabled pref,
+ * sync its new value to the gfx.direct2d.disabled pref too.
+ */
+function updateHardwareAcceleration(aVal)
+{
+ if (AppConstants.platform == "win") {
+ document.getElementById("gfx.direct2d.disabled").value = aVal;
+ }
+}
diff --git a/comm/suite/components/pref/content/pref-content.xul b/comm/suite/components/pref/content/pref-content.xul
new file mode 100644
index 0000000000..513027c03c
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-content.xul
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD;
+ <!ENTITY % prefContentDTD SYSTEM "chrome://communicator/locale/pref/pref-content.dtd"> %prefContentDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="content_pane"
+ label="&pref.content.title;"
+ script="chrome://communicator/content/pref/pref-content.js">
+
+ <preferences id="content_preferences">
+ <preference id="general.autoScroll"
+ name="general.autoScroll"
+ type="bool"/>
+ <preference id="general.smoothScroll"
+ name="general.smoothScroll"
+ type="bool"/>
+ <preference id="zoom.minPercent"
+ name="zoom.minPercent"
+ type="int"/>
+ <preference id="zoom.maxPercent"
+ name="zoom.maxPercent"
+ type="int"/>
+ <preference id="browser.zoom.full"
+ name="browser.zoom.full"
+ type="bool" inverted="true"/>
+ <preference id="browser.zoom.siteSpecific"
+ name="browser.zoom.siteSpecific"
+ type="bool"/>
+ <preference id="browser.zoom.showZoomStatusPanel"
+ name="browser.zoom.showZoomStatusPanel"
+ type="bool"/>
+ <preference id="browser.enable_automatic_image_resizing"
+ name="browser.enable_automatic_image_resizing"
+ type="bool"/>
+ <preference id="gfx.direct2d.disabled"
+ name="gfx.direct2d.disabled"
+ type="bool" inverted="true"/>
+ <preference id="layers.acceleration.disabled"
+ name="layers.acceleration.disabled"
+ type="bool" inverted="true"
+ onchange="updateHardwareAcceleration(this.value);"/>
+ </preferences>
+
+ <description>&pref.content.description;</description>
+
+ <groupbox id="scrollingGroup" align="start">
+ <caption label="&scrolling.label;"/>
+
+ <checkbox id="useAutoScroll"
+ label="&useAutoScroll.label;"
+ accesskey="&useAutoScroll.accesskey;"
+ preference="general.autoScroll"/>
+ <checkbox id="useSmoothScroll"
+ label="&useSmoothScroll.label;"
+ accesskey="&useSmoothScroll.accesskey;"
+ preference="general.smoothScroll"/>
+ </groupbox>
+
+ <groupbox id="zoomPreferences" align="start">
+ <caption label="&zoomPrefs.label;"/>
+
+ <hbox align="center">
+ <label value="&minZoom.label;"
+ accesskey="&minZoom.accesskey;"
+ control="minZoom"/>
+ <textbox id="minZoom"
+ type="number"
+ size="3"
+ increment="10"
+ preference="zoom.minPercent"
+ oninput="DisableMinCheck(this);"
+ onchange="AdjustMaxZoom();"/>
+ <label value="&maxZoom.label;"
+ accesskey="&maxZoom.accesskey;"
+ control="maxZoom"/>
+ <textbox id="maxZoom"
+ type="number"
+ size="3"
+ increment="10"
+ preference="zoom.maxPercent"
+ oninput="DisableMinCheck(this);"
+ onchange="AdjustMinZoom();"/>
+ <label value="&percent.label;"/>
+ </hbox>
+
+ <checkbox id="textZoomOnly"
+ label="&textZoomOnly.label;"
+ accesskey="&textZoomOnly.accesskey;"
+ preference="browser.zoom.full"/>
+ <checkbox id="zoomSiteSpecific"
+ label="&siteSpecific.label;"
+ accesskey="&siteSpecific.accesskey;"
+ preference="browser.zoom.siteSpecific"/>
+ <checkbox id="showZoomStatusPanel"
+ label="&showZoomStatusPanel.label;"
+ accesskey="&showZoomStatusPanel.accesskey;"
+ preference="browser.zoom.showZoomStatusPanel"/>
+ <checkbox id="enableAutomaticImageResizing"
+ label="&enableAutomaticImageResizing.label;"
+ accesskey="&enableAutomaticImageResizing.accesskey;"
+ preference="browser.enable_automatic_image_resizing"/>
+
+ <hbox align="center">
+ <label value="&defaultZoom.label;"
+ accesskey="&defaultZoom.accesskey;"
+ control="defaultZoom"/>
+ <textbox id="defaultZoom"
+ type="number"
+ size="3"
+ increment="10"
+ onchange="SetDefaultZoom();"/>
+ <label value="&percent.label;"/>
+ </hbox>
+ </groupbox>
+
+ <vbox class="box-padded" align="start">
+ <checkbox id="allowHWAccel"
+ label="&allowHWAccel.label;"
+ accesskey="&allowHWAccel.accesskey;"
+ preference="layers.acceleration.disabled"/>
+ </vbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-cookies.js b/comm/suite/components/pref/content/pref-cookies.js
new file mode 100644
index 0000000000..fd51881735
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-cookies.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ SetDisables(false);
+}
+
+function SetDisables(aSetFocus)
+{
+ // Policy 1 was "ask before accepting" and is no longer valid.
+
+ // const for Cookie Accept Policy
+ const kCookiesDisabled = 2;
+ // const for Cookie Lifetime Policy
+ const kAcceptForNDays = 3;
+
+ var behavior = document.getElementById("networkCookieBehavior");
+ var behaviorPref = document.getElementById(behavior.getAttribute("preference"));
+
+ var lifetime = document.getElementById("networkCookieLifetime");
+ var lifetimePref = document.getElementById(lifetime.getAttribute("preference"));
+ var days = document.getElementById("lifetimeDays");
+ var daysPref = document.getElementById(days.getAttribute("preference"));
+
+ var cookiesDisabled = (behaviorPref.value == kCookiesDisabled);
+ lifetime.disabled = cookiesDisabled || lifetimePref.locked;
+ days.disabled = cookiesDisabled || daysPref.locked ||
+ (lifetimePref.value != kAcceptForNDays);
+
+ if (!days.disabled && aSetFocus)
+ days.focus();
+}
diff --git a/comm/suite/components/pref/content/pref-cookies.xul b/comm/suite/components/pref/content/pref-cookies.xul
new file mode 100644
index 0000000000..c41be21dc7
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-cookies.xul
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-cookies.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="cookies_pane"
+ label="&pref.cookies.title;"
+ script="chrome://communicator/content/pref/pref-cookies.js">
+ <preferences id="cookies_preferences">
+ <preference id="network.cookie.cookieBehavior"
+ name="network.cookie.cookieBehavior"
+ type="int"
+ onchange="SetDisables(false);"/>
+ <preference id="network.cookie.lifetimePolicy"
+ name="network.cookie.lifetimePolicy"
+ type="int"
+ onchange="SetDisables(this.value == '3');"/>
+ <preference id="network.cookie.lifetime.days"
+ name="network.cookie.lifetime.days"
+ type="int"/>
+ <preference id="pref.advanced.cookies.disable_button.view_cookies"
+ name="pref.advanced.cookies.disable_button.view_cookies"
+ type="bool"/>
+ </preferences>
+
+ <groupbox id="networkCookieAcceptPolicy">
+ <caption label="&cookiePolicy.label;"/>
+ <radiogroup id="networkCookieBehavior"
+ preference="network.cookie.cookieBehavior">
+ <radio value="2"
+ label="&disableCookies.label;"
+ accesskey="&disableCookies.accesskey;"/>
+ <radio value="1"
+ label="&accNo3rdPartyCookies.label;"
+ accesskey="&accNo3rdPartyCookies.accesskey;"/>
+ <radio value="3"
+ label="&acc3rdPartyVisited.label;"
+ accesskey="&acc3rdPartyVisited.accesskey;"/>
+ <radio value="0"
+ label="&accAllCookies.label;"
+ accesskey="&accAllCookies.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ <groupbox id="networkCookieLifetimePolicy">
+ <caption label="&cookieRetentionPolicy.label;"/>
+ <radiogroup id="networkCookieLifetime"
+ preference="network.cookie.lifetimePolicy">
+ <radio value="0"
+ label="&acceptNormally.label;"
+ accesskey="&acceptNormally.accesskey;"/>
+ <radio value="2"
+ label="&acceptForSession.label;"
+ accesskey="&acceptForSession.accesskey;"/>
+ <hbox align="center">
+ <radio id="acceptForNDays"
+ value="3"
+ label="&acceptforNDays.label;"
+ accesskey="&acceptforNDays.accesskey;"
+ aria-labelledby="acceptForNDays lifetimeDays daysLabel"/>
+ <textbox id="lifetimeDays"
+ type="number"
+ max="999"
+ min="0"
+ size="3"
+ maxlength="3"
+ preference="network.cookie.lifetime.days"
+ aria-labelledby="acceptForNDays lifetimeDays daysLabel"/>
+ <label id="daysLabel"
+ value="&days.label;"/>
+ </hbox>
+ </radiogroup>
+ </groupbox>
+ <groupbox id="manageCookiesAndSites">
+ <caption label="&manageCookies.label;"/>
+ <description>&manageCookiesDescription.label;</description>
+ <hbox pack="end">
+ <button id="viewCookieButton"
+ label="&viewCookies.label;"
+ accesskey="&viewCookies.accesskey;"
+ preference="pref.advanced.cookies.disable_button.view_cookies"
+ oncommand="toDataManager('|cookies');"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-debugging.js b/comm/suite/components/pref/content/pref-debugging.js
new file mode 100644
index 0000000000..269a0afeac
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-debugging.js
@@ -0,0 +1,15 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- *
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup() {
+ let paintFlashing = document.getElementById("nglayout.debug.paint_flashing");
+ enableFlashingChrome(paintFlashing.value);
+}
+
+function enableFlashingChrome(aValue) {
+ var paintFlashingChrome = document.getElementById("nglayoutDebugPaintFlashingChrome");
+
+ paintFlashingChrome.disabled = aValue;
+}
diff --git a/comm/suite/components/pref/content/pref-debugging.xul b/comm/suite/components/pref/content/pref-debugging.xul
new file mode 100644
index 0000000000..8e42f6f756
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-debugging.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-debugging.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="debugging_pane"
+ label="&pref.debugging.title;"
+ script="chrome://communicator/content/pref/pref-debugging.js">
+
+ <preferences id="debugging_preferences">
+ <preference id="nglayout.debug.paint_flashing"
+ name="nglayout.debug.paint_flashing"
+ type="bool"
+ onchange="enableFlashingChrome(this.value);"/>
+ <preference id="nglayout.debug.paint_flashing_chrome"
+ name="nglayout.debug.paint_flashing_chrome"
+ type="bool"/>
+ <preference id="nglayout.debug.paint_dumping"
+ name="nglayout.debug.paint_dumping"
+ type="bool"/>
+ <preference id="nglayout.debug.invalidate_dumping"
+ name="nglayout.debug.invalidate_dumping"
+ type="bool"/>
+ <preference id="nglayout.debug.event_dumping"
+ name="nglayout.debug.event_dumping"
+ type="bool"/>
+ <preference id="nglayout.debug.motion_event_dumping"
+ name="nglayout.debug.motion_event_dumping"
+ type="bool"/>
+ <preference id="nglayout.debug.crossing_event_dumping"
+ name="nglayout.debug.crossing_event_dumping"
+ type="bool"/>
+ <preference id="layout.reflow.showframecounts"
+ name="layout.reflow.showframecounts"
+ type="bool"/>
+ <preference id="layout.reflow.dumpframecounts"
+ name="layout.reflow.dumpframecounts"
+ type="bool"/>
+ <preference id="layout.reflow.dumpframebyframecounts"
+ name="layout.reflow.dumpframebyframecounts"
+ type="bool"/>
+ <preference id="xul.debug.box"
+ name="xul.debug.box"
+ type="bool"/>
+ <preference id="nglayout.debug.disable_xul_cache"
+ name="nglayout.debug.disable_xul_cache"
+ type="bool"/>
+ </preferences>
+
+ <hbox>
+ <!-- Event Debugging -->
+ <groupbox id="eventDebugging" align="start" flex="1">
+ <caption label="&debugEvents.label;"/>
+ <checkbox id="nglayoutDebugPaintFlashing"
+ label="&debugPaintFlashing.label;"
+ accesskey="&debugPaintFlashing.accesskey;"
+ preference="nglayout.debug.paint_flashing"/>
+ <checkbox id="nglayoutDebugPaintFlashingChrome"
+ label="&debugPaintFlashingChrome.label;"
+ accesskey="&debugPaintFlashingChrome.accesskey;"
+ preference="nglayout.debug.paint_flashing_chrome"/>
+ <checkbox id="nglayoutDebugPaintDumping"
+ label="&debugPaintDumping.label;"
+ accesskey="&debugPaintDumping.accesskey;"
+ preference="nglayout.debug.paint_dumping"/>
+ <checkbox id="nglayoutDebugInvalidateDumping"
+ label="&debugInvalidateDumping.label;"
+ accesskey="&debugInvalidateDumping.accesskey;"
+ preference="nglayout.debug.invalidate_dumping"/>
+ <checkbox id="nglayoutDebugEventDumping"
+ label="&debugEventDumping.label;"
+ accesskey="&debugEventDumping.accesskey;"
+ preference="nglayout.debug.event_dumping"/>
+ <checkbox id="nglayoutDebugMotionEventDumping"
+ label="&debugMotionEventDumping.label;"
+ accesskey="&debugMotionEventDumping.accesskey;"
+ preference="nglayout.debug.motion_event_dumping"/>
+ <checkbox id="nglayoutDebugCrossingEventDumping"
+ label="&debugCrossingEventDumping.label;"
+ accesskey="&debugCrossingEventDumping.accesskey;"
+ preference="nglayout.debug.crossing_event_dumping"/>
+ </groupbox>
+
+ <vbox align="start" flex="1">
+ <!-- Reflow Event Debugging -->
+ <groupbox id="reflowEventDebugging">
+ <caption label="&debugReflowEvents.label;"/>
+ <checkbox id="layoutReflowShowFrameCounts"
+ label="&debugReflowShowFrameCounts.label;"
+ accesskey="&debugReflowShowFrameCounts.accesskey;"
+ preference="layout.reflow.showframecounts"/>
+ <checkbox id="layoutReflowDumpFrameCounts"
+ label="&debugReflowDumpFrameCounts.label;"
+ accesskey="&debugReflowDumpFrameCounts.accesskey;"
+ preference="layout.reflow.dumpframecounts"/>
+ <checkbox id="layoutReflowDumpFrameByFrameCounts"
+ label="&debugReflowDumpFrameByFrameCounts.label;"
+ accesskey="&debugReflowDumpFrameByFrameCounts.accesskey;"
+ preference="layout.reflow.dumpframebyframecounts"/>
+ </groupbox>
+
+ <!-- Render Debugging -->
+ <groupbox id="renderDebugging">
+ <caption label="&debugRendering.label;"/>
+ <checkbox id="debugXULBoxes"
+ label="&debugXULBox.label;"
+ accesskey="&debugXULBox.accesskey;"
+ preference="xul.debug.box"/>
+ <checkbox id="nglayoutDebugDisableXULCache"
+ label="&debugDisableXULCache.label;"
+ accesskey="&debugDisableXULCache.accesskey;"
+ preference="nglayout.debug.disable_xul_cache"/>
+ </groupbox>
+ </vbox>
+ </hbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-download.js b/comm/suite/components/pref/content/pref-download.js
new file mode 100644
index 0000000000..bc52f800b6
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-download.js
@@ -0,0 +1,197 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { FileUtils } =
+ ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+
+const kDesktop = 0;
+const kDownloads = 1;
+const kUserDir = 2;
+var gFPHandler;
+var gSoundUrlPref;
+
+function Startup()
+{
+ // Define globals
+ gFPHandler = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ gSoundUrlPref = document.getElementById("browser.download.finished_sound_url");
+ setSoundEnabled(document.getElementById("browser.download.finished_download_sound").value);
+}
+
+/**
+ * Enables/disables the folder field and Browse button based on whether a
+ * default download directory is being used.
+ */
+function readUseDownloadDir()
+{
+ var downloadFolder = document.getElementById("downloadFolder");
+ var chooseFolder = document.getElementById("chooseFolder");
+ var preference = document.getElementById("browser.download.useDownloadDir");
+ downloadFolder.disabled = !preference.value;
+ chooseFolder.disabled = !preference.value;
+}
+
+/**
+ * Displays a file picker in which the user can choose the location where
+ * downloads are automatically saved, updating preferences and UI in
+ * response to the choice, if one is made.
+ */
+function chooseFolder()
+{
+ return chooseFolderTask().catch(Cu.reportError);
+}
+
+async function chooseFolderTask()
+{
+ let title = document.getElementById("bundle_prefutilities")
+ .getString("downloadfolder");
+ let folderListPref = document.getElementById("browser.download.folderList");
+ let currentDirPref = await _indexToFolder(folderListPref.value);
+ let defDownloads = await _indexToFolder(kDownloads);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.init(window, title, Ci.nsIFilePicker.modeGetFolder);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ // First try to open what's currently configured
+ if (currentDirPref && currentDirPref.exists()) {
+ fp.displayDirectory = currentDirPref;
+ } else if (defDownloads && defDownloads.exists()) {
+ // Try the system's download dir
+ fp.displayDirectory = defDownloads;
+ } else {
+ // Fall back to Desktop
+ fp.displayDirectory = await _indexToFolder(kDesktop);
+ }
+
+ let result = await new Promise(resolve => fp.open(resolve));
+ if (result != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ document.getElementById("browser.download.dir").value = fp.file;
+ folderListPref.value = await _folderToIndex(fp.file);
+ // Note, the real prefs will not be updated yet, so dnld manager's
+ // userDownloadsDirectory may not return the right folder after
+ // this code executes. displayDownloadDirPref will be called on
+ // the assignment above to update the UI.
+}
+
+/**
+ * Initializes the download folder display settings based on the user's
+ * preferences.
+ */
+function displayDownloadDirPref()
+{
+ displayDownloadDirPrefTask().catch(Cu.reportError);
+}
+
+async function displayDownloadDirPrefTask()
+{
+ var folderListPref = document.getElementById("browser.download.folderList");
+ var currentDirPref = await _indexToFolder(folderListPref.value); // file
+ var prefutilitiesBundle = document.getElementById("bundle_prefutilities");
+ var iconUrlSpec = gFPHandler.getURLSpecFromFile(currentDirPref);
+ var downloadFolder = document.getElementById("downloadFolder");
+ downloadFolder.image = "moz-icon://" + iconUrlSpec + "?size=16";
+
+ // Display a 'pretty' label or the path in the UI.
+ switch (folderListPref.value) {
+ case kDesktop:
+ downloadFolder.label = prefutilitiesBundle.getString("desktopFolderName");
+ break;
+ case kDownloads:
+ downloadFolder.label = prefutilitiesBundle.getString("downloadsFolderName");
+ break;
+ default:
+ downloadFolder.label = currentDirPref ? currentDirPref.path : "";
+ break;
+ }
+}
+
+/**
+ * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads
+ * folder returned is the desktop folder; otherwise, it is a folder whose name
+ * indicates that it is a download folder and whose path is as determined by
+ * the XPCOM directory service via the download manager's attribute
+ * defaultDownloadsDirectory.
+ *
+ * @throws if aFolder is not "Desktop" or "Downloads"
+ */
+async function _getDownloadsFolder(aFolder)
+{
+ switch (aFolder) {
+ case "Desktop":
+ return Services.dirsvc.get("Desk", Ci.nsIFile);
+ case "Downloads":
+ let downloadsDir = await Downloads.getSystemDownloadsDirectory();
+ return new FileUtils.File(downloadsDir);
+ }
+ throw "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'";
+}
+
+/**
+ * Determines the type of the given folder.
+ *
+ * @param aFolder
+ * the folder whose type is to be determined
+ * @returns integer
+ * kDesktop if aFolder is the Desktop or is unspecified,
+ * kDownloads if aFolder is the Downloads folder,
+ * kUserDir otherwise
+ */
+async function _folderToIndex(aFolder)
+{
+ if (!aFolder || aFolder.equals(await _getDownloadsFolder("Desktop"))) {
+ return kDesktop;
+ }
+
+ if (aFolder.equals(await _getDownloadsFolder("Downloads"))) {
+ return kDownloads;
+ }
+
+ return kUserDir;
+ }
+
+/**
+ * Converts an integer into the corresponding folder.
+ *
+ * @param aIndex
+ * an integer
+ * @returns the Desktop folder if aIndex == kDesktop,
+ * the Downloads folder if aIndex == kDownloads,
+ * the folder stored in browser.download.dir
+ */
+async function _indexToFolder(aIndex)
+{
+ var folder;
+ switch (aIndex) {
+ case kDownloads:
+ folder = await _getDownloadsFolder("Downloads");
+ break;
+ case kDesktop:
+ folder = await _getDownloadsFolder("Desktop");
+ break;
+ default:
+ folder = document.getElementById("browser.download.dir").value;
+ break;
+ }
+ if (!folder ||
+ !folder.exists()) {
+ return "";
+ }
+
+ return folder;
+}
+
+function setSoundEnabled(aEnable)
+{
+ EnableElementById("downloadSndURL", aEnable, false);
+ document.getElementById("downloadSndPlay").disabled = !aEnable;
+}
diff --git a/comm/suite/components/pref/content/pref-download.xul b/comm/suite/components/pref/content/pref-download.xul
new file mode 100644
index 0000000000..e3f626204c
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-download.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefDownloadDTD SYSTEM "chrome://communicator/locale/pref/pref-download.dtd">
+%prefDownloadDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="download_pane"
+ label="&pref.download.title;"
+ script="chrome://communicator/content/pref/pref-download.js">
+
+ <preferences>
+ <preference id="browser.download.manager.behavior"
+ name="browser.download.manager.behavior"
+ type="int"/>
+ <preference id="browser.download.manager.focusWhenStarting"
+ name="browser.download.manager.focusWhenStarting"
+ type="bool" inverted="true"/>
+ <preference id="browser.download.useDownloadDir"
+ name="browser.download.useDownloadDir"
+ type="bool"/>
+ <preference id="browser.download.dir"
+ name="browser.download.dir"
+ type="file"/>
+ <preference id="browser.download.folderList"
+ name="browser.download.folderList"
+ type="int"
+ onchange="displayDownloadDirPref();"/>
+ <preference id="browser.download.finished_download_sound"
+ name="browser.download.finished_download_sound"
+ type="bool"
+ onchange="setSoundEnabled(this.value);"/>
+ <preference id="browser.download.finished_sound_url"
+ name="browser.download.finished_sound_url"
+ type="string"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&downloadBehavior.label;"/>
+ <radiogroup id="downloadBehavior"
+ preference="browser.download.manager.behavior">
+ <radio value="2"
+ label="&doNothing.label;"
+ accesskey="&doNothing.accesskey;"/>
+ <radio value="1"
+ label="&openProgressDialog.label;"
+ accesskey="&openProgressDialog.accesskey;"/>
+ <radio value="0"
+ label="&openDM.label;"
+ accesskey="&openDM.accesskey;"/>
+ </radiogroup>
+ <checkbox id="focusWhenStarting"
+ class="indent"
+ preference="browser.download.manager.focusWhenStarting"
+ label="&flashWhenOpen.label;"
+ accesskey="&flashWhenOpen.accesskey;"/>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&downloadLocation.label;"/>
+ <radiogroup id="saveWhere"
+ preference="browser.download.useDownloadDir"
+ onsyncfrompreference="return document.getElementById('download_pane').readUseDownloadDir();">
+ <hbox id="saveToRow">
+ <radio id="saveTo" value="true"
+ label="&saveTo.label;"
+ accesskey="&saveTo.accesskey;"
+ aria-labelledby="saveTo downloadFolder"/>
+ <filefield id="downloadFolder" flex="1"
+ preference="browser.download.dir"
+ preference-editable="true"
+ aria-labelledby="saveTo"
+ onsyncfrompreference="document.getElementById('download_pane').displayDownloadDirPref();"/>
+ <button id="chooseFolder" oncommand="chooseFolder();"
+ label="&chooseDownloadFolder.label;"
+ accesskey="&chooseDownloadFolder.accesskey;"/>
+ </hbox>
+ <radio id="alwaysAsk" value="false"
+ label="&alwaysAsk.label;"
+ accesskey="&alwaysAsk.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&finishedBehavior.label;"/>
+ <hbox align="center">
+ <checkbox id="finishedNotificationSound"
+ label="&playSound.label;"
+ preference="browser.download.finished_download_sound"
+ accesskey="&playSound.accesskey;"/>
+ </hbox>
+
+ <hbox align="center" class="indent">
+ <filefield id="downloadSndURL"
+ flex="1"
+ preference="browser.download.finished_sound_url"
+ preference-editable="true"
+ onsyncfrompreference="return WriteSoundField(this, document.getElementById('download_pane').gSoundUrlPref.value);"/>
+ <hbox align="center">
+ <button id="downloadSndBrowse"
+ label="&browse.label;"
+ accesskey="&browse.accesskey;"
+ oncommand="SelectSound(gSoundUrlPref);">
+ <observes element="downloadSndURL" attribute="disabled"/>
+ </button>
+ <button id="downloadSndPlay"
+ label="&playButton.label;"
+ accesskey="&playButton.accesskey;"
+ oncommand="PlaySound(gSoundUrlPref.value, false);"/>
+ </hbox>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-findasyoutype.js b/comm/suite/components/pref/content/pref-findasyoutype.js
new file mode 100644
index 0000000000..fefd5a0d46
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-findasyoutype.js
@@ -0,0 +1,15 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ var prefAutostart = document.getElementById("accessibility.typeaheadfind.autostart");
+ SetLinksOnlyEnabled(prefAutostart.value);
+}
+
+function SetLinksOnlyEnabled(aEnable)
+{
+ EnableElementById("findAsYouTypeAutoWhat", aEnable, false);
+}
diff --git a/comm/suite/components/pref/content/pref-findasyoutype.xul b/comm/suite/components/pref/content/pref-findasyoutype.xul
new file mode 100644
index 0000000000..612b3fa768
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-findasyoutype.xul
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-findasyoutype.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="findasyoutype_pane"
+ label="&pref.findAsYouType.title;"
+ script="chrome://communicator/content/pref/pref-findasyoutype.js">
+
+ <preferences id="findasyoutype_preferences">
+ <preference id="accessibility.typeaheadfind.autostart"
+ name="accessibility.typeaheadfind.autostart"
+ onchange="SetLinksOnlyEnabled(this.value);"
+ type="bool"/>
+ <preference id="accessibility.typeaheadfind.linksonly"
+ name="accessibility.typeaheadfind.linksonly"
+ type="bool"/>
+ <preference id="accessibility.typeaheadfind.enablesound"
+ name="accessibility.typeaheadfind.enablesound"
+ type="bool"/>
+ <preference id="accessibility.typeaheadfind.enabletimeout"
+ name="accessibility.typeaheadfind.enabletimeout"
+ type="bool"/>
+ <preference id="accessibility.typeaheadfind.usefindbar"
+ name="accessibility.typeaheadfind.usefindbar"
+ type="bool"/>
+ </preferences>
+
+ <groupbox align="start">
+ <caption label="&findAsYouTypeBehavior.label;"/>
+ <checkbox id="findAsYouTypeEnableAuto"
+ label="&findAsYouTypeEnableAuto.label;"
+ accesskey="&findAsYouTypeEnableAuto.accesskey;"
+ preference="accessibility.typeaheadfind.autostart"/>
+ <radiogroup id="findAsYouTypeAutoWhat"
+ class="indent"
+ preference="accessibility.typeaheadfind.linksonly">
+ <radio value="false"
+ label="&findAsYouTypeAutoText.label;"
+ accesskey="&findAsYouTypeAutoText.accesskey;"/>
+ <radio value="true"
+ label="&findAsYouTypeAutoLinks.label;"
+ accesskey="&findAsYouTypeAutoLinks.accesskey;"/>
+ </radiogroup>
+ <description>&findAsYouTypeTip.label;</description>
+
+ <vbox class="box-padded"
+ align="start">
+ <separator class="thin" />
+ <checkbox id="findAsYouTypeSound"
+ label="&findAsYouTypeSound.label;"
+ accesskey="&findAsYouTypeSound.accesskey;"
+ preference="accessibility.typeaheadfind.enablesound"/>
+ <checkbox id="findAsYouTypeTimeout"
+ label="&findAsYouTypeTimeout.label;"
+ accesskey="&findAsYouTypeTimeout.accesskey;"
+ preference="accessibility.typeaheadfind.enabletimeout"/>
+ <checkbox id="findAsYouTypeFindbarEnable"
+ label="&findAsYouTypeFindbarEnable.label;"
+ accesskey="&findAsYouTypeFindbarEnable.accesskey;"
+ preference="accessibility.typeaheadfind.usefindbar"/>
+ </vbox>
+ <description>&findAsYouTypeFindbarEnableTip.label;</description>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-fonts.js b/comm/suite/components/pref/content/pref-fonts.js
new file mode 100644
index 0000000000..aa226c89af
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-fonts.js
@@ -0,0 +1,220 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gAllFonts = null;
+var gFontEnumerator = null;
+var gDisabled = false;
+
+function GetFontEnumerator()
+{
+ if (!gFontEnumerator)
+ {
+ gFontEnumerator = Cc["@mozilla.org/gfx/fontenumerator;1"]
+ .createInstance(Ci.nsIFontEnumerator);
+ }
+ return gFontEnumerator;
+}
+
+function BuildFontList(aLanguage, aFontType, aMenuList, aPreference)
+{
+ var defaultFont = null;
+ // Load Font Lists
+ var fonts = GetFontEnumerator().EnumerateFonts(aLanguage, aFontType);
+ if (fonts.length)
+ {
+ defaultFont = GetFontEnumerator().getDefaultFont(aLanguage, aFontType);
+ }
+ else
+ {
+ fonts = GetFontEnumerator().EnumerateFonts(aLanguage, "");
+ if (fonts.length)
+ defaultFont = GetFontEnumerator().getDefaultFont(aLanguage, "");
+ }
+
+ if (!gAllFonts)
+ gAllFonts = GetFontEnumerator().EnumerateAllFonts();
+
+ // Reset the list
+ while (aMenuList.hasChildNodes())
+ aMenuList.lastChild.remove();
+
+ // Build the UI for the Default Font and Fonts for this CSS type.
+ var popup = document.createElement("menupopup");
+ var separator;
+ if (fonts.length > 0)
+ {
+ const prefutilitiesBundle = document.getElementById("bundle_prefutilities");
+ let label = defaultFont ?
+ prefutilitiesBundle.getFormattedString("labelDefaultFont2", [defaultFont]) :
+ prefutilitiesBundle.getString("labelDefaultFontUnnamed");
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("value", ""); // Default Font has a blank value
+ popup.appendChild(menuitem);
+
+ separator = document.createElement("menuseparator");
+ popup.appendChild(separator);
+
+ for (let font of fonts)
+ {
+ menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("value", font);
+ menuitem.setAttribute("label", font);
+ popup.appendChild(menuitem);
+ }
+ }
+
+ // Build the UI for the remaining fonts.
+ if (gAllFonts.length > fonts.length)
+ {
+ // Both lists are sorted, and the Fonts-By-Type list is a subset of the
+ // All-Fonts list, so walk both lists side-by-side, skipping values we've
+ // already created menu items for.
+
+ if (fonts.length)
+ {
+ separator = document.createElement("menuseparator");
+ popup.appendChild(separator);
+ }
+
+ for (i = 0; i < gAllFonts.length; ++i)
+ {
+ if (fonts.lastIndexOf(gAllFonts[i], 0) == 0)
+ {
+ fonts.shift(); //Remove matched font from array
+ }
+ else
+ {
+ menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("value", gAllFonts[i]);
+ menuitem.setAttribute("label", gAllFonts[i]);
+ popup.appendChild(menuitem);
+ }
+ }
+ }
+ aMenuList.appendChild(popup);
+
+ // Fully populated so re-enable menulist before setting preference,
+ // unless panel is locked.
+ if (!gDisabled)
+ aMenuList.disabled = false;
+ aMenuList.setAttribute("preference", aPreference.id);
+ aPreference.setElementValue(aMenuList);
+}
+
+function ReadFontLanguageGroup()
+{
+ var prefs = [{format: "default", type: "string", element: "defaultFontType", fonttype: "" },
+ {format: "name.", type: "unichar", element: "serif", fonttype: "serif" },
+ {format: "name.", type: "unichar", element: "sans-serif", fonttype: "sans-serif"},
+ {format: "name.", type: "unichar", element: "monospace", fonttype: "monospace" },
+ {format: "name.", type: "unichar", element: "cursive", fonttype: "cursive" },
+ {format: "name.", type: "unichar", element: "fantasy", fonttype: "fantasy" },
+ {format: "name-list.", type: "unichar", element: null, fonttype: "serif" },
+ {format: "name-list.", type: "unichar", element: null, fonttype: "sans-serif"},
+ {format: "name-list.", type: "unichar", element: null, fonttype: "monospace" },
+ {format: "name-list.", type: "unichar", element: null, fonttype: "cursive" },
+ {format: "name-list.", type: "unichar", element: null, fonttype: "fantasy" },
+ {format: "size.variable", type: "int", element: "sizeVar", fonttype: "" },
+ {format: "size.fixed", type: "int", element: "sizeMono", fonttype: "" },
+ {format: "minimum-size", type: "int", element: "minSize", fonttype: "" }];
+ gDisabled = document.getElementById("browser.display.languageList").locked;
+ var fontLanguage = document.getElementById("font.language.group");
+ if (gDisabled)
+ fontLanguage.disabled = true;
+ var languageGroup = fontLanguage.value;
+ var preferences = document.getElementById("fonts_preferences");
+ for (var i = 0; i < prefs.length; ++i)
+ {
+ var name = "font."+ prefs[i].format + prefs[i].fonttype + "." + languageGroup;
+ var preference = document.getElementById(name);
+ if (!preference)
+ {
+ preference = document.createElement("preference");
+ preference.id = name;
+ preference.setAttribute("name", name);
+ preference.setAttribute("type", prefs[i].type);
+ preferences.appendChild(preference);
+ }
+
+ if (!prefs[i].element)
+ continue;
+
+ var element = document.getElementById(prefs[i].element);
+ if (element)
+ {
+ if (prefs[i].fonttype)
+ {
+ // Set an empty label so it does not jump when items are added.
+ element.setAttribute("label", "");
+ // Disable menulist for the moment.
+ element.disabled = true;
+ // Lazily populate font lists, each gets re-enabled at the end.
+ window.setTimeout(BuildFontList, 0, languageGroup,
+ prefs[i].fonttype, element, preference);
+ }
+ else
+ {
+ // Unless the panel is locked, make sure these elements are not
+ // disabled just in case they were in the last language group.
+ element.disabled = gDisabled;
+ element.setAttribute("preference", preference.id);
+ preference.setElementValue(element);
+ }
+ }
+ }
+}
+
+function ReadFontSelection(aElement)
+{
+ // Determine the appropriate value to select, for the following cases:
+ // - there is no setting
+ // - the font selected by the user is no longer present (e.g. deleted from
+ // fonts folder)
+ var preference = document.getElementById(aElement.getAttribute("preference"));
+ if (preference.value)
+ {
+ var fontItems = aElement.getElementsByAttribute("value", preference.value);
+
+ // There is a setting that actually is in the list. Respect it.
+ if (fontItems.length)
+ return undefined;
+ }
+
+ var defaultValue = aElement.firstChild.firstChild.getAttribute("value");
+ var languagePref = document.getElementById("font.language.group");
+ preference = document.getElementById("font.name-list." + aElement.id + "." + languagePref.value);
+ if (!preference || !preference.hasUserValue)
+ return defaultValue;
+
+ var fontNames = preference.value.split(",");
+
+ for (var i = 0; i < fontNames.length; ++i)
+ {
+ fontItems = aElement.getElementsByAttribute("value", fontNames[i].trim());
+ if (fontItems.length)
+ return fontItems[0].getAttribute("value");
+ }
+ return defaultValue;
+}
+
+function ReadFontPref(aElement, aDefaultValue)
+{
+ // Check to see if preference value exists,
+ // if not return given default value.
+ var preference = document.getElementById(aElement.getAttribute("preference"));
+ return preference.value || aDefaultValue;
+}
+
+function ReadUseDocumentFonts()
+{
+ var preference = document.getElementById("browser.display.use_document_fonts");
+ return preference.value == 1;
+}
+
+function WriteUseDocumentFonts(aUseDocumentFonts)
+{
+ return aUseDocumentFonts.checked ? 1 : 0;
+}
diff --git a/comm/suite/components/pref/content/pref-fonts.xul b/comm/suite/components/pref/content/pref-fonts.xul
new file mode 100644
index 0000000000..554f161a73
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-fonts.xul
@@ -0,0 +1,260 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-fonts.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="fonts_pane"
+ label="&pref.fonts.title;"
+ script="chrome://communicator/content/pref/pref-fonts.js">
+ <preferences id="fonts_preferences">
+ <preference id="font.language.group"
+ name="font.language.group"
+ type="wstring"/>
+ <preference id="browser.display.use_document_fonts"
+ name="browser.display.use_document_fonts"
+ type="int"/>
+ <preference id="browser.display.languageList"
+ name="browser.display.languageList"
+ type="wstring"/>
+ </preferences>
+
+ <groupbox>
+ <caption align="center">
+ <label value="&language.label;"
+ accesskey="&language.accesskey;"
+ control="selectLangs"/>
+ <menulist id="selectLangs" preference="font.language.group"
+ onsyncfrompreference="document.getElementById('fonts_pane').ReadFontLanguageGroup();">
+ <menupopup>
+ <menuitem value="ar" label="&font.langGroup.arabic;"/>
+ <menuitem value="x-armn" label="&font.langGroup.armenian;"/>
+ <menuitem value="x-beng" label="&font.langGroup.bengali;"/>
+ <menuitem value="zh-CN" label="&font.langGroup.simpl-chinese;"/>
+ <menuitem value="zh-TW" label="&font.langGroup.trad-chinese;"/>
+ <menuitem value="zh-HK" label="&font.langGroup.trad-chinese-hk;"/>
+ <menuitem value="x-cyrillic" label="&font.langGroup.cyrillic;"/>
+ <menuitem value="x-devanagari" label="&font.langGroup.devanagari;"/>
+ <menuitem value="x-ethi" label="&font.langGroup.ethiopic;"/>
+ <menuitem value="x-geor" label="&font.langGroup.georgian;"/>
+ <menuitem value="el" label="&font.langGroup.el;"/>
+ <menuitem value="x-gujr" label="&font.langGroup.gujarati;"/>
+ <menuitem value="x-guru" label="&font.langGroup.gurmukhi;"/>
+ <menuitem value="he" label="&font.langGroup.hebrew;"/>
+ <menuitem value="ja" label="&font.langGroup.japanese;"/>
+ <menuitem value="x-knda" label="&font.langGroup.kannada;"/>
+ <menuitem value="x-khmr" label="&font.langGroup.khmer;"/>
+ <menuitem value="ko" label="&font.langGroup.korean;"/>
+ <menuitem value="x-western" label="&font.langGroup.latin;"/>
+ <menuitem value="x-mlym" label="&font.langGroup.malayalam;"/>
+ <menuitem value="x-math" label="&font.langGroup.math;"/>
+ <menuitem value="x-orya" label="&font.langGroup.odia;"/>
+ <menuitem value="x-sinh" label="&font.langGroup.sinhala;"/>
+ <menuitem value="x-tamil" label="&font.langGroup.tamil;"/>
+ <menuitem value="x-telu" label="&font.langGroup.telugu;"/>
+ <menuitem value="th" label="&font.langGroup.thai;"/>
+ <menuitem value="x-tibt" label="&font.langGroup.tibetan;"/>
+ <menuitem value="x-cans" label="&font.langGroup.canadian;"/>
+ <menuitem value="x-unicode" label="&font.langGroup.other;"/>
+ </menupopup>
+ </menulist>
+ </caption>
+
+ <separator class="thin"/>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <spacer/>
+ <label value="&typefaces.label;"/>
+ <label value="&sizes.label;"/>
+ </row>
+ <row>
+ <separator class="thin"/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&proportional.label;"
+ accesskey="&proportional.accesskey;"
+ control="defaultFontType"/>
+ </hbox>
+ <menulist id="defaultFontType" flex="1" style="width: 0px;"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);">
+ <menupopup>
+ <menuitem value="serif"
+ label="&useDefaultFontSerif.label;"/>
+ <menuitem value="sans-serif"
+ label="&useDefaultFontSansSerif.label;"/>
+ </menupopup>
+ </menulist>
+ <menulist id="sizeVar" class="small-margin"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontPref(this, 16);">
+ <menupopup>
+ <menuitem value="8" label="8"/>
+ <menuitem value="9" label="9"/>
+ <menuitem value="10" label="10"/>
+ <menuitem value="11" label="11"/>
+ <menuitem value="12" label="12"/>
+ <menuitem value="13" label="13"/>
+ <menuitem value="14" label="14"/>
+ <menuitem value="15" label="15"/>
+ <menuitem value="16" label="16"/>
+ <menuitem value="17" label="17"/>
+ <menuitem value="18" label="18"/>
+ <menuitem value="20" label="20"/>
+ <menuitem value="22" label="22"/>
+ <menuitem value="24" label="24"/>
+ <menuitem value="26" label="26"/>
+ <menuitem value="28" label="28"/>
+ <menuitem value="30" label="30"/>
+ <menuitem value="32" label="32"/>
+ <menuitem value="34" label="34"/>
+ <menuitem value="36" label="36"/>
+ <menuitem value="40" label="40"/>
+ <menuitem value="44" label="44"/>
+ <menuitem value="48" label="48"/>
+ <menuitem value="56" label="56"/>
+ <menuitem value="64" label="64"/>
+ <menuitem value="72" label="72"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row>
+ <separator class="thin"/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&serif.label;"
+ accesskey="&serif.accesskey;"
+ control="serif"/>
+ </hbox>
+ <menulist id="serif" class="prefpanel-font-list"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);"/>
+ <spacer/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&sans-serif.label;"
+ accesskey="&sans-serif.accesskey;"
+ control="sans-serif"/>
+ </hbox>
+ <menulist id="sans-serif" class="prefpanel-font-list"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);"/>
+ <spacer/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&cursive.label;"
+ accesskey="&cursive.accesskey;"
+ control="cursive"/>
+ </hbox>
+ <menulist id="cursive" class="prefpanel-font-list"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);"/>
+ <spacer/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&fantasy.label;"
+ accesskey="&fantasy.accesskey;"
+ control="fantasy"/>
+ </hbox>
+ <menulist id="fantasy" class="prefpanel-font-list"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);"/>
+ <spacer/>
+ </row>
+ <row>
+ <separator class="thin"/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&monospace.label;"
+ accesskey="&monospace.accesskey;"
+ control="monospace"/>
+ </hbox>
+ <menulist id="monospace" class="prefpanel-font-list"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontSelection(this);"/>
+ <menulist id="sizeMono"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontPref(this, 12);">
+ <menupopup>
+ <menuitem value="8" label="8"/>
+ <menuitem value="9" label="9"/>
+ <menuitem value="10" label="10"/>
+ <menuitem value="11" label="11"/>
+ <menuitem value="12" label="12"/>
+ <menuitem value="13" label="13"/>
+ <menuitem value="14" label="14"/>
+ <menuitem value="15" label="15"/>
+ <menuitem value="16" label="16"/>
+ <menuitem value="17" label="17"/>
+ <menuitem value="18" label="18"/>
+ <menuitem value="20" label="20"/>
+ <menuitem value="22" label="22"/>
+ <menuitem value="24" label="24"/>
+ <menuitem value="26" label="26"/>
+ <menuitem value="28" label="28"/>
+ <menuitem value="30" label="30"/>
+ <menuitem value="32" label="32"/>
+ <menuitem value="34" label="34"/>
+ <menuitem value="36" label="36"/>
+ <menuitem value="40" label="40"/>
+ <menuitem value="44" label="44"/>
+ <menuitem value="48" label="48"/>
+ <menuitem value="56" label="56"/>
+ <menuitem value="64" label="64"/>
+ <menuitem value="72" label="72"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row>
+ <separator class="thin"/>
+ </row>
+ <row>
+ <spacer/>
+ <hbox align="center" pack="end">
+ <label value="&minSize.label;"
+ accesskey="&minSize.accesskey;"
+ control="minSize"/>
+ </hbox>
+ <menulist id="minSize"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadFontPref(this, 0);">
+ <menupopup>
+ <menuitem value="0" label="&minSize.none;"/>
+ <menuitem value="9" label="9"/>
+ <menuitem value="10" label="10"/>
+ <menuitem value="11" label="11"/>
+ <menuitem value="12" label="12"/>
+ <menuitem value="13" label="13"/>
+ <menuitem value="14" label="14"/>
+ <menuitem value="15" label="15"/>
+ <menuitem value="16" label="16"/>
+ <menuitem value="17" label="17"/>
+ <menuitem value="18" label="18"/>
+ <menuitem value="20" label="20"/>
+ <menuitem value="22" label="22"/>
+ <menuitem value="24" label="24"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <!-- Unchecking this removes the ability to select dynamic fonts -->
+ <checkbox id="browserUseDocumentFonts"
+ label="&useDocumentFonts.label;"
+ accesskey="&useDocumentFonts.accesskey;"
+ preference="browser.display.use_document_fonts"
+ onsyncfrompreference="return document.getElementById('fonts_pane').ReadUseDocumentFonts();"
+ onsynctopreference="return document.getElementById('fonts_pane').WriteUseDocumentFonts(this);"/>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-history.js b/comm/suite/components/pref/content/pref-history.js
new file mode 100644
index 0000000000..8f22073241
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-history.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ var urlbarHistButton = document.getElementById("ClearUrlBarHistoryButton");
+ var lastUrlPref = document.getElementById("general.open_location.last_url");
+ var locBarPref = document.getElementById("browser.urlbar.historyEnabled");
+
+ var isBtnDisabled = lastUrlPref.locked || !locBarPref.value;
+
+ try {
+ if (!isBtnDisabled && !lastUrlPref.hasUserValue) {
+ var file = GetUrlbarHistoryFile();
+ if (!file.exists())
+ isBtnDisabled = true;
+ else {
+ var connection = Services.storage.openDatabase(file);
+ isBtnDisabled = !connection.tableExists("urlbarhistory");
+ connection.close();
+ }
+ }
+ urlbarHistButton.disabled = isBtnDisabled;
+ }
+ catch(ex) {
+ }
+ var globalHistButton = document.getElementById("browserClearHistory");
+ var globalHistory = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+ if (!globalHistory.hasHistoryEntries)
+ globalHistButton.disabled = true;
+}
+
+function prefClearGlobalHistory()
+{
+ const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
+ PlacesUtils.history.clear();
+}
+
+function prefClearUrlbarHistory(aButton)
+{
+ document.getElementById("general.open_location.last_url").valueFromPreferences = "";
+ var file = GetUrlbarHistoryFile();
+ if (file.exists())
+ file.remove(false);
+ aButton.disabled = true;
+}
+
+function prefUrlBarHistoryToggle(aChecked)
+{
+ var file = GetUrlbarHistoryFile();
+ if (file.exists())
+ document.getElementById("ClearUrlBarHistoryButton").disabled = !aChecked;
+}
diff --git a/comm/suite/components/pref/content/pref-history.xul b/comm/suite/components/pref/content/pref-history.xul
new file mode 100644
index 0000000000..63638544c5
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-history.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-history.dtd" >
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="history_pane"
+ label="&pref.history.title;"
+ script="chrome://communicator/content/pref/pref-history.js">
+ <preferences id="history_preferences">
+ <preference id="places.history.enabled"
+ name="places.history.enabled"
+ type="bool"/>
+ <preference id="pref.browser.history.disable_button.clear_hist"
+ name="pref.browser.history.disable_button.clear_hist"
+ type="bool"/>
+ <preference id="pref.browser.history.disable_button.clear_urlbar"
+ name="pref.browser.history.disable_button.clear_urlbar"
+ type="bool"/>
+ <preference id="browser.urlbar.historyEnabled"
+ name="browser.urlbar.historyEnabled"
+ type="bool"/>
+ <preference id="general.open_location.last_url"
+ name="general.open_location.last_url"
+ type="string"/>
+ <preference id="browser.formfill.enable"
+ name="browser.formfill.enable"
+ type="bool"/>
+ <preference id="browser.formfill.expire_days"
+ name="browser.formfill.expire_days"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.history.caption;"/>
+ <hbox align="center">
+ <description flex="1">&historyPages.label;</description>
+ <hbox align="center"
+ pack="end">
+ <button label="&clearHistory.label;"
+ accesskey="&clearHistory.accesskey;"
+ oncommand="prefClearGlobalHistory();"
+ id="browserClearHistory"
+ preference="pref.browser.history.disable_button.clear_hist"/>
+ </hbox>
+ </hbox>
+ <checkbox id="histEnable"
+ label="&enableHistory.label;"
+ accesskey="&enableHistory.accesskey;"
+ preference="places.history.enabled"/>
+ </groupbox>
+
+ <!-- no honey, I haven't been viewing porn, honest! -->
+ <groupbox>
+ <caption label="&locationBarHistory.caption;"/>
+ <hbox align="center">
+ <vbox pack="end">
+ <checkbox id="urlbarHistoryEnabled"
+ label="&urlBarHistoryEnabled.caption;"
+ accesskey="&urlBarHistoryEnabled.accesskey;"
+ preference="browser.urlbar.historyEnabled"
+ oncommand="prefUrlBarHistoryToggle(this.checked);"/>
+ <hbox align="center"
+ pack="end">
+ <description flex="1">&clearLocationBar.label;</description>
+ <button id="ClearUrlBarHistoryButton"
+ label="&clearLocationBarButton.label;"
+ accesskey="&clearLocationBarButton.accesskey;"
+ oncommand="prefClearUrlbarHistory(this); this.disabled = true;"
+ preference="pref.browser.history.disable_button.clear_urlbar"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </groupbox>
+
+ <!-- form history -->
+ <groupbox>
+ <caption label="&formfillHistory.caption;"/>
+ <checkbox id="formfillEnable"
+ label="&enableFormfill.label;"
+ accesskey="&enableFormfill.accesskey;"
+ preference="browser.formfill.enable"/>
+ <hbox align="center">
+ <label value="&formfillExpire.label;"
+ accesskey="&formfillExpire.accesskey;"
+ control="formfillDay"/>
+ <textbox id="formfillDay"
+ type="number"
+ size="4"
+ preference="browser.formfill.expire_days"/>
+ <label value="&formfillDays.label;"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-http.js b/comm/suite/components/pref/content/pref-http.js
new file mode 100644
index 0000000000..eb04b9f274
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-http.js
@@ -0,0 +1,42 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup() {
+ let compatMode = document.getElementById("uaFirefoxCompat");
+ let modeFirefox =
+ document.getElementById("general.useragent.compatMode.firefox");
+ let modeStrict =
+ document.getElementById("general.useragent.compatMode.strict-firefox");
+
+ if (modeStrict.value)
+ compatMode.value = "strict";
+ else if (modeFirefox.value)
+ compatMode.value = "compat";
+ else
+ compatMode.value = "none";
+}
+
+function updateUAPrefs(aCompatMode) {
+ let modeFirefox =
+ document.getElementById("general.useragent.compatMode.firefox");
+ // The strict option will only work in builds compiled from a SeaMonkey
+ // release branch. Additional code needs to be added to the mozilla sources.
+ // See Bug 1242294 for the needed changes.
+ let modeStrict =
+ document.getElementById("general.useragent.compatMode.strict-firefox");
+ switch (aCompatMode.value) {
+ case "strict":
+ modeStrict.value = true;
+ modeFirefox.value = false;
+ break;
+ case "compat":
+ modeStrict.value = false;
+ modeFirefox.value = true;
+ break;
+ case "none":
+ modeStrict.value = false;
+ modeFirefox.value = false;
+ }
+}
diff --git a/comm/suite/components/pref/content/pref-http.xul b/comm/suite/components/pref/content/pref-http.xul
new file mode 100644
index 0000000000..bea6545418
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-http.xul
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-http.dtd">
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="http_pane"
+ label="&pref.http.title;"
+ script="chrome://communicator/content/pref/pref-http.js">
+
+ <preferences>
+ <preference id="network.http.version"
+ name="network.http.version"
+ type="string"/>
+ <preference id="network.http.proxy.version"
+ name="network.http.proxy.version"
+ type="string"/>
+ <preference id="general.useragent.compatMode.firefox"
+ name="general.useragent.compatMode.firefox"
+ type="bool"/>
+ <preference id="general.useragent.compatMode.strict-firefox"
+ name="general.useragent.compatMode.strict-firefox"
+ type="bool"/>
+ </preferences>
+
+ <description>&prefPara;</description>
+
+ <hbox align="start">
+ <groupbox flex="1">
+ <caption label="&prefDirect.label;"/>
+ <vbox class="indent" align="start">
+ <radiogroup id="httpVersion"
+ preference="network.http.version">
+ <radio value="1.0"
+ label="&prefEnableHTTP10.label;"
+ accesskey="&prefEnableHTTP10.accesskey;"/>
+ <radio value="1.1"
+ label="&prefEnableHTTP11.label;"
+ accesskey="&prefEnableHTTP11.accesskey;"/>
+ </radiogroup>
+ </vbox>
+ </groupbox>
+
+ <groupbox flex="1">
+ <caption label="&prefProxy.label;"/>
+ <vbox class="indent" align="start">
+ <radiogroup id="httpVersionProxy"
+ preference="network.http.proxy.version">
+ <radio value="1.0"
+ label="&prefEnableHTTP10.label;"
+ accesskey="&prefEnableHTTP10Proxy.accesskey;"/>
+ <radio value="1.1"
+ label="&prefEnableHTTP11.label;"
+ accesskey="&prefEnableHTTP11Proxy.accesskey;"/>
+ </radiogroup>
+ </vbox>
+ </groupbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <caption label="&prefUseragent.label;"/>
+ <radiogroup id="uaFirefoxCompat"
+ oncommand="updateUAPrefs(this);">
+ <radio value="strict"
+ label="&prefFirefoxStrict.label;"
+ accesskey="&prefFirefoxStrict.accesskey;"/>
+ <radio value="none"
+ label="&prefFirefoxNone.label;"
+ accesskey="&prefFirefoxNone.accesskey;"/>
+ <radio value="compat"
+ label="&prefFirefoxCompat2.label;"
+ accesskey="&prefFirefoxCompat2.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <description>&prefCompatWarning2.desc;</description>
+ </prefpane>
+
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-images.xul b/comm/suite/components/pref/content/pref-images.xul
new file mode 100644
index 0000000000..92c048dc56
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-images.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % prefImagesDTD SYSTEM "chrome://communicator/locale/pref/pref-images.dtd" >
+%prefImagesDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="images_pane"
+ label="&pref.images.title;">
+ <preferences id="images_preferences">
+ <preference id="permissions.default.image"
+ name="permissions.default.image" type="int"/>
+ <preference id="pref.advanced.images.disable_button.view_image" type="bool"
+ name="pref.advanced.images.disable_button.view_image"/>
+ </preferences>
+
+ <groupbox id="imagesArea">
+ <caption label="&imageBlocking.label;"/>
+
+ <description>&imageDetails;</description>
+
+ <radiogroup id="networkImageBehaviour"
+ preference="permissions.default.image">
+ <radio value="2" label="&loadNoImagesRadio.label;"
+ accesskey="&loadNoImagesRadio.accesskey;"/>
+ <radio value="3" label="&loadOrgImagesRadio.label;"
+ accesskey="&loadOrgImagesRadio.accesskey;"/>
+ <radio value="1" label="&loadAllImagesRadio.label;"
+ accesskey="&loadAllImagesRadio.accesskey;"/>
+ </radiogroup>
+
+ <hbox pack="end">
+ <button id="viewImages"
+ label="&viewPermissions.label;"
+ accesskey="&viewPermissions.accesskey;"
+ oncommand="toDataManager('|permissions');"
+ preference="pref.advanced.images.disable_button.view_image"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-keynav.js b/comm/suite/components/pref/content/pref-keynav.js
new file mode 100644
index 0000000000..5210fdbe5c
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-keynav.js
@@ -0,0 +1,54 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+const kTabToLinks = 4;
+const kTabToForms = 2;
+const kTabToTextboxes = 1;
+
+function Startup()
+{
+ if (AppConstants.platform == "macosx") {
+ document.getElementById("tabNavigationPrefs").setAttribute("hidden", true);
+ }
+
+ UpdateBrowseWithCaretItems();
+}
+
+function ReadTabNav(aField)
+{
+ var curval = document.getElementById("accessibility.tabfocus").value;
+ // Return the right bit based on the id of "aField"
+ if (aField.id == "tabNavigationLinks")
+ return (curval & kTabToLinks) != 0;
+
+ return (curval & kTabToForms) != 0;
+}
+
+function WriteTabNav(aField)
+{
+ var curval = document.getElementById("accessibility.tabfocus").value;
+ // Textboxes are always part of the tab order
+ curval |= kTabToTextboxes;
+ // Select the bit, we have to change, based on the id of "aField"
+ var bit = kTabToForms;
+ if (aField.id == "tabNavigationLinks")
+ bit = kTabToLinks;
+
+ if (aField.checked)
+ return curval | bit;
+
+ return curval & ~bit;
+}
+
+function UpdateBrowseWithCaretItems()
+{
+ document.getElementById("browseWithCaretWarn").disabled =
+ !document.getElementById("accessibility.browsewithcaret_shortcut.enabled").value ||
+ document.getElementById("accessibility.browsewithcaret").locked;
+}
diff --git a/comm/suite/components/pref/content/pref-keynav.xul b/comm/suite/components/pref/content/pref-keynav.xul
new file mode 100644
index 0000000000..39ea0e7d1f
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-keynav.xul
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-keynav.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="keynav_pane"
+ label="&pref.keyNav.title;"
+ script="chrome://communicator/content/pref/pref-keynav.js">
+
+ <preferences id="keynav_preferences">
+ <preference id="accessibility.tabfocus"
+ name="accessibility.tabfocus"
+ type="int"/>
+ <preference id="accessibility.browsewithcaret"
+ name="accessibility.browsewithcaret"
+ type="bool"/>
+ <preference id="accessibility.browsewithcaret_shortcut.enabled"
+ name="accessibility.browsewithcaret_shortcut.enabled"
+ type="bool"
+ onchange="UpdateBrowseWithCaretItems();"/>
+ <preference id="accessibility.warn_on_browsewithcaret"
+ name="accessibility.warn_on_browsewithcaret"
+ type="bool"/>
+ <preference id="ui.key.accelKey"
+ name="ui.key.accelKey"
+ type="int"/>
+ <preference id="ui.key.menuAccessKey"
+ name="ui.key.menuAccessKey"
+ type="int"/>
+ </preferences>
+
+ <groupbox id="tabNavigationPrefs"
+ align="start">
+ <caption label="&tabNavigationBehavior.label;"/>
+ <description>&tabNavigationDesc.label;</description>
+
+ <checkbox id="tabNavigationLinks"
+ label="&tabNavigationLinks.label;"
+ accesskey="&tabNavigationLinks.accesskey;"
+ preference="accessibility.tabfocus"
+ onsyncfrompreference="return document.getElementById('keynav_pane').ReadTabNav(this);"
+ onsynctopreference="return document.getElementById('keynav_pane').WriteTabNav(this);"/>
+ <checkbox id="tabNavigationForms"
+ label="&tabNavigationForms.label;"
+ accesskey="&tabNavigationForms.accesskey;"
+ preference="accessibility.tabfocus"
+ onsyncfrompreference="return document.getElementById('keynav_pane').ReadTabNav(this);"
+ onsynctopreference="return document.getElementById('keynav_pane').WriteTabNav(this);"/>
+ <description>&tabNavigationTextboxes.label;</description>
+ </groupbox>
+
+ <groupbox id="browseWithCaretPrefs"
+ align="start">
+ <caption label="&accessibilityBrowseWithCaret.label;"/>
+ <description>&browseWithCaretDesc.label;</description>
+ <checkbox id="browseWithCaretUse"
+ label="&browseWithCaretUse.label;"
+ accesskey="&browseWithCaretUse.accesskey;"
+ preference="accessibility.browsewithcaret"/>
+ <checkbox id="browseWithCaretShortCut"
+ label="&browseWithCaretShortCut.label;"
+ accesskey="&browseWithCaretShortCut.accesskey;"
+ preference="accessibility.browsewithcaret_shortcut.enabled"/>
+ <checkbox id="browseWithCaretWarn"
+ class="indent"
+ label="&browseWithCaretWarn.label;"
+ accesskey="&browseWithCaretWarn.accesskey;"
+ preference="accessibility.warn_on_browsewithcaret"/>
+ </groupbox>
+
+ <groupbox id="modifiers">
+ <caption label="&modifiers.label;"/>
+ <hbox align="center">
+ <label id="acceleratorKey"
+ value="&acceleratorKey.label;"
+ accesskey="&acceleratorKey.accesskey;"
+ control="acceleratorKeyValue"/>
+ <textbox id="acceleratorKeyValue"
+ type="number"
+ min="0"
+ max="255"
+ size="3"
+ preference="ui.key.accelKey"
+ aria-labelledby="acceleratorKey acceleratorKeyValue"/>
+ <label id="menuAccessKey"
+ value="&menuAccessKey.label;"
+ accesskey="&menuAccessKey.accesskey;"
+ control="menuAccessKeyValue"/>
+ <textbox id="menuAccessKeyValue"
+ type="number"
+ min="0"
+ max="255"
+ size="3"
+ preference="ui.key.menuAccessKey"
+ aria-labelledby="menuAccessKey menuAccessKeyValue"/>
+ </hbox>
+ <description>&modifiersDesc.label;</description>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-languages-add.js b/comm/suite/components/pref/content/pref-languages-add.js
new file mode 100644
index 0000000000..e539b961cc
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-languages-add.js
@@ -0,0 +1,147 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gLanguageNames;
+var gAvailableLanguages;
+var gOtherLanguages;
+var gSelectedLanguages = [];
+var gInvalidLanguages;
+
+function OnLoadAddLanguages()
+{
+ gLanguageNames = window.arguments[0];
+ gAvailableLanguages = document.getElementById("availableLanguages");
+ gSelectedLanguages = document.getElementById("intl.accept_languages").value
+ .toLowerCase().split(/\s*,\s*/);
+ gOtherLanguages = document.getElementById("otherLanguages");
+
+ if (gLanguageNames)
+ {
+ for (var i = 0; i < gLanguageNames.length; i++)
+ {
+ if (!gSelectedLanguages.includes(gLanguageNames[i][1]))
+ gAvailableLanguages.appendItem(gLanguageNames[i][0],
+ gLanguageNames[i][1]);
+ }
+ }
+}
+
+function IsRFC1766LangTag(aCandidate)
+{
+ /* reject bogus lang strings, INCLUDING those with HTTP "q"
+ values kludged on the end of them
+
+ Valid language codes examples:
+ i.e. ja-JP-kansai (Kansai dialect of Japanese)
+ en-US-texas (Texas dialect)
+ i-klingon-tng (did TOS Klingons speak in non-English?)
+ sgn-US-MA (Martha Vineyard's Sign Language)
+ */
+ var tags = aCandidate.split('-');
+ var checkedTags = 0;
+
+ if (/^[ix]$/.test(tags[0]))
+ {
+ if (tags.length < 2)
+ return false;
+ checkedTags++;
+ }
+ else
+ /* if not IANA "i" or a private "x" extension, the primary
+ tag should be a ISO 639 country code, two or three letters long.
+ we don't check if the country code is bogus or not.
+ */
+ {
+ if (!/^[a-z]{2,3}$/.test(tags[0]))
+ return false;
+ checkedTags++;
+
+ /* the first subtag can be either a 2 letter ISO 3166 country code,
+ or an IANA registered tag from 3 to 8 characters.
+ */
+ if (tags.length > 1)
+ {
+ if (!/^[a-z0-9]{2,8}$/.test(tags[1]))
+ return false;
+
+ /* do not allow user-assigned ISO 3166 country codes */
+ if (/^(aa|zz|x[a-z]|q[m-z])$/.test(tags[1]))
+ return false;
+ checkedTags++;
+ }
+ }
+
+ /* any remaining subtags must be one to eight alphabetic characters */
+
+ while (checkedTags < tags.length)
+ {
+ if (!/^[a-z0-9]{1,8}$/.test(tags[checkedTags]))
+ return false;
+ checkedTags++;
+ }
+ return true;
+}
+
+function WriteAddedLanguages(aListbox)
+{
+ var invalidLangs = [];
+ // selected languages
+ var languages = aListbox.selectedItems;
+ var addedLang = Array.from(languages, e => e.value);
+
+ // user-defined languages
+ languages = gOtherLanguages.value;
+ if (languages)
+ {
+ let languageIds = languages.replace(/\s+/g, "").toLowerCase().split(",");
+ for (var i = 0; i < languageIds.length; i++)
+ {
+ let languageId = languageIds[i];
+ if (IsRFC1766LangTag(languageId))
+ {
+ if (!addedLang.includes(languageId) &&
+ !gSelectedLanguages.includes(languageId))
+ addedLang.push(languageId);
+ }
+ else
+ {
+ invalidLangs.push(languageId);
+ }
+ }
+ }
+
+ if (invalidLangs.length)
+ gInvalidLanguages = invalidLangs.join(", ");
+ else
+ gSelectedLanguages = gSelectedLanguages.concat(addedLang);
+
+ return gSelectedLanguages.join(",");
+}
+
+function OnAccept()
+{
+ if (!gInvalidLanguages)
+ return true;
+
+ let prefLangBundle = document.getElementById("prefLangAddBundle");
+ const kErrorMsg = prefLangBundle.getString("illegalOtherLanguage") + " " +
+ gInvalidLanguages;
+ const kErrorTitle = prefLangBundle.getString("illegalOtherLanguageTitle");
+ Services.prompt.alert(this.window, kErrorTitle, kErrorMsg);
+
+ gInvalidLanguages = null;
+ gOtherLanguages.focus();
+ return false;
+}
+
+function HandleDoubleClick()
+{
+ document.documentElement.acceptDialog();
+}
+
+function DoBeforeAccept()
+{
+ gAvailableLanguages.doCommand();
+}
diff --git a/comm/suite/components/pref/content/pref-languages-add.xul b/comm/suite/components/pref/content/pref-languages-add.xul
new file mode 100644
index 0000000000..0ae11aee9b
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-languages-add.xul
@@ -0,0 +1,54 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE prefwindow SYSTEM "chrome://communicator/locale/pref/pref-languages.dtd" >
+
+
+<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="addLanguagesPreferences"
+ title="&languages.customize.add.title.label;"
+ type="child"
+ onload="OnLoadAddLanguages();"
+ onbeforeaccept="DoBeforeAccept();"
+ ondialogaccept="return OnAccept();">
+
+ <script src="chrome://communicator/content/pref/pref-languages-add.js"/>
+
+ <prefpane id="addLanguagesPane">
+ <preferences id="addLanguages">
+ <preference id="intl.accept_languages"
+ name="intl.accept_languages"
+ type="wstring"/>
+ </preferences>
+
+ <stringbundleset id="langAddBundleset">
+ <stringbundle id="prefLangAddBundle"
+ src="chrome://communicator/locale/pref/pref-languages.properties"/>
+ </stringbundleset>
+
+ <description style="width: 1px;">&languages.customize.prefAddLangDescript;</description>
+ <separator class="thin"/>
+ <description style="width: 1px;">&languages.customize.available.label;</description>
+
+ <listbox id="availableLanguages"
+ flex="1"
+ seltype="multiple"
+ preference="intl.accept_languages"
+ ondblclick="HandleDoubleClick();"
+ onsynctopreference="return WriteAddedLanguages(this);"/>
+
+ <hbox align="center">
+ <label value="&languages.customize.others.label;"
+ accesskey="&languages.customize.others.accesskey;"
+ control="otherLanguages"/>
+ <textbox id="otherLanguages" size="12" flex="1"/>
+ <label value="&languages.customize.others.examples;" control="otherLanguages"/>
+ </hbox>
+ </prefpane>
+</prefwindow>
diff --git a/comm/suite/components/pref/content/pref-languages.js b/comm/suite/components/pref/content/pref-languages.js
new file mode 100644
index 0000000000..de2895ee11
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-languages.js
@@ -0,0 +1,200 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gActiveLanguages;
+var gLanguages;
+var gLanguageNames = [];
+var gLanguageTitles = {};
+
+function Startup()
+{
+ gActiveLanguages = document.getElementById("activeLanguages");
+ // gLanguages stores the ordered list of languages, due to the nature
+ // of childNodes it is live and updates automatically.
+ gLanguages = gActiveLanguages.childNodes;
+
+ ReadAvailableLanguages();
+}
+
+function AddLanguage()
+{
+ document.documentElement.openSubDialog("chrome://communicator/content/pref/pref-languages-add.xul", "addlangwindow", gLanguageNames);
+}
+
+function ReadAvailableLanguages()
+{
+ var i = 0;
+ var languagesBundle = document.getElementById("languageNamesBundle");
+ var prefLangBundle = document.getElementById("prefLangBundle");
+ var regionsBundle = document.getElementById("regionNamesBundle");
+ var langStrings = document.getElementById("acceptedBundle").strings;
+
+ while (langStrings.hasMoreElements())
+ {
+ // Progress through the bundle.
+ var curItem = langStrings.getNext();
+
+ if (!(curItem instanceof Ci.nsIPropertyElement))
+ break;
+
+ var stringNameProperty = curItem.key.split('.');
+
+ var str = stringNameProperty[0];
+ if (str && stringNameProperty[1] == 'accept')
+ {
+ var stringLangRegion = str.split('-');
+
+ if (stringLangRegion[0])
+ {
+ var language = "";
+ var region = null;
+
+ try
+ {
+ language = languagesBundle.getString(stringLangRegion[0]);
+ }
+ catch (ex) {}
+
+ if (stringLangRegion.length > 1)
+ {
+ try
+ {
+ region = regionsBundle.getString(stringLangRegion[1]);
+ }
+ catch (ex) {}
+ }
+
+ var title;
+ if (region)
+ title = prefLangBundle.getFormattedString("languageRegionCodeFormat",
+ [language, region, str]);
+ else
+ title = prefLangBundle.getFormattedString("languageCodeFormat",
+ [language, str]);
+ gLanguageTitles[str] = title;
+ if (curItem.value == "true")
+ gLanguageNames.push([title, str]);
+ }
+ }
+ }
+
+ // Sort on first element.
+ gLanguageNames.sort(
+ function compareFn(a, b)
+ {
+ return a[0].localeCompare(b[0]);
+ }
+ );
+}
+
+function ReadActiveLanguages()
+{
+ var arrayOfPrefs = document.getElementById("intl.accept_languages").value
+ .toLowerCase().split(/\s*,\s*/);
+
+ // No need to rebuild listitems if languages in prefs and listitems match.
+ if (InSync(arrayOfPrefs))
+ return;
+
+ while (gActiveLanguages.hasChildNodes())
+ gActiveLanguages.lastChild.remove();
+
+ arrayOfPrefs.forEach(
+ function(aKey)
+ {
+ if (aKey)
+ {
+ let langTitle = gLanguageTitles.hasOwnProperty(aKey) ?
+ gLanguageTitles[aKey] : "[" + aKey + "]";
+ gActiveLanguages.appendItem(langTitle, aKey);
+ }
+ }
+ );
+
+ SelectLanguage();
+
+ return;
+}
+
+// Checks whether listitems and pref values matches, returns false if not.
+function InSync(aPrefArray)
+{
+ // Can't match if they don't have the same length.
+ if (aPrefArray.length != gLanguages.length)
+ return false;
+
+ return aPrefArray.every(
+ function(aElement, aIndex)
+ {
+ return aElement == gLanguages[aIndex].value;
+ }
+ );
+}
+
+// Called on onsynctopreference.
+function WriteActiveLanguages()
+{
+ return Array.from(gLanguages, e => e.value).join(",");
+}
+
+function MoveUp()
+{
+ var selected = gActiveLanguages.selectedItem;
+ var before = selected.previousSibling;
+ if (before)
+ {
+ before.parentNode.insertBefore(selected, before);
+ gActiveLanguages.selectItem(selected);
+ gActiveLanguages.ensureElementIsVisible(selected);
+ }
+
+ SelectLanguage();
+ gActiveLanguages.doCommand();
+}
+
+function MoveDown()
+{
+ var selected = gActiveLanguages.selectedItem;
+ if (selected.nextSibling)
+ {
+ var before = selected.nextSibling.nextSibling;
+ gActiveLanguages.insertBefore(selected, before);
+ gActiveLanguages.selectItem(selected);
+ }
+
+ SelectLanguage();
+ gActiveLanguages.doCommand();
+}
+
+function RemoveActiveLanguage(aEvent)
+{
+ if (aEvent && aEvent.keyCode != aEvent.DOM_VK_DELETE &&
+ aEvent.keyCode != aEvent.DOM_VK_BACK_SPACE)
+ return;
+
+ var nextNode = null;
+
+ while (gActiveLanguages.selectedItem)
+ {
+ var selectedNode = gActiveLanguages.selectedItem;
+ nextNode = selectedNode.nextSibling || selectedNode.previousSibling;
+ selectedNode.remove();
+ }
+
+ if (nextNode)
+ gActiveLanguages.selectItem(nextNode);
+
+ SelectLanguage();
+ gActiveLanguages.doCommand();
+}
+
+function SelectLanguage()
+{
+ var len = gActiveLanguages.selectedItems.length;
+ EnableElementById("langRemove", len, false);
+ var selected = gActiveLanguages.selectedItem;
+ EnableElementById("langDown", (len == 1) && selected.nextSibling, false);
+ EnableElementById("langUp", (len == 1) && selected.previousSibling, false);
+}
diff --git a/comm/suite/components/pref/content/pref-languages.xul b/comm/suite/components/pref/content/pref-languages.xul
new file mode 100644
index 0000000000..a17deae032
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-languages.xul
@@ -0,0 +1,124 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % prefLanguagesDTD SYSTEM "chrome://communicator/locale/pref/pref-languages.dtd"> %prefLanguagesDTD;
+ <!ENTITY % prefUtilitiesDTD SYSTEM "chrome://communicator/locale/pref/prefutilities.dtd"> %prefUtilitiesDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="languages_pane"
+ label="&languages.customize.title;"
+ script="chrome://communicator/content/pref/pref-languages.js">
+
+ <preferences id="languages_preferences">
+ <preference id="intl.accept_languages"
+ name="intl.accept_languages"
+ type="wstring"/>
+ <preference id="pref.browser.language.disable_button.up"
+ name="pref.browser.language.disable_button.up"
+ type="bool"/>
+ <preference id="pref.browser.language.disable_button.down"
+ name="pref.browser.language.disable_button.down"
+ type="bool"/>
+ <preference id="pref.browser.language.disable_button.add"
+ name="pref.browser.language.disable_button.add"
+ type="bool"/>
+ <preference id="pref.browser.language.disable_button.remove"
+ name="pref.browser.language.disable_button.remove"
+ type="bool"/>
+ <preference id="intl.charset.fallback.override"
+ name="intl.charset.fallback.override"
+ type="string"/>
+ </preferences>
+
+ <stringbundleset id="langBundleset">
+ <stringbundle id="acceptedBundle"
+ src="resource://gre/res/language.properties"/>
+ <stringbundle id="prefLangBundle"
+ src="chrome://communicator/locale/pref/pref-languages.properties"/>
+ </stringbundleset>
+
+ <groupbox flex="1">
+ <caption label="&langtitle.label;"/>
+ <description>&languages.customize.prefLangDescript;</description>
+ <label accesskey="&languages.customize.active.accesskey;"
+ control="activeLanguages">&languages.customize.active.label;</label>
+ <hbox flex="1">
+ <listbox id="activeLanguages"
+ flex="1"
+ style="width: 0px; height: 0px;"
+ seltype="multiple"
+ preference="intl.accept_languages"
+ onkeypress="RemoveActiveLanguage(event);"
+ onselect="SelectLanguage();"
+ onsynctopreference="return document.getElementById('languages_pane').WriteActiveLanguages();"
+ onsyncfrompreference="return document.getElementById('languages_pane').ReadActiveLanguages(this);"/>
+ <vbox>
+ <button id="langUp"
+ class="up"
+ disabled="true"
+ label="&languages.customize.moveUp.label;"
+ accesskey="&languages.customize.moveUp.accesskey;"
+ preference="pref.browser.language.disable_button.up"
+ oncommand="MoveUp();"/>
+ <button id="langDown"
+ class="down"
+ disabled="true"
+ label="&languages.customize.moveDown.label;"
+ accesskey="&languages.customize.moveDown.accesskey;"
+ preference="pref.browser.language.disable_button.down"
+ oncommand="MoveDown();"/>
+ <spacer flex="1"/>
+ <button id="langAdd"
+ label="&languages.customize.addButton.label;"
+ accesskey="&languages.customize.addButton.accesskey;"
+ preference="pref.browser.language.disable_button.add"
+ oncommand="AddLanguage();"/>
+ <button id="langRemove"
+ disabled="true"
+ label="&languages.customize.deleteButton.label;"
+ accesskey="&languages.customize.deleteButton.accesskey;"
+ preference="pref.browser.language.disable_button.remove"
+ oncommand="RemoveActiveLanguage(null);"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+
+ <groupbox align="start">
+ <caption label="&languages.customize.Fallback2.grouplabel;"/>
+ <description>&languages.customize.Fallback2.desc;</description>
+ <hbox align="center">
+ <label value="&languages.customize.Fallback2.label;"
+ accesskey="&languages.customize.Fallback2.accesskey;"
+ control="defaultCharsetList"/>
+ <menulist id="defaultCharsetList"
+ preference="intl.charset.fallback.override">
+ <menupopup>
+ <menuitem label="&FallbackCharset.auto;" value=""/>
+ <menuitem label="&FallbackCharset.arabic;" value="windows-1256"/>
+ <menuitem label="&FallbackCharset.baltic;" value="windows-1257"/>
+ <menuitem label="&FallbackCharset.ceiso;" value="ISO-8859-2"/>
+ <menuitem label="&FallbackCharset.cewindows;" value="windows-1250"/>
+ <menuitem label="&FallbackCharset.simplified;" value="gbk"/>
+ <menuitem label="&FallbackCharset.traditional;" value="Big5"/>
+ <menuitem label="&FallbackCharset.cyrillic;" value="windows-1251"/>
+ <menuitem label="&FallbackCharset.greek;" value="ISO-8859-7"/>
+ <menuitem label="&FallbackCharset.hebrew;" value="windows-1255"/>
+ <menuitem label="&FallbackCharset.japanese;" value="Shift_JIS"/>
+ <menuitem label="&FallbackCharset.korean;" value="EUC-KR"/>
+ <menuitem label="&FallbackCharset.thai;" value="windows-874"/>
+ <menuitem label="&FallbackCharset.turkish;" value="windows-1254"/>
+ <menuitem label="&FallbackCharset.vietnamese;" value="windows-1258"/>
+ <menuitem label="&FallbackCharset.other;" value="windows-1252"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-links.js b/comm/suite/components/pref/content/pref-links.js
new file mode 100644
index 0000000000..2ca7a3e921
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-links.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ ToggleRestrictionGroup(document.getElementById("browser.link.open_newwindow").value);
+}
+
+function ToggleRestrictionGroup(value)
+{
+ document.getElementById("restrictionGroup").disabled =
+ value == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
+ document.getElementById("browser.link.open_newwindow.restriction").locked;
+}
diff --git a/comm/suite/components/pref/content/pref-links.xul b/comm/suite/components/pref/content/pref-links.xul
new file mode 100644
index 0000000000..6ebb8d9cb2
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-links.xul
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-links.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="links_pane"
+ label="&linksHeader.label;"
+ script="chrome://communicator/content/pref/pref-links.js">
+
+ <preferences id="links_preferences">
+ <preference id="browser.link.open_newwindow"
+ name="browser.link.open_newwindow"
+ type="int"
+ onchange="ToggleRestrictionGroup(this.value);"/>
+ <preference id="browser.link.open_newwindow.restriction"
+ name="browser.link.open_newwindow.restriction"
+ type="int"/>
+ <preference id="browser.link.open_external"
+ name="browser.link.open_external"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&newWindow.label;"/>
+ <description>&newWindowDescription.label;</description>
+ <radiogroup id="newWindowGroup"
+ class="indent"
+ preference="browser.link.open_newwindow">
+ <radio value="1"
+ label="&openCurrent.label;"
+ accesskey="&newWindowGroupCurrent.accesskey;"/>
+ <radio value="3"
+ label="&openTab.label;"
+ accesskey="&newWindowGroupTab.accesskey;"/>
+ <radio value="2"
+ label="&openWindow.label;"
+ accesskey="&newWindowGroupWindow.accesskey;"/>
+ </radiogroup>
+ <separator class="thin"/>
+ <description>&newWindowRestriction.label;</description>
+ <radiogroup id="restrictionGroup"
+ class="indent"
+ preference="browser.link.open_newwindow.restriction">
+ <radio value="0"
+ label="&divertAll.label;"
+ accesskey="&divertAll.accesskey;"/>
+ <radio value="2"
+ label="&divertNoFeatures.label;"
+ accesskey="&divertNoFeatures.accesskey;"/>
+ <radio value="1"
+ label="&dontDivert.label;"
+ accesskey="&dontDivert.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&external.label;"/>
+ <description>&externalDescription.label;</description>
+ <radiogroup id="externalGroup"
+ class="indent"
+ preference="browser.link.open_external">
+ <radio value="1"
+ label="&openCurrent.label;"
+ accesskey="&externalGroupCurrent.accesskey;"/>
+ <radio value="3"
+ label="&openTab.label;"
+ accesskey="&externalGroupTab.accesskey;"/>
+ <radio value="2"
+ label="&openWindow.label;"
+ accesskey="&externalGroupWindow.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-locationbar.js b/comm/suite/components/pref/content/pref-locationbar.js
new file mode 100644
index 0000000000..042621eb35
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-locationbar.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ // On systems that has the file view component, autoFill and showPopup will
+ // return results from local browsing "history", even if autocomplete.enabled
+ // is turned off, so we'll need to remove the dependent look in the ui.
+
+ if ("@mozilla.org/autocomplete/search;1?name=file" in Cc)
+ {
+ // We indent the checkboxes with the class attribute set to "indent", so
+ // just remove the attribute.
+ document.getElementById("autoFill").removeAttribute("class");
+ document.getElementById("showPopup").removeAttribute("class");
+ }
+
+ updateDependent();
+}
+
+function updateDependent()
+{
+ var matchHistoryPref = document.getElementById("browser.urlbar.suggest.history");
+ EnableElementById("matchOnlyTyped", matchHistoryPref.value);
+
+ var matchBookmarkPref = document.getElementById("browser.urlbar.suggest.bookmark");
+ var autoCompleteEnabled = matchHistoryPref.value || matchBookmarkPref.value;
+ EnableElementById("matchBehavior", autoCompleteEnabled);
+
+ // If autoFill has a class attribute, we don't have the file view component.
+ // We then need to update autoFill and showPopup.
+ if (document.getElementById("autoFill").hasAttribute("class"))
+ {
+ EnableElementById("autoFill", autoCompleteEnabled);
+ EnableElementById("showPopup", autoCompleteEnabled);
+ }
+
+ // We need to update autocomplete.enabled as the backend still respects it.
+ document.getElementById("browser.urlbar.autocomplete.enabled").value =
+ autoCompleteEnabled;
+}
diff --git a/comm/suite/components/pref/content/pref-locationbar.xul b/comm/suite/components/pref/content/pref-locationbar.xul
new file mode 100644
index 0000000000..3c781e4c60
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-locationbar.xul
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-locationbar.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="locationBar_pane"
+ label="&pref.locationBar.title;"
+ script="chrome://communicator/content/pref/pref-locationbar.js">
+
+ <preferences id="locationBar_preferences">
+ <!-- The suggest preferences need to come first otherwise the backend
+ will enable both bookmarks and history if either is enabled -->
+ <preference id="browser.urlbar.suggest.bookmark"
+ name="browser.urlbar.suggest.bookmark"
+ type="bool"
+ onchange="updateDependent();"/>
+ <preference id="browser.urlbar.suggest.history"
+ name="browser.urlbar.suggest.history"
+ type="bool"
+ onchange="updateDependent();"/>
+ <preference id="browser.urlbar.suggest.history.onlyTyped"
+ name="browser.urlbar.suggest.history.onlyTyped"
+ type="bool"/>
+ <preference id="browser.urlbar.autocomplete.enabled"
+ name="browser.urlbar.autocomplete.enabled"
+ type="bool"/>
+ <preference id="browser.urlbar.matchBehavior"
+ name="browser.urlbar.matchBehavior"
+ type="int"/>
+ <preference id="browser.urlbar.autoFill"
+ name="browser.urlbar.autoFill"
+ type="bool"
+ onchange="updateMatchPrefs();"/>
+ <preference id="browser.urlbar.showPopup"
+ name="browser.urlbar.showPopup"
+ type="bool"
+ onchange="updateMatchPrefs();"/>
+ <preference id="browser.urlbar.showSearch"
+ name="browser.urlbar.showSearch"
+ type="bool"/>
+ <preference id="browser.urlbar.formatting.enabled"
+ name="browser.urlbar.formatting.enabled"
+ type="bool"/>
+ <preference id="browser.urlbar.highlight.secure"
+ name="browser.urlbar.highlight.secure"
+ type="bool"/>
+ <preference id="browser.fixup.alternate.enabled"
+ name="browser.fixup.alternate.enabled"
+ type="bool"/>
+ <preference id="keyword.enabled"
+ name="keyword.enabled"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&autoComplete.label;"/>
+ <checkbox id="matchHistory"
+ label="&autoCompleteMatchHistory.label;"
+ accesskey="&autoCompleteMatchHistory.accesskey;"
+ preference="browser.urlbar.suggest.history"/>
+ <checkbox id="matchOnlyTyped"
+ class="indent"
+ label="&autoCompleteMatchOnlyTyped.label;"
+ accesskey="&autoCompleteMatchOnlyTyped.accesskey;"
+ preference="browser.urlbar.suggest.history.onlyTyped"/>
+ <checkbox id="matchBookmark"
+ label="&autoCompleteMatchBookmarks.label;"
+ accesskey="&autoCompleteMatchBookmarks.accesskey;"
+ preference="browser.urlbar.suggest.bookmark"/>
+ <hbox align="center" class="indent">
+ <label value="&autoCompleteMatch.label;" control="matchBehavior"
+ accesskey="&autoCompleteMatch.accesskey;"/>
+ <menulist id="matchBehavior"
+ preference="browser.urlbar.matchBehavior">
+ <menupopup>
+ <menuitem value="0" label="&autoCompleteMatchAnywhere;"/>
+ <menuitem value="1" label="&autoCompleteMatchWordsFirst;"/>
+ <menuitem value="2" label="&autoCompleteMatchWords;"/>
+ <menuitem value="3" label="&autoCompleteMatchStart;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <checkbox id="autoFill"
+ class="indent"
+ label="&autoCompleteAutoFill.label;"
+ accesskey="&autoCompleteAutoFill.accesskey;"
+ preference="browser.urlbar.autoFill"/>
+ <checkbox id="showPopup"
+ class="indent"
+ label="&autoCompleteShowPopup.label;"
+ accesskey="&autoCompleteShowPopup.accesskey;"
+ preference="browser.urlbar.showPopup"/>
+ <checkbox id="showSearch"
+ label="&showInternetSearch.label;"
+ accesskey="&showInternetSearch.accesskey;"
+ preference="browser.urlbar.showSearch"/>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&formatting.label;"/>
+ <checkbox id="domainFormattingEnabled"
+ label="&domainFormatting.label;"
+ accesskey="&domainFormatting.accesskey;"
+ preference="browser.urlbar.formatting.enabled"/>
+ <checkbox id="highlightSecureEnabled"
+ label="&highlightSecure.label;"
+ accesskey="&highlightSecure.accesskey;"
+ preference="browser.urlbar.highlight.secure"/>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&unknownLocations.label;"/>
+ <checkbox id="domainGuessingEnabled"
+ label="&domainGuessing.label;"
+ accesskey="&domainGuessing.accesskey;"
+ preference="browser.fixup.alternate.enabled"/>
+ <checkbox id="browserGoBrowsingEnabled"
+ label="&keywords.label;"
+ accesskey="&keywords.accesskey;"
+ preference="keyword.enabled"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-media.xul b/comm/suite/components/pref/content/pref-media.xul
new file mode 100644
index 0000000000..3a2411a634
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-media.xul
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-media.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="media_pane"
+ label="&pref.media.title;">
+
+ <preferences id="media_preferences">
+ <preference id="media.autoplay.enabled"
+ name="media.autoplay.enabled"
+ type="bool"/>
+ <preference id="media.eme.enabled"
+ name="media.eme.enabled"
+ type="bool"/>
+ <preference id="image.animation_mode"
+ name="image.animation_mode"
+ type="string"/>
+ </preferences>
+
+ <groupbox id="mediaHTML5Preferences" align="start">
+ <caption label="&mediaHTML5Preferences.label;"/>
+ <checkbox id="autoplay"
+ label="&allowMediaAutoplay.label;"
+ accesskey="&allowMediaAutoplay.accesskey;"
+ preference="media.autoplay.enabled"/>
+ </groupbox>
+
+ <!-- REMOVE #ifndef once EME are ready for prime time, meta bug 1015800 -->
+#ifndef RELEASE_OR_BETA
+ <groupbox id="drmPreferences">
+ <caption label="&enableDrmMedia.label;"/>
+ <checkbox id="emeForSuite"
+ label="&enableEmeForSuite.label;"
+ accesskey="&enableEmeForSuite.accesskey;"
+ preference="media.eme.enabled"/>
+ </groupbox>
+#endif
+
+ <groupbox>
+ <caption label="&animLoopingTitle.label;"/>
+ <radiogroup id="imageLooping"
+ preference="image.animation_mode">
+ <radio value="normal"
+ label="&animLoopAsSpecified.label;"
+ accesskey="&animLoopAsSpecified.accesskey;"/>
+ <radio value="once"
+ label="&animLoopOnce.label;"
+ accesskey="&animLoopOnce.accesskey;"/>
+ <radio value="none"
+ label="&animLoopNever.label;"
+ accesskey="&animLoopNever.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-mousewheel.js b/comm/suite/components/pref/content/pref-mousewheel.js
new file mode 100644
index 0000000000..6902a0c3cf
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-mousewheel.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function doEnabling(aElement)
+{
+ var preference = document.getElementById(aElement.getAttribute("preference"));
+ var prefix = aElement.id.replace(/action$/, "");
+ var vertical = document.getElementById(prefix + "delta_multiplier_y");
+ EnableElement(vertical, preference.value);
+ updateCheckbox(vertical);
+ var actionX = document.getElementById(prefix + "action_x");
+ if (actionX.value < 0)
+ doEnablingX(actionX);
+}
+
+function doEnablingX(aElement)
+{
+ var preference = document.getElementById(aElement.getAttribute("preference"));
+ var prefix = aElement.id.replace(/action_x$/, "");
+ var value = preference.value;
+ if (value < 0) {
+ var action = document.getElementById(prefix + "action");
+ preference = document.getElementById(action.getAttribute("preference"));
+ value = preference.value;
+ }
+ var horizontal = document.getElementById(prefix + "delta_multiplier_x");
+ EnableElement(horizontal, value);
+ updateCheckbox(horizontal);
+}
+
+function updateCheckbox(aTextbox)
+{
+ var preference = document.getElementById(aTextbox.getAttribute("preference"));
+ var checkbox = aTextbox.parentNode.lastChild;
+ checkbox.checked = preference.value < 0;
+ checkbox.disabled = !preference.value || aTextbox.disabled
+}
+
+function updateTextbox(aCheckbox)
+{
+ var textbox = aCheckbox.previousSibling.previousSibling;
+ var preference = document.getElementById(textbox.getAttribute("preference"));
+ preference.value = -preference.value;
+}
diff --git a/comm/suite/components/pref/content/pref-mousewheel.xul b/comm/suite/components/pref/content/pref-mousewheel.xul
new file mode 100644
index 0000000000..63346781cc
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-mousewheel.xul
@@ -0,0 +1,298 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-mousewheel.dtd" >
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="mousewheel_pane"
+ label="&pref.mouseWheel.title;"
+ script="chrome://communicator/content/pref/pref-mousewheel.js">
+
+ <preferences id="mousewheel_preferences">
+ <preference id="mousewheel.default.action"
+ name="mousewheel.default.action"
+ type="int"/>
+ <preference id="mousewheel.default.delta_multiplier_y"
+ name="mousewheel.default.delta_multiplier_y"
+ type="int"/>
+ <preference id="mousewheel.default.action.override_x"
+ name="mousewheel.default.action.override_x"
+ type="int"/>
+ <preference id="mousewheel.default.delta_multiplier_x"
+ name="mousewheel.default.delta_multiplier_x"
+ type="int"/>
+ <preference id="mousewheel.with_alt.action"
+ name="mousewheel.with_alt.action"
+ type="int"/>
+ <preference id="mousewheel.with_alt.delta_multiplier_y"
+ name="mousewheel.with_alt.delta_multiplier_y"
+ type="int"/>
+ <preference id="mousewheel.with_alt.action.override_x"
+ name="mousewheel.with_alt.action.override_x"
+ type="int"/>
+ <preference id="mousewheel.with_alt.delta_multiplier_x"
+ name="mousewheel.with_alt.delta_multiplier_x"
+ type="int"/>
+ <preference id="mousewheel.with_control.action"
+ name="mousewheel.with_control.action"
+ type="int"/>
+ <preference id="mousewheel.with_control.delta_multiplier_y"
+ name="mousewheel.with_control.delta_multiplier_y"
+ type="int"/>
+ <preference id="mousewheel.with_control.action.override_x"
+ name="mousewheel.with_control.action.override_x"
+ type="int"/>
+ <preference id="mousewheel.with_control.delta_multiplier_x"
+ name="mousewheel.with_control.delta_multiplier_x"
+ type="int"/>
+ <preference id="mousewheel.with_shift.action"
+ name="mousewheel.with_shift.action"
+ type="int"/>
+ <preference id="mousewheel.with_shift.delta_multiplier_y"
+ name="mousewheel.with_shift.delta_multiplier_y"
+ type="int"/>
+ <preference id="mousewheel.with_shift.action.override_x"
+ name="mousewheel.with_shift.action.override_x"
+ type="int"/>
+ <preference id="mousewheel.with_shift.delta_multiplier_x"
+ name="mousewheel.with_shift.delta_multiplier_x"
+ type="int"/>
+ </preferences>
+
+ <description>&mouseWheelPanel.label;</description>
+
+ <tabbox class="spaced">
+ <tabs>
+ <tab label="&usingJustTheWheel.label;"/>
+#ifndef XP_MACOSX
+ <tab label="&usingWheelAndAlt.label2;"/>
+#else
+ <tab label="&usingWheelAndOption.label;"/>
+#endif
+ <tab label="&usingWheelAndCtrl.label2;"/>
+ <tab label="&usingWheelAndShft.label2;"/>
+ </tabs>
+
+ <tabpanels>
+
+ <!-- no key modifiers -->
+ <vbox>
+ <groupbox>
+ <caption label="&mouseWheelGroup.label;"/>
+ <radiogroup id="mousewheel_default_action"
+ preference="mousewheel.default.action"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnabling(this);">
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothing.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocument.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&history.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoom.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_default_delta_multiplier_y"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_default_delta_multiplier_y"
+ accesskey="&wheelSpeed.accesskey;"
+ preference="mousewheel.default.delta_multiplier_y"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirection.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&mouseWheelHorizGroup.label;"/>
+ <radiogroup id="mousewheel_default_action_x"
+ preference="mousewheel.default.action.override_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnablingX(this);">
+ <radio value="-1" label="&sameAsVertical.label;" accesskey="&sameAsVertical.accesskey;"/>
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothingHoriz.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocumentHoriz.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&historyHoriz.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoomHoriz.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_default_delta_multiplier_x"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_default_delta_multiplier_x"
+ accesskey="&wheelSpeedHoriz.accesskey;"
+ preference="mousewheel.default.delta_multiplier_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirectionHoriz.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+ </vbox>
+
+ <!-- alt modifiers -->
+ <vbox>
+ <groupbox>
+ <caption label="&mouseWheelGroup.label;"/>
+ <radiogroup id="mousewheel_with_alt_action"
+ preference="mousewheel.with_alt.action"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnabling(this);">
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothing.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocument.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&history.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoom.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_alt_delta_multiplier_y"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_alt_delta_multiplier_y"
+ accesskey="&wheelSpeed.accesskey;"
+ preference="mousewheel.with_alt.delta_multiplier_y"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirection.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&mouseWheelHorizGroup.label;"/>
+ <radiogroup id="mousewheel_with_alt_action_x"
+ preference="mousewheel.with_alt.action.override_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnablingX(this);">
+ <radio value="-1" label="&sameAsVertical.label;" accesskey="&sameAsVertical.accesskey;"/>
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothingHoriz.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocumentHoriz.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&historyHoriz.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoomHoriz.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_alt_delta_multiplier_x"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_alt_delta_multiplier_x"
+ accesskey="&wheelSpeedHoriz.accesskey;"
+ preference="mousewheel.with_alt.delta_multiplier_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirectionHoriz.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+ </vbox>
+
+ <!-- control modifiers -->
+ <vbox>
+ <groupbox>
+ <caption label="&mouseWheelGroup.label;"/>
+ <radiogroup id="mousewheel_with_control_action"
+ preference="mousewheel.with_control.action"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnabling(this);">
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothing.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocument.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&history.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoom.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_control_delta_multiplier_y"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_control_delta_multiplier_y"
+ accesskey="&wheelSpeed.accesskey;"
+ preference="mousewheel.with_control.delta_multiplier_y"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirection.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&mouseWheelHorizGroup.label;"/>
+ <radiogroup id="mousewheel_with_control_action_x"
+ preference="mousewheel.with_control.action.override_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnablingX(this);">
+ <radio value="-1" label="&sameAsVertical.label;" accesskey="&sameAsVertical.accesskey;"/>
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothingHoriz.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocumentHoriz.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&historyHoriz.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoomHoriz.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_control_delta_multiplier_x"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_control_delta_multiplier_x"
+ accesskey="&wheelSpeedHoriz.accesskey;"
+ preference="mousewheel.with_control.delta_multiplier_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirectionHoriz.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+ </vbox>
+
+ <!-- shift modifiers -->
+ <vbox>
+ <groupbox>
+ <caption label="&mouseWheelGroup.label;"/>
+ <radiogroup id="mousewheel_with_shift_action"
+ preference="mousewheel.with_shift.action"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnabling(this);">
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothing.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocument.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&history.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoom.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_shift_delta_multiplier_y"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_shift_delta_multiplier_y"
+ accesskey="&wheelSpeed.accesskey;"
+ preference="mousewheel.with_shift.delta_multiplier_y"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirection.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&mouseWheelHorizGroup.label;"/>
+ <radiogroup id="mousewheel_with_shift_action_x"
+ preference="mousewheel.with_shift.action.override_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').doEnablingX(this);">
+ <radio value="-1" label="&sameAsVertical.label;" accesskey="&sameAsVertical.accesskey;"/>
+ <radio value="0" label="&doNothing.label;" accesskey="&doNothingHoriz.accesskey;"/>
+ <radio value="1" label="&scrollDocument.label;" accesskey="&scrollDocumentHoriz.accesskey;"/>
+ <radio value="2" label="&history.label;" accesskey="&historyHoriz.accesskey;"/>
+ <radio value="3" label="&zoom.label;" accesskey="&zoomHoriz.accesskey;"/>
+ </radiogroup>
+ <hbox align="center">
+ <label control="mousewheel_with_shift_delta_multiplier_x"
+ value="&wheelSpeed.label;"/>
+ <textbox type="number" min="-999999" max="999999" size="6"
+ id="mousewheel_with_shift_delta_multiplier_x"
+ accesskey="&wheelSpeedHoriz.accesskey;"
+ preference="mousewheel.with_shift.delta_multiplier_x"
+ onsyncfrompreference="document.getElementById('mousewheel_pane').updateCheckbox(this);"/>
+ <label value="%"/>
+ <checkbox label="&reverseDirection.label;"
+ accesskey="&reverseDirectionHoriz.accesskey;"
+ oncommand="updateTextbox(this);"/>
+ </hbox>
+ </groupbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-navigator.js b/comm/suite/components/pref/content/pref-navigator.js
new file mode 100644
index 0000000000..5ff271dc2b
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-navigator.js
@@ -0,0 +1,262 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {ShellService} = ChromeUtils.import("resource:///modules/ShellService.jsm");
+
+// The contents of this file will be loaded into the scope of the object
+// <prefpane id="navigator_pane">!
+
+// platform integration
+const PFINT_NOT_DEFAULT = 0;
+const PFINT_DEFAULT = 1;
+const PFINT_PENDING = 2;
+
+
+// put "global" definitions here for easy reference
+var gDefaultHomePage = "";
+var gHomePagePrefPeak = 0;
+var gPreferences = null;
+
+
+// <preferences> access helper methods
+function GetHomePagePrefCount()
+{
+ return document.getElementById("browser.startup.homepage.count").value;
+}
+
+function SetHomePagePrefCount(aCount)
+{
+ document.getElementById("browser.startup.homepage.count").value = aCount;
+}
+
+function GetHomePagePrefName(aIndex)
+{
+ var prefname = "browser.startup.homepage";
+ if (aIndex > 0)
+ prefname += "." + aIndex;
+ return prefname;
+}
+
+function GetHomePagePref(aIndex)
+{
+ // return the <preference> at aIndex
+ return document.getElementById(GetHomePagePrefName(aIndex));
+}
+
+function AddHomePagePref(aIndex)
+{
+ // create new <preference> for aIndex
+ var pref = document.createElement("preference");
+ var prefname = GetHomePagePrefName(aIndex);
+ pref.setAttribute("id", prefname);
+ pref.setAttribute("name", prefname);
+ pref.setAttribute("type", "wstring");
+ gPreferences.appendChild(pref);
+ return pref;
+}
+
+// homepage group textbox helper methods
+function GetHomePageGroup()
+{
+ return document.getElementById("browserStartupHomepage").value;
+}
+
+function SetHomePageValue(aValue)
+{
+ document.getElementById("browserStartupHomepage").value = aValue;
+}
+
+// helper methods for reading current page URIs
+function GetMostRecentBrowser()
+{
+ var browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ return browserWindow && browserWindow.getBrowser();
+}
+
+function GetCurrentPage()
+{
+ var tabbrowser = GetMostRecentBrowser();
+ return tabbrowser && tabbrowser.currentURI.spec || ""; // ensure string
+}
+
+function GetCurrentGroup()
+{
+ var uris = [];
+ var tabbrowser = GetMostRecentBrowser();
+ if (tabbrowser)
+ {
+ var browsers = tabbrowser.browsers;
+ var browsersLen = browsers.length;
+ for (var i = 0; i < browsersLen; ++i)
+ uris[i] = browsers[i].currentURI.spec;
+ }
+ return uris.join("\n");
+}
+
+// synchronize button states with current input
+function CanonifyURLList(aList)
+{
+ return (aList + "\n").replace(/\n+/g, "\n");
+}
+
+function UpdateHomePageButtons()
+{
+ var homePageGroup = CanonifyURLList(GetHomePageGroup());
+ var currentPage = CanonifyURLList(GetCurrentPage());
+ var currentGroup = CanonifyURLList(GetCurrentGroup());
+
+ // disable "current page" button if current page is already the homepage
+ var currentPageButton = document.getElementById("browserUseCurrent");
+ currentPageButton.disabled = (homePageGroup == currentPage) ||
+ (currentPage == "\n");
+
+ // disable "current group" button if current group already set or no group
+ var currentGroupButton = document.getElementById("browserUseCurrentGroup");
+ currentGroupButton.disabled = (homePageGroup == currentGroup) ||
+ (currentGroup == currentPage);
+
+ // disable "restore" button if homepage hasn't changed
+ var restoreButton = document.getElementById("browserUseDefault");
+ restoreButton.disabled = (homePageGroup == gDefaultHomePage);
+}
+
+function UpdateHomePagePrefs()
+{
+ // update the list of <preference>s to the current settings
+ var newCount = 1; // current number of homepages
+ var homePageGroup = CanonifyURLList(GetHomePageGroup()).split("\n");
+ GetHomePagePref(0).value = "about:blank"; // in case it's empty
+ if (homePageGroup[0])
+ {
+ // we have at least one homepage
+ // (the last index is always empty due to canonification)
+ newCount = homePageGroup.length - 1
+ for (var i = 0; i < newCount; ++i)
+ {
+ var pref = GetHomePagePref(i) || AddHomePagePref(i);
+ pref.value = homePageGroup[i];
+ }
+ }
+
+ // work around bug 410562:
+ // reset unneeded preferences on dialogaccept only
+
+ // update pref count watermark before setting new number of homepages
+ var alreadyRequested = (gHomePagePrefPeak > 0);
+ var oldCount = GetHomePagePrefCount();
+ if (gHomePagePrefPeak < oldCount)
+ gHomePagePrefPeak = oldCount;
+ SetHomePagePrefCount(newCount);
+
+ var needCleanup = (newCount < gHomePagePrefPeak);
+ if (document.documentElement.instantApply)
+ {
+ // throw away unneeded preferences now
+ if (needCleanup)
+ HomePagePrefCleanup();
+ }
+ else if (needCleanup != alreadyRequested)
+ {
+ // cleanup necessity changed
+ if (needCleanup)
+ {
+ // register OK handler for the capturing phase
+ window.addEventListener("dialogaccept", this.HomePagePrefCleanup, true);
+ }
+ else
+ {
+ // no cleanup necessary, remove OK handler
+ window.removeEventListener("dialogaccept", this.HomePagePrefCleanup, true);
+ }
+ }
+}
+
+function HomePagePrefCleanup()
+{
+ // remove the old user prefs values that we didn't overwrite
+ var count = GetHomePagePrefCount();
+ for (var j = count; j < gHomePagePrefPeak; ++j)
+ {
+ // clear <preference>
+ var pref = GetHomePagePref(j);
+ pref.valueFromPreferences = undefined;
+ pref.remove();
+ }
+ gHomePagePrefPeak = 0; // cleanup done
+}
+
+function UpdateHomePageListFromInput()
+{
+ UpdateHomePagePrefs();
+ UpdateHomePageButtons();
+}
+
+function UpdateHomePageList(aSingleURL)
+{
+ // write single URL into input box and set it as the list of homepages
+ SetHomePageValue(aSingleURL);
+ UpdateHomePageListFromInput();
+}
+
+function SelectFile()
+{
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ let title = document.getElementById("bundle_prefutilities")
+ .getString("choosehomepage");
+ fp.init(window, title, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll |
+ Ci.nsIFilePicker.filterText |
+ Ci.nsIFilePicker.filterXML |
+ Ci.nsIFilePicker.filterHTML |
+ Ci.nsIFilePicker.filterImages);
+
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK && fp.fileURL.spec &&
+ fp.fileURL.spec.length > 0) {
+ UpdateHomePageList(fp.fileURL.spec);
+ }
+ });
+}
+
+function SetHomePageToCurrentPage()
+{
+ UpdateHomePageList(GetCurrentPage());
+}
+
+function SetHomePageToCurrentGroup()
+{
+ UpdateHomePageList(GetCurrentGroup());
+}
+
+function SetHomePageToDefaultPage()
+{
+ UpdateHomePageList(gDefaultHomePage);
+}
+
+function Startup()
+{
+ // homepage groups can have an arbitrary number of <preference>s,
+ // except for the default (0), thus we create them manually here
+ gPreferences = document.getElementById("navigator_preferences");
+ var count = GetHomePagePrefCount();
+ var homePageGroup = GetHomePagePref(0).value + "\n";
+ for (var i = 1; i < count; ++i)
+ homePageGroup += AddHomePagePref(i).value + "\n";
+ gDefaultHomePage = CanonifyURLList(GetHomePagePref(0).defaultValue);
+ SetHomePageValue(homePageGroup);
+ UpdateHomePageButtons();
+}
+
+function SwitchPage(aIndex)
+{
+ document.getElementById("behaviourDeck").selectedIndex = aIndex;
+}
+
+function WriteConcurrentTabs()
+{
+ var val = document.getElementById("maxConcurrentTabsGroup").value;
+ return val > 0 ? document.getElementById("maxConcurrentTabs").value : val;
+}
diff --git a/comm/suite/components/pref/content/pref-navigator.xul b/comm/suite/components/pref/content/pref-navigator.xul
new file mode 100644
index 0000000000..2b9888419f
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-navigator.xul
@@ -0,0 +1,188 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % navigatorDTD SYSTEM "chrome://communicator/locale/pref/pref-navigator.dtd"> %navigatorDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="navigator_pane"
+ label="&pref.navigator.title;"
+ script="chrome://communicator/content/pref/pref-navigator.js">
+
+ <preferences id="navigator_preferences">
+ <preference id="browser.startup.page"
+ name="browser.startup.page"
+ type="int"/>
+ <preference id="browser.windows.loadOnNewWindow"
+ name="browser.windows.loadOnNewWindow"
+ type="int"/>
+ <preference id="browser.tabs.loadOnNewTab"
+ name="browser.tabs.loadOnNewTab"
+ type="int"/>
+ <preference id="browser.startup.homepage"
+ name="browser.startup.homepage"
+ type="wstring"/>
+ <preference id="browser.startup.homepage.count"
+ name="browser.startup.homepage.count"
+ type="int"/>
+ <preference id="browser.sessionstore.max_concurrent_tabs"
+ name="browser.sessionstore.max_concurrent_tabs"
+ type="int"/>
+ <preference id="browser.chrome.site_icons"
+ name="browser.chrome.site_icons"
+ type="bool"/>
+ <preference id="browser.chrome.favicons"
+ name="browser.chrome.favicons"
+ type="bool"/>
+ <preference id="pref.browser.homepage.disable_button.select_file"
+ name="pref.browser.homepage.disable_button.select_file"
+ type="bool"/>
+ <preference id="pref.browser.homepage.disable_button.current_page"
+ name="pref.browser.homepage.disable_button.current_page"
+ type="bool"/>
+ <preference id="pref.browser.homepage.disable_button.current_group"
+ name="pref.browser.homepage.disable_button.current_group"
+ type="bool"/>
+ <preference id="pref.browser.homepage.disable_button.default_page"
+ name="pref.browser.homepage.disable_button.default_page"
+ type="bool"/>
+ </preferences>
+
+ <hbox>
+ <!-- navigator startup / new window / new tab behaviour -->
+ <groupbox flex="1">
+ <caption align="center">
+ <label value="&navRadio.label;"
+ accesskey="&navRadio.accesskey;"
+ control="selectDisplayOn"/>
+ <menulist id="selectDisplayOn"
+ oncommand="SwitchPage(this.selectedIndex);">
+ <menupopup>
+ <menuitem label="&navStartPageMenu.label;"/>
+ <menuitem label="&newWinPageMenu.label;"/>
+ <menuitem label="&newTabPageMenu.label;"/>
+ </menupopup>
+ </menulist>
+ </caption>
+ <deck id="behaviourDeck" flex="1">
+ <radiogroup id="startupPage" preference="browser.startup.page">
+ <radio value="0"
+ label="&blankPageRadio.label;"
+ accesskey="&blankPageRadio.accesskey;"/>
+ <radio value="1"
+ label="&homePageRadio.label;"
+ accesskey="&homePageRadio.accesskey;"/>
+ <radio value="2"
+ label="&lastPageRadio.label;"
+ accesskey="&lastPageRadio.accesskey;"/>
+ <radio value="3"
+ label="&restoreSessionRadio.label;"
+ accesskey="&restoreSessionRadio.accesskey;"/>
+ </radiogroup>
+ <radiogroup id="newWinPage"
+ preference="browser.windows.loadOnNewWindow">
+ <radio value="0"
+ label="&blankPageRadio.label;"
+ accesskey="&blankPageRadio.accesskey;"/>
+ <radio value="1"
+ label="&homePageRadio.label;"
+ accesskey="&homePageRadio.accesskey;"/>
+ <radio value="2"
+ label="&lastPageRadio.label;"
+ accesskey="&lastPageRadio.accesskey;"/>
+ </radiogroup>
+ <radiogroup id="newTabPage" preference="browser.tabs.loadOnNewTab">
+ <radio value="0"
+ label="&blankPageRadio.label;"
+ accesskey="&blankPageRadio.accesskey;"/>
+ <radio value="1"
+ label="&homePageRadio.label;"
+ accesskey="&homePageRadio.accesskey;"/>
+ <radio value="2"
+ label="&lastPageRadio.label;"
+ accesskey="&lastPageRadio.accesskey;"/>
+ </radiogroup>
+ </deck>
+ </groupbox>
+
+ <!-- session restore background tabs -->
+ <groupbox flex="1">
+ <caption label="&restoreSessionIntro.label;"/>
+ <radiogroup id="maxConcurrentTabsGroup"
+ align="start"
+ preference="browser.sessionstore.max_concurrent_tabs"
+ onsyncfrompreference="var val = document.getElementById(this.getAttribute('preference')).value; return val > 0 ? 3 : val;"
+ onsynctopreference="return document.getElementById('navigator_pane').WriteConcurrentTabs();">
+ <radio value="-1"
+ label="&restoreImmediately.label;"
+ accesskey="&restoreImmediately.accesskey;"/>
+ <hbox align="center">
+ <radio id="restoreTabs"
+ value="3"
+ onclick="this.nextSibling.focus();"
+ label="&restoreTabs.label;"
+ accesskey="&restoreTabs.accesskey;"/>
+ <textbox id="maxConcurrentTabs"
+ type="number"
+ size="2"
+ min="1"
+ value="3"
+ aria-labelledby="restoreTabs maxConcurrentTabs restoreTabsAtATime"
+ preference="browser.sessionstore.max_concurrent_tabs"
+ onsyncfrompreference="var pref = document.getElementById(this.getAttribute('preference')); var val = pref.value; var valid = val > 0; this.disabled = pref.locked || !valid; return valid ? val : this.value;"
+ onsynctopreference="return document.getElementById('navigator_pane').WriteConcurrentTabs();"/>
+ <label id="restoreTabsAtATime" value="&restoreTabsAtATime.label;">
+ <observes element="maxConcurrentTabsGroup" attribute="disabled"/>
+ </label>
+ </hbox>
+ <radio value="0"
+ label="&restoreDeferred.label;"
+ accesskey="&restoreDeferred.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ </hbox>
+
+ <groupbox id="siteIconPreferences">
+ <caption label="&siteIcons.label;"/>
+
+ <checkbox id="useSiteIcons"
+ label="&useSiteIcons.label;"
+ accesskey="&useSiteIcons.accesskey;"
+ preference="browser.chrome.site_icons"/>
+ <checkbox id="useFavIcons"
+ label="&useFavIcons.label;"
+ accesskey="&useFavIcons.accesskey;"
+ preference="browser.chrome.favicons"/>
+ </groupbox>
+
+ <!-- homepage specification -->
+ <description>&homePageIntro.label;</description>
+ <hbox>
+ <textbox id="browserStartupHomepage" class="uri-element" flex="1"
+ multiline="true" wrap="off" timeout="500"
+ oninput="UpdateHomePageListFromInput();"/>
+ <vbox>
+ <button label="&browseFile.label;" accesskey="&browseFile.accesskey;"
+ oncommand="SelectFile();"
+ id="browserChooseFile"
+ preference="pref.browser.homepage.disable_button.select_file"/>
+ <button label="&useCurrent.label;" accesskey="&useCurrent.accesskey;"
+ oncommand="SetHomePageToCurrentPage();"
+ id="browserUseCurrent"
+ preference="pref.browser.homepage.disable_button.current_page"/>
+ <button label="&useCurrentGroup.label;" accesskey="&useCurrentGroup.accesskey;"
+ oncommand="SetHomePageToCurrentGroup();"
+ id="browserUseCurrentGroup"
+ preference="pref.browser.homepage.disable_button.current_group"/>
+ <button label="&useDefault.label;" accesskey="&useDefault.accesskey;"
+ oncommand="SetHomePageToDefaultPage();"
+ id="browserUseDefault"
+ preference="pref.browser.homepage.disable_button.default_page"/>
+ </vbox>
+ </hbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-offlineapps.js b/comm/suite/components/pref/content/pref-offlineapps.js
new file mode 100644
index 0000000000..db7c44cb81
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-offlineapps.js
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function Startup()
+{
+ OfflineAppsObserver.init();
+
+ let always = document.getElementById("offline-apps.allow_by_default").value;
+ UpdateNotifyBox(always);
+}
+
+var OfflineAppsObserver = {
+
+ init: function offlineAppsInit() {
+ this.update();
+ Services.obs.addObserver(this, "perm-changed");
+ window.addEventListener("unload", this);
+ },
+
+ update: function offlineAppsUpdate() {
+ UpdateActualCacheSize();
+ UpdateOfflineApps();
+ },
+
+ observe: function offlineAppsObserve(aSubject, aTopic, aData) {
+ if (aTopic == "perm-changed")
+ this.update();
+ },
+
+ handleEvent: function offlineAppsEvent(aEvent) {
+ if (aEvent.type == "unload") {
+ window.removeEventListener("unload", this);
+ Services.obs.removeObserver(this, "perm-changed");
+ }
+ }
+}
+
+function UpdateActualCacheSize()
+{
+ var visitor = {
+ onCacheStorageInfo: function(aEntryCount, aTotalSize)
+ {
+ let actualSizeLabel = document.getElementById("offlineAppSizeInfo");
+ let sizeStrings = DownloadUtils.convertByteUnits(aTotalSize);
+ let bundle = document.getElementById("bundle_prefutilities");
+ let sizeStr = bundle.getFormattedString("offlineAppSizeInfo",
+ sizeStrings);
+ actualSizeLabel.textContent = sizeStr;
+ },
+
+ onCacheEntryInfo: function(entryInfo)
+ {
+ },
+
+ onCacheEntryVisitCompleted: function()
+ {
+ }
+ };
+
+ Services.cache2.appCacheStorage(Services.loadContextInfo.default, null)
+ .asyncVisitStorage(visitor, false);
+}
+
+/**
+ * Clears the application cache.
+ */
+var callback = {
+ onCacheEntryDoomed: function(aResult) {
+ UpdateActualCacheSize();
+ UpdateOfflineApps();
+ }
+};
+
+function ClearOfflineAppCache()
+{
+ try {
+ Services.cache2.appCacheStorage(Services.loadContextInfo.default, null)
+ .asyncEvictStorage(callback);
+ } catch(ex) {}
+}
+
+function UpdateNotifyBox(aValue)
+{
+ EnableElementById("offlineNotifyAsk", !aValue);
+
+ // remove this once bug 934457 and bug 1024832 are fixed
+ document.getElementById("offlineNotifyPermissions").disabled = aValue;
+}
+
+function _getOfflineAppUsage(aPermission)
+{
+ var appCache = Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+ var groups = appCache.getGroups();
+
+ var usage = 0;
+ for (let i = 0; i < groups.length; i++) {
+ let uri = Services.io.newURI(groups[i]);
+ if (aPermission.matchesURI(uri, true))
+ usage += appCache.getActiveCache(groups[i]).usage;
+ }
+ return usage;
+}
+
+/**
+ * Updates the list of offline applications.
+ */
+function UpdateOfflineApps()
+{
+ var list = document.getElementById("offlineAppsList");
+ while (list.hasChildNodes())
+ list.lastChild.remove();
+
+ var bundle = document.getElementById("bundle_prefutilities");
+ var pm = Services.perms;
+ var enumerator = pm.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext()
+ .QueryInterface(Ci.nsIPermission);
+ if (perm.type != "offline-app" ||
+ perm.capability != pm.ALLOW_ACTION)
+ continue;
+
+ let usage = _getOfflineAppUsage(perm);
+ let row = document.createElement("listitem");
+ row.setAttribute("host", perm.principal.URI.host);
+ let converted = DownloadUtils.convertByteUnits(usage);
+ row.setAttribute("usage", bundle.getFormattedString("offlineAppUsage",
+ converted));
+ list.appendChild(row);
+ }
+}
+
+function OfflineAppSelected(aList)
+{
+ document.getElementById("offlineAppsListRemove")
+ .setAttribute("disabled", !aList.selectedItem);
+}
+
+function RemoveOfflineApp()
+{
+ var list = document.getElementById("offlineAppsList");
+ var item = list.selectedItem;
+ var host = item.getAttribute("host");
+
+ var flags = Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
+
+ var bundle = document.getElementById("bundle_prefutilities");
+ var title = bundle.getString("offlineAppRemoveTitle");
+ var prompt = bundle.getFormattedString("offlineAppRemovePrompt", [host]);
+ var confirm = bundle.getString("offlineAppRemoveConfirm");
+ if (Services.prompt.confirmEx(window, title, prompt, flags, confirm,
+ null, null, null, {}))
+ return;
+
+ // clear offline cache entries
+ var appCache = Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+ var groups = appCache.getGroups();
+ for (let i = 0; i < groups.length; i++) {
+ var uri = Services.io.newURI(groups[i]);
+ if (uri.asciiHost == host)
+ appCache.getActiveCache(groups[i]).discard();
+ }
+
+ // remove the permission
+ // Services.perms.remove(host, "offline-app");
+
+ UpdateOfflineApps();
+ OfflineAppSelected(list);
+ UpdateActualCacheSize();
+}
diff --git a/comm/suite/components/pref/content/pref-offlineapps.xul b/comm/suite/components/pref/content/pref-offlineapps.xul
new file mode 100644
index 0000000000..d12a26c808
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-offlineapps.xul
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % prefOfflineCacheDTD SYSTEM "chrome://communicator/locale/pref/pref-offlineapps.dtd">
+ %prefOfflineCacheDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="offlineapps_pane"
+ label="&pref.offlineapps.title;"
+ script="chrome://communicator/content/pref/pref-offlineapps.js">
+
+ <preferences>
+ <preference id="offline-apps.allow_by_default"
+ name="offline-apps.allow_by_default"
+ type="bool"
+ onchange="UpdateNotifyBox(this.value);"/>
+ <preference id="browser.offline-apps.notify"
+ name="browser.offline-apps.notify"
+ type="bool"/>
+ </preferences>
+
+ <groupbox id="offlineGroup" flex="1">
+ <caption label="&pref.offlineCache.caption;"/>
+
+ <hbox align="center">
+ <label id="offlineAppSizeInfo" flex="1"/>
+ <button id="clearOfflineAppCache"
+ icon="clear"
+ label="&clearOfflineAppCache.label;"
+ accesskey="&clearOfflineAppCache.accesskey;"
+ oncommand="ClearOfflineAppCache();"/>
+ </hbox>
+ <radiogroup id="offlineDefault"
+ preference="offline-apps.allow_by_default">
+ <radio id="offlineAlwaysAllow"
+ value="true"
+ label="&offlineAlwaysAllow.label;"
+ accesskey="&offlineAlwaysAllow.accesskey;"/>
+ <hbox align="center">
+ <radio id="offlineExplicit"
+ flex="1"
+ value="false"
+ label="&offlineExplicit.label;"
+ accesskey="&offlineExplicit.accesskey;"/>
+ <button id="offlineNotifyPermissions"
+ label="&offlineNotifyPermissions.label;"
+ accesskey="&offlineNotifyPermissions.accesskey;"
+ oncommand="toDataManager('|permissions');"/>
+ </hbox>
+ </radiogroup>
+ <checkbox id="offlineNotifyAsk"
+ class="indent"
+ label="&offlineNotifyAsk.label;"
+ accesskey="&offlineNotifyAsk.accesskey;"
+ preference="browser.offline-apps.notify"/>
+ <separator class="thin"/>
+ <hbox flex="1">
+ <vbox flex="1">
+ <label id="offlineAppsListLabel">&offlineAppsUsage.label;</label>
+ <listbox id="offlineAppsList"
+ flex="1"
+ aria-labelledby="offlineAppsListLabel"
+ onselect="OfflineAppSelected(this);">
+ </listbox>
+ </vbox>
+ <vbox pack="end">
+ <button id="offlineAppsListRemove"
+ disabled="true"
+ label="&offlineAppsListRemove.label;"
+ accesskey="&offlineAppsListRemove.accesskey;"
+ oncommand="RemoveOfflineApp();"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-popups.js b/comm/suite/components/pref/content/pref-popups.js
new file mode 100644
index 0000000000..dc8a2b42c2
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-popups.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gSoundUrlPref;
+
+function Startup()
+{
+ gSoundUrlPref = document.getElementById("privacy.popups.sound_url");
+
+ SetLists();
+
+ SetButtons();
+}
+
+function SetLists()
+{
+ const kPopupType = "popup";
+
+ var pref = document.getElementById("privacy.popups.remove_blacklist");
+ if (pref.value)
+ {
+ var enumerator = Services.perms.enumerator;
+ var uris = [];
+
+ while (enumerator.hasMoreElements())
+ {
+ var permission = enumerator.getNext();
+ if (permission instanceof Ci.nsIPermission)
+ {
+ if ((permission.type == kPopupType) &&
+ (permission.capability == Ci.nsIPermissionManager.DENY_ACTION))
+ uris.push(permission.principal.URI);
+ }
+ }
+
+ for (var i in uris)
+ Services.perms.remove(uris[i], kPopupType);
+
+ pref.value = false;
+ }
+
+ pref = document.getElementById("privacy.popups.prefill_whitelist");
+ if (pref.value)
+ {
+ try
+ {
+ var whitelist = document.getElementById("privacy.popups.default_whitelist").value;
+ var hosts = whitelist.split(",");
+
+ for (var i in hosts)
+ {
+ var host = "http://" + hosts[i];
+ var uri = Services.io.newURI(host);
+ Services.perms.add(uri, kPopupType, true);
+ }
+ }
+ catch (ex) {}
+
+ pref.value = false;
+ }
+}
+
+function SetButtons()
+{
+ var prefString = document.getElementById("popupPolicy")
+ .getAttribute("preference");
+ var enable = document.getElementById(prefString).value;
+ EnableElementById("exceptionsButton", enable, false);
+ EnableElementById("displayIcon", enable, false);
+ EnableElementById("displayPopupsNotification", enable, false);
+
+ var element = document.getElementById("playSound");
+ EnableElement(element, enable, false);
+
+ prefString = element.getAttribute("preference");
+ EnableSoundRadio(enable && document.getElementById(prefString).value);
+}
+
+function EnableSoundRadio(aSoundChecked)
+{
+ const kCustomSound = 1;
+
+ var element = document.getElementById("popupSoundType");
+ EnableElement(element, aSoundChecked, false);
+ var pref = document.getElementById(element.getAttribute("preference"));
+ EnableSoundUrl(aSoundChecked && (pref.value == kCustomSound));
+}
+
+function EnableSoundUrl(aCustomSelected)
+{
+ EnableElementById("playSoundUrl", aCustomSelected, false);
+ EnableElementById("selectSound", aCustomSelected, false);
+ EnableElementById("playSoundButton", aCustomSelected, false);
+}
diff --git a/comm/suite/components/pref/content/pref-popups.xul b/comm/suite/components/pref/content/pref-popups.xul
new file mode 100644
index 0000000000..78f7a75e57
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-popups.xul
@@ -0,0 +1,132 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % prefPopupsDTD SYSTEM "chrome://communicator/locale/pref/pref-popups.dtd">
+%prefPopupsDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="popups_pane"
+ label="&pref.popups.title;"
+ script="chrome://communicator/content/pref/pref-popups.js">
+ <preferences id="popups_preferences">
+ <preference id="dom.disable_open_during_load"
+ name="dom.disable_open_during_load"
+ type="bool"
+ onchange="SetButtons();"/>
+ <preference id="pref.advanced.popups.disable_button.view_popups"
+ name="pref.advanced.popups.disable_button.view_popups"
+ type="bool"/>
+ <preference id="privacy.popups.sound_enabled"
+ name="privacy.popups.sound_enabled"
+ type="bool"
+ onchange="EnableSoundRadio(this.value);"/>
+ <preference id="privacy.popups.sound_type"
+ name="privacy.popups.sound_type"
+ type="int"
+ onchange="EnableSoundUrl(this.value == 1);"/>
+ <preference id="privacy.popups.sound_url"
+ name="privacy.popups.sound_url"
+ type="string"
+ onchange="EnableElementById('previewSound', true, false);"/>
+ <preference id="pref.advanced.popups.disable_button.select_sound"
+ name="pref.advanced.popups.disable_button.select_sound"
+ type="bool"/>
+ <preference id="pref.advanced.popups.disable_button.preview_sound"
+ name="pref.advanced.popups.disable_button.preview_sound"
+ type="bool"/>
+ <preference id="privacy.popups.statusbar_icon_enabled"
+ name="privacy.popups.statusbar_icon_enabled"
+ type="bool"/>
+ <preference id="privacy.popups.showBrowserMessage"
+ name="privacy.popups.showBrowserMessage"
+ type="bool"/>
+ <preference id="privacy.popups.prefill_whitelist"
+ name="privacy.popups.prefill_whitelist"
+ type="bool"/>
+ <preference id="privacy.popups.remove_blacklist"
+ name="privacy.popups.remove_blacklist"
+ type="bool"/>
+ <preference id="privacy.popups.default_whitelist"
+ name="privacy.popups.default_whitelist"
+ type="string"/>
+ </preferences>
+
+ <groupbox id="popupsArea">
+ <caption label="&pref.popups.caption;"/>
+
+ <hbox>
+ <checkbox id="popupPolicy"
+ label="&popupBlock.label;"
+ accesskey="&popupBlock.accesskey;"
+ preference="dom.disable_open_during_load"/>
+ <spacer flex="1"/>
+ <button id="exceptionsButton"
+ label="&viewPermissions.label;"
+ accesskey="&viewPermissions.accesskey;"
+ preference="pref.advanced.popups.disable_button.view_popups"
+ oncommand="toDataManager('|permissions');"/>
+ </hbox>
+ <separator class="thin"/>
+ <description id="whenBlock">&whenBlock.description;</description>
+ <hbox>
+ <checkbox id="playSound"
+ label="&playSound.label;"
+ accesskey="&playSound.accesskey;"
+ preference="privacy.popups.sound_enabled"/>
+ </hbox>
+ <hbox class="indent">
+ <radiogroup id="popupSoundType"
+ preference="privacy.popups.sound_type"
+ aria-labelledby="playSound">
+ <radio id="popupSystemSound"
+ class="iconic"
+ value="0"
+ label="&systemSound.label;"
+ accesskey="&systemSound.accesskey;"/>
+ <radio id="popupCustomSound"
+ class="iconic"
+ value="1"
+ label="&customSound.label;"
+ accesskey="&customSound.accesskey;"/>
+ </radiogroup>
+ </hbox>
+ <hbox class="indent">
+ <filefield id="playSoundUrl"
+ flex="1"
+ preference="privacy.popups.sound_url"
+ preference-editable="true"
+ onsyncfrompreference="return WriteSoundField(this, document.getElementById('popups_pane').gSoundUrlPref.value);"
+ aria-labelledby="popupCustomSound"/>
+ <button id="selectSound"
+ label="&selectSound.label;"
+ accesskey="&selectSound.accesskey;"
+ preference="pref.advanced.popups.disable_button.select_sound"
+ oncommand="SelectSound(gSoundUrlPref);"/>
+ <button id="playSoundButton"
+ label="&playSoundButton.label;"
+ accesskey="&playSoundButton.accesskey;"
+ preference="pref.advanced.popups.disable_button.preview_sound"
+ oncommand="PlaySound(gSoundUrlPref.value, false);"/>
+ </hbox>
+ <hbox>
+ <checkbox id="displayIcon"
+ label="&displayIcon.label;"
+ accesskey="&displayIcon.accesskey;"
+ preference="privacy.popups.statusbar_icon_enabled"/>
+ </hbox>
+ <hbox>
+ <checkbox id="displayPopupsNotification"
+ label="&displayNotification.label;"
+ accesskey="&displayNotification.accesskey;"
+ preference="privacy.popups.showBrowserMessage"/>
+ </hbox>
+ <separator class="thin"/>
+ <description>&popupNote.description;</description>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-privatedata.js b/comm/suite/components/pref/content/pref-privatedata.js
new file mode 100644
index 0000000000..ba7305bc41
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-privatedata.js
@@ -0,0 +1,30 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup() {
+ let pref = document.getElementById("privacy.sanitize.sanitizeOnShutdown");
+ updateClearOnShutdownBox(pref.valueFromPreferences);
+}
+
+/**
+ * Disable/enable clear on shutdown items in dialog depending on general pref
+ * to clear on shutdown.
+ */
+function updateClearOnShutdownBox(aDisable) {
+ let clearOnShutdownBox = document.getElementById("clearOnShutdownBox");
+ for (let childNode of clearOnShutdownBox.childNodes) {
+ childNode.disabled = !aDisable;
+ }
+}
+
+/**
+ * Displays a dialog from which individual parts of private data may be
+ * cleared.
+ */
+function clearPrivateDataNow() {
+ Cc["@mozilla.org/suite/suiteglue;1"]
+ .getService(Ci.nsISuiteGlue)
+ .sanitize(window);
+}
diff --git a/comm/suite/components/pref/content/pref-privatedata.xul b/comm/suite/components/pref/content/pref-privatedata.xul
new file mode 100644
index 0000000000..97e236def8
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-privatedata.xul
@@ -0,0 +1,181 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % prefPrivateDataDTD SYSTEM "chrome://communicator/locale/pref/pref-privatedata.dtd">
+ %prefPrivateDataDTD;
+ <!ENTITY % prefSanitizeDTD SYSTEM "chrome://communicator/locale/sanitize.dtd">
+ %prefSanitizeDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="privatedata_pane" label="&pref.privatedata.title;"
+ script="chrome://communicator/content/pref/pref-privatedata.js">
+ <preferences id="privatedata_preferences">
+ <!-- Clear Private Data -->
+ <preference id="privacy.sanitize.sanitizeOnShutdown"
+ name="privacy.sanitize.sanitizeOnShutdown"
+ type="bool"
+ onchange="updateClearOnShutdownBox(this.value);"/>
+ <!-- Clear Private Data on shutdown -->
+ <preference id="privacy.clearOnShutdown.history"
+ name="privacy.clearOnShutdown.history"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.urlbar"
+ name="privacy.clearOnShutdown.urlbar"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.formdata"
+ name="privacy.clearOnShutdown.formdata"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.passwords"
+ name="privacy.clearOnShutdown.passwords"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.downloads"
+ name="privacy.clearOnShutdown.downloads"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.cookies"
+ name="privacy.clearOnShutdown.cookies"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.cache"
+ name="privacy.clearOnShutdown.cache"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.offlineApps"
+ name="privacy.clearOnShutdown.offlineApps"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.sessions"
+ name="privacy.clearOnShutdown.sessions"
+ type="bool"/>
+ <preference id="privacy.clearOnShutdown.siteSettings"
+ name="privacy.clearOnShutdown.siteSettings"
+ type="bool"/>
+
+ <!-- Clear Private Data manually -->
+ <preference id="privacy.cpd.history"
+ name="privacy.cpd.history"
+ type="bool"/>
+ <preference id="privacy.cpd.urlbar"
+ name="privacy.cpd.urlbar"
+ type="bool"/>
+ <preference id="privacy.cpd.formdata"
+ name="privacy.cpd.formdata"
+ type="bool"/>
+ <preference id="privacy.cpd.passwords"
+ name="privacy.cpd.passwords"
+ type="bool"/>
+ <preference id="privacy.cpd.downloads"
+ name="privacy.cpd.downloads"
+ type="bool"/>
+ <preference id="privacy.cpd.cookies"
+ name="privacy.cpd.cookies"
+ type="bool"/>
+ <preference id="privacy.cpd.cache"
+ name="privacy.cpd.cache"
+ type="bool"/>
+ <preference id="privacy.cpd.offlineApps"
+ name="privacy.cpd.offlineApps"
+ type="bool"/>
+ <preference id="privacy.cpd.sessions"
+ name="privacy.cpd.sessions"
+ type="bool"/>
+ <preference id="privacy.cpd.siteSettings"
+ name="privacy.cpd.siteSettings"
+ type="bool"/>
+ </preferences>
+
+ <!-- Clear Private Data -->
+ <groupbox id="clearPrivateDataGroup">
+ <caption label="&clearPrivateData.label;"/>
+ <button id="clearDataNow" icon="clear"
+ label="&clearDataDialog.label;"
+ accesskey="&clearDataDialog.accesskey;"
+ oncommand="clearPrivateDataNow();"/>
+ <separator class="thin" />
+ <hbox id="clearDataBox" align="center">
+ <checkbox id="alwaysClear" flex="1"
+ label="&alwaysClear.label;"
+ accesskey="&alwaysClear.accesskey;"
+ preference="privacy.sanitize.sanitizeOnShutdown"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <label id="clearDataSettings"
+ value="&clearData.label;"/>
+
+ <hbox>
+ <groupbox id="clearCpdBox" flex="1">
+ <caption label="&clearData.cpd.label;"/>
+ <checkbox label="&itemHistory.label;"
+ accesskey="&itemHistory.accesskey;"
+ preference="privacy.cpd.history"/>
+ <checkbox label="&itemUrlBar.label;"
+ accesskey="&itemUrlBar.accesskey;"
+ preference="privacy.cpd.urlbar"/>
+ <checkbox label="&itemDownloads.label;"
+ accesskey="&itemDownloads.accesskey;"
+ preference="privacy.cpd.downloads"/>
+ <checkbox label="&itemFormSearchHistory.label;"
+ accesskey="&itemFormSearchHistory.accesskey;"
+ preference="privacy.cpd.formdata"/>
+ <checkbox label="&itemCache.label;"
+ accesskey="&itemCache.accesskey;"
+ preference="privacy.cpd.cache"/>
+ <checkbox label="&itemCookies.label;"
+ accesskey="&itemCookies.accesskey;"
+ preference="privacy.cpd.cookies"/>
+ <checkbox label="&itemOfflineApps.label;"
+ accesskey="&itemOfflineApps.accesskey;"
+ preference="privacy.cpd.offlineApps"/>
+ <checkbox label="&itemPasswords.label;"
+ accesskey="&itemPasswords.accesskey;"
+ preference="privacy.cpd.passwords"/>
+ <checkbox label="&itemSessions.label;"
+ accesskey="&itemSessions.accesskey;"
+ preference="privacy.cpd.sessions"/>
+ <checkbox label="&itemSitePreferences.label;"
+ accesskey="&itemSitePreferences.accesskey;"
+ preference="privacy.cpd.siteSettings"/>
+ </groupbox>
+
+ <groupbox id="clearOnShutdownBox" flex="1">
+ <caption label="&clearData.onShutdown.label;"/>
+ <checkbox label="&itemHistory.label;"
+ accesskey="&itemHistoryS.accesskey;"
+ preference="privacy.clearOnShutdown.history"/>
+ <checkbox label="&itemUrlBar.label;"
+ accesskey="&itemUrlBarS.accesskey;"
+ preference="privacy.clearOnShutdown.urlbar"/>
+ <checkbox label="&itemDownloads.label;"
+ accesskey="&itemDownloadsS.accesskey;"
+ preference="privacy.clearOnShutdown.downloads"/>
+ <checkbox label="&itemFormSearchHistory.label;"
+ accesskey="&itemFormSearchHistoryS.accesskey;"
+ preference="privacy.clearOnShutdown.formdata"/>
+ <checkbox label="&itemCache.label;"
+ accesskey="&itemCacheS.accesskey;"
+ preference="privacy.clearOnShutdown.cache"/>
+ <checkbox label="&itemCookies.label;"
+ accesskey="&itemCookiesS.accesskey;"
+ preference="privacy.clearOnShutdown.cookies"/>
+ <checkbox label="&itemOfflineApps.label;"
+ accesskey="&itemOfflineAppsS.accesskey;"
+ preference="privacy.clearOnShutdown.offlineApps"/>
+ <checkbox label="&itemPasswords.label;"
+ accesskey="&itemPasswordsS.accesskey;"
+ preference="privacy.clearOnShutdown.passwords"/>
+ <checkbox label="&itemSessions.label;"
+ accesskey="&itemSessionsS.accesskey;"
+ preference="privacy.clearOnShutdown.sessions"/>
+ <checkbox label="&itemSitePreferences.label;"
+ accesskey="&itemSitePreferencesS.accesskey;"
+ preference="privacy.clearOnShutdown.siteSettings"/>
+ </groupbox>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-proxies-advanced.xul b/comm/suite/components/pref/content/pref-proxies-advanced.xul
new file mode 100644
index 0000000000..69313f80e7
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-proxies-advanced.xul
@@ -0,0 +1,194 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE prefwindow SYSTEM "chrome://communicator/locale/pref/pref-proxies-advanced.dtd" >
+
+<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="advancedProxyPreferences"
+ type="child"
+ onload="AdvancedInit();"
+ title="&pref.proxies.advanced.title;"
+ persist="screenX screenY">
+
+ <script src="chrome://communicator/content/pref/pref-proxies.js"/>
+ <script src="chrome://communicator/content/pref/preferences.js"/>
+
+ <prefpane helpTopic="nav-prefs-advanced-proxy-advanced"
+ helpURI="chrome://communicator/locale/help/suitehelp.rdf">
+ <preferences>
+ <preference id="network.proxy.http"
+ name="network.proxy.http"
+ type="string"
+ onchange="DoProxyHostCopy(this.value);"/>
+ <preference id="network.proxy.http_port"
+ name="network.proxy.http_port"
+ type="int"
+ onchange="DoProxyPortCopy(this.value);"/>
+ <preference id="network.proxy.ssl"
+ name="network.proxy.ssl"
+ type="string"/>
+ <preference id="network.proxy.ssl_port"
+ name="network.proxy.ssl_port"
+ type="int"/>
+ <preference id="network.proxy.ftp"
+ name="network.proxy.ftp"
+ type="string"/>
+ <preference id="network.proxy.ftp_port"
+ name="network.proxy.ftp_port"
+ type="int"/>
+ <preference id="network.proxy.share_proxy_settings"
+ name="network.proxy.share_proxy_settings"
+ type="bool"
+ onchange="DoProxyCopy(this.value);"/>
+ <preference id="network.proxy.socks"
+ name="network.proxy.socks"
+ type="string"/>
+ <preference id="network.proxy.socks_port"
+ name="network.proxy.socks_port"
+ type="int"/>
+ <preference id="network.proxy.socks_version"
+ name="network.proxy.socks_version"
+ type="int"/>
+ <preference id="network.proxy.socks_remote_dns"
+ name="network.proxy.socks_remote_dns"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&protocols.caption;"/>
+ <description style="width: 1px;">&protocols.description;</description>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row>
+ <hbox align="center" pack="end">
+ <label value="&http.label;"
+ accesskey="&http.accesskey;"
+ control="networkProxyHTTP"/>
+ </hbox>
+ <hbox align="center">
+ <textbox id="networkProxyHTTP"
+ preference="network.proxy.http"
+ flex="1"
+ class="uri-element"/>
+ <label value="&port.label;"
+ accesskey="&HTTPPort.accesskey;"
+ control="networkProxyHTTP_Port"/>
+ <textbox id="networkProxyHTTP_Port"
+ preference="network.proxy.http_port"
+ type="number"
+ max="65535"
+ size="5"/>
+ </hbox>
+ </row>
+
+ <row>
+ <spacer/>
+ <hbox>
+ <checkbox id="networkProxyShareSettings"
+ label="&reuseProxy.label;"
+ accesskey="&reuseProxy.accesskey;"
+ preference="network.proxy.share_proxy_settings"/>
+ </hbox>
+ </row>
+
+ <row>
+ <hbox align="center" pack="end">
+ <label value="&ssl.label;"
+ accesskey="&ssl.accesskey;"
+ control="networkProxySSL"/>
+ </hbox>
+ <hbox align="center">
+ <textbox id="networkProxySSL"
+ preference="network.proxy.ssl"
+ flex="1"
+ class="uri-element"/>
+ <label value="&port.label;"
+ accesskey="&SSLPort.accesskey;"
+ control="networkProxySSL_Port"/>
+ <textbox id="networkProxySSL_Port"
+ preference="network.proxy.ssl_port"
+ type="number"
+ max="65535"
+ size="5"/>
+ </hbox>
+ </row>
+
+ <row>
+ <hbox align="center" pack="end">
+ <label value="&ftp.label;" accesskey="&ftp.accesskey;"
+ control="networkProxyFTP"/>
+ </hbox>
+ <hbox align="center">
+ <textbox id="networkProxyFTP"
+ preference="network.proxy.ftp"
+ flex="1"
+ class="uri-element"/>
+ <label value="&port.label;"
+ accesskey="&FTPPort.accesskey;"
+ control="networkProxyFTP_Port"/>
+ <textbox id="networkProxyFTP_Port"
+ preference="network.proxy.ftp_port"
+ type="number"
+ max="65535"
+ size="5"/>
+ </hbox>
+ </row>
+
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&socks.caption;"/>
+ <description style="width: 1px;">&socks.description;</description>
+
+ <hbox align="center" pack="end">
+ <label value="&socks.label;"
+ accesskey="&socks.accesskey;"
+ control="networkProxySOCKS"/>
+ <textbox id="networkProxySOCKS"
+ preference="network.proxy.socks"
+ flex="1"
+ class="uri-element"/>
+ <label value="&port.label;"
+ accesskey="&SOCKSport.accesskey;"
+ control="networkProxySOCKS_Port"/>
+ <textbox id="networkProxySOCKS_Port"
+ type="number"
+ preference="network.proxy.socks_port"
+ max="65535"
+ size="5"/>
+ </hbox>
+
+ <radiogroup id="networkProxySOCKSVersion"
+ orient="horizontal"
+ preference="network.proxy.socks_version">
+ <radio id="networkProxySOCKSVersion4"
+ value="4"
+ label="&socks4.label;"
+ accesskey="&socks4.accesskey;"/>
+ <radio id="networkProxySOCKSVersion5"
+ value="5"
+ label="&socks5.label;"
+ accesskey="&socks5.accesskey;"/>
+ </radiogroup>
+
+ <hbox align="left">
+ <checkbox id="networkProxySOCKSRemoteDNS"
+ label="&socksRemoteDNS.label;"
+ accesskey="&socksRemoteDNS.accesskey;"
+ preference="network.proxy.socks_remote_dns"/>
+ </hbox>
+
+ </groupbox>
+ </prefpane>
+</prefwindow>
diff --git a/comm/suite/components/pref/content/pref-proxies.js b/comm/suite/components/pref/content/pref-proxies.js
new file mode 100644
index 0000000000..5120c3f5d0
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-proxies.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const kNoProxy = 0;
+const kManualProxy = 1;
+const kAutoConfigProxy = 2;
+const kObsoleteProxy = 3;
+const kAutoDiscoverProxy = 4;
+const kSystemProxy = 5;
+
+var gInstantApply;
+var gHTTP;
+var gHTTPPort;
+var gSSL;
+var gSSLPort;
+var gFTP;
+var gFTPPort;
+var gAutoURL;
+var gProxyType;
+var gShareSettings;
+
+// Only used by main prefwindow
+function Startup()
+{
+ InitCommonGlobals();
+ gAutoURL = document.getElementById("network.proxy.autoconfig_url");
+ gProxyType = document.getElementById("network.proxy.type");
+
+ // Check for system proxy settings class and unhide UI if present
+ if ("@mozilla.org/system-proxy-settings;1" in Cc)
+ document.getElementById("systemPref").hidden = false;
+
+ // Calculate a sane default for network.proxy.share_proxy_settings.
+ if (gShareSettings.value == null)
+ gShareSettings.value = DefaultForShareSettingsPref();
+
+ // The pref value 3 (kObsoleteProxy) for network.proxy.type is unused to
+ // maintain backwards compatibility. Treat 3 (kObsoleteProxy) equally to
+ // 0 (kNoProxy). See bug 115720.
+ if (gProxyType.value == kObsoleteProxy)
+ gProxyType.value = kNoProxy;
+
+ DoEnabling();
+}
+
+// Only used by child prefwindow
+function AdvancedInit()
+{
+ InitCommonGlobals();
+ DoProxyCopy(gShareSettings.value);
+}
+
+function InitCommonGlobals()
+{
+ gInstantApply = document.documentElement.instantApply;
+ gHTTP = document.getElementById("network.proxy.http");
+ gHTTPPort = document.getElementById("network.proxy.http_port");
+ gSSL = document.getElementById("network.proxy.ssl");
+ gSSLPort = document.getElementById("network.proxy.ssl_port");
+ gFTP = document.getElementById("network.proxy.ftp");
+ gFTPPort = document.getElementById("network.proxy.ftp_port");
+ gShareSettings = document.getElementById("network.proxy.share_proxy_settings");
+}
+
+// Returns true if all protocol specific proxies and all their
+// ports are set to the same value, false otherwise.
+function DefaultForShareSettingsPref()
+{
+ return gHTTP.value == gSSL.value &&
+ gHTTP.value == gFTP.value &&
+ gHTTPPort.value == gSSLPort.value &&
+ gHTTPPort.value == gFTPPort.value;
+}
+
+function DoEnabling()
+{
+ // convenience arrays
+ var manual = ["networkProxyHTTP", "networkProxyHTTP_Port",
+ "networkProxyNone", "advancedButton"];
+ var auto = ["networkProxyAutoconfigURL", "autoReload"];
+
+ switch (gProxyType.value)
+ {
+ case kNoProxy:
+ case kAutoDiscoverProxy:
+ case kSystemProxy:
+ Disable(manual);
+ Disable(auto);
+ break;
+ case kManualProxy:
+ Disable(auto);
+ if (!gProxyType.locked)
+ EnableUnlockedElements(manual, true);
+ break;
+ case kAutoConfigProxy:
+ default:
+ Disable(manual);
+ if (!gProxyType.locked)
+ {
+ EnableElementById("networkProxyAutoconfigURL", true, false);
+ EnableUnlockedButton(gAutoURL);
+ }
+ break;
+ }
+}
+
+function Disable(aElementIds)
+{
+ for (var i = 0; i < aElementIds.length; i++)
+ document.getElementById(aElementIds[i]).setAttribute("disabled", "true");
+}
+
+function EnableUnlockedElements(aElementIds, aEnable)
+{
+ for (var i = 0; i < aElementIds.length; i++)
+ EnableElementById(aElementIds[i], aEnable, false);
+}
+
+function EnableUnlockedButton(aElement)
+{
+ var enable = gInstantApply ||
+ (aElement.valueFromPreferences == aElement.value);
+ EnableElementById("autoReload", enable, false);
+}
+
+function ReloadPAC() {
+ // This reloads the PAC URL stored in preferences.
+ // When not in instant apply mode, the button that calls this gets
+ // disabled if the preference and what is showing in the UI differ.
+ Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService().reloadPAC();
+}
+
+function FixProxyURL(aURL)
+{
+ try
+ {
+ aURL.value =
+ Services.uriFixup.createFixupURI(aURL.value,
+ Ci.nsIURIFixup.FIXUP_FLAG_NONE).spec;
+ }
+ catch (e) {}
+
+ if (!gInstantApply)
+ EnableUnlockedButton(aURL);
+}
+
+function OpenAdvancedDialog()
+{
+ document.documentElement.openSubDialog("chrome://communicator/content/pref/pref-proxies-advanced.xul",
+ "AdvancedProxyPreferences", null);
+}
+
+function DoProxyCopy(aChecked)
+{
+ DoProxyHostCopy(gHTTP.value);
+ DoProxyPortCopy(gHTTPPort.value);
+ var nonshare = ["networkProxySSL", "networkProxySSL_Port",
+ "networkProxyFTP", "networkProxyFTP_Port"];
+ EnableUnlockedElements(nonshare, !aChecked);
+}
+
+function DoProxyHostCopy(aValue)
+{
+ if (!gShareSettings.value)
+ return;
+
+ gSSL.value = aValue;
+ gFTP.value = aValue;
+}
+
+function DoProxyPortCopy(aValue)
+{
+ if (!gShareSettings.value)
+ return;
+
+ gSSLPort.value = aValue;
+ gFTPPort.value = aValue;
+}
+
+function UpdateProxies()
+{
+ var noProxiesPref = document.getElementById("network.proxy.no_proxies_on");
+
+ noProxiesPref.value = noProxiesPref.value.replace(/[;, \n]+/g, ", ")
+ .replace(/^, |, $/g, "");
+}
diff --git a/comm/suite/components/pref/content/pref-proxies.xul b/comm/suite/components/pref/content/pref-proxies.xul
new file mode 100644
index 0000000000..acd1a1f053
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-proxies.xul
@@ -0,0 +1,156 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-proxies.dtd">
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="proxies_pane"
+ label="&pref.proxies.title;"
+ script="chrome://communicator/content/pref/pref-proxies.js">
+ <preferences id="proxies_preferences">
+ <preference id="network.proxy.type"
+ name="network.proxy.type"
+ type="int"
+ onchange="DoEnabling();"/>
+ <preference id="network.proxy.autoconfig_url"
+ name="network.proxy.autoconfig_url"
+ type="string"/>
+ <preference id="pref.advanced.proxies.disable_button.reload"
+ name="pref.advanced.proxies.disable_button.reload"
+ type="bool"/>
+ <preference id="network.proxy.http"
+ name="network.proxy.http"
+ type="string"
+ onchange="DoProxyHostCopy(this.value);"/>
+ <preference id="network.proxy.http_port"
+ name="network.proxy.http_port"
+ type="int"
+ onchange="DoProxyPortCopy(this.value);"/>
+ <preference id="pref.advanced.proxies.disable_button.advanced"
+ name="pref.advanced.proxies.disable_button.advanced"
+ type="bool"/>
+ <preference id="network.proxy.no_proxies_on"
+ name="network.proxy.no_proxies_on"
+ type="string"/>
+ <preference id="network.proxy.ssl"
+ name="network.proxy.ssl"
+ type="string"/>
+ <preference id="network.proxy.ssl_port"
+ name="network.proxy.ssl_port"
+ type="int"/>
+ <preference id="network.proxy.ftp"
+ name="network.proxy.ftp"
+ type="string"/>
+ <preference id="network.proxy.ftp_port"
+ name="network.proxy.ftp_port"
+ type="int"/>
+ <preference id="network.proxy.share_proxy_settings"
+ name="network.proxy.share_proxy_settings"
+ type="bool"/>
+ </preferences>
+
+ <description>&pref.proxies.desc;</description>
+ <groupbox>
+ <caption label="&proxyTitle.label;"/>
+ <radiogroup id="networkProxyType"
+ preference="network.proxy.type"
+ align="stretch">
+ <vbox align="start">
+ <radio value="0"
+ label="&directTypeRadio.label;"
+ accesskey="&directTypeRadio.accesskey;"/>
+ <radio value="4"
+ label="&wpadTypeRadio.label;"
+ accesskey="&wpadTypeRadio.accesskey;"/>
+ <radio value="5"
+ label="&systemTypeRadio.label;"
+ accesskey="&systemTypeRadio.accesskey;"
+ id="systemPref"
+ hidden="true"/>
+ <radio value="2"
+ label="&autoTypeRadio.label;"
+ accesskey="&autoTypeRadio.accesskey;"/>
+ </vbox>
+
+ <hbox class="indent" align="center">
+ <textbox id="networkProxyAutoconfigURL"
+ flex="1"
+ class="uri-element"
+ onchange="FixProxyURL(this);"
+ preference="network.proxy.autoconfig_url"/>
+ <button id="autoReload"
+ label="&reload.label;"
+ accesskey="&reload.accesskey;"
+ oncommand="ReloadPAC();"
+ preference="pref.advanced.proxies.disable_button.reload"/>
+ </hbox>
+
+ <vbox align="start">
+ <radio value="1"
+ label="&manualTypeRadio.label;"
+ accesskey="&manualTypeRadio.accesskey;"/>
+ </vbox>
+
+ <grid class="indent">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label value="&http.label;"
+ accesskey="&http.accesskey;"
+ control="networkProxyHTTP"/>
+ </hbox>
+ <textbox id="networkProxyHTTP"
+ preference="network.proxy.http"
+ class="uri-element"/>
+ </row>
+
+ <row>
+ <hbox align="center" pack="end">
+ <label value="&port.label;"
+ accesskey="&HTTPPort.accesskey;"
+ control="networkProxyHTTP_Port"/>
+ </hbox>
+ <hbox align="center">
+ <textbox id="networkProxyHTTP_Port"
+ preference="network.proxy.http_port"
+ type="number"
+ max="65535"
+ size="5"/>
+ <spacer flex="1"/>
+ <button id="advancedButton"
+ label="&advanced.label;"
+ accesskey="&advanced.accesskey;"
+ align="end"
+ oncommand="OpenAdvancedDialog();"
+ preference="pref.advanced.proxies.disable_button.advanced"/>
+ </hbox>
+ </row>
+
+ <row align="baseline">
+ <hbox align="center" pack="end">
+ <label value="&noproxy.label;"
+ accesskey="&noproxy.accesskey;"
+ control="networkProxyNone"/>
+ </hbox>
+ <textbox id="networkProxyNone"
+ multiline="true"
+ preference="network.proxy.no_proxies_on"
+ class="uri-element"
+ onchange="UpdateProxies();"/>
+ </row>
+ <row>
+ <spacer/>
+ <description control="networkProxyNone">&noproxyExplain.label;
+ </description>
+ </row>
+ </rows>
+ </grid>
+ </radiogroup>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-scripts.js b/comm/suite/components/pref/content/pref-scripts.js
new file mode 100644
index 0000000000..eb16b4d62f
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-scripts.js
@@ -0,0 +1,29 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function setDisableState(id, state) {
+ var component = document.getElementById(id);
+ var preference = component.getAttribute("preference");
+ var isLocked = document.getElementById(preference).locked;
+ component.disabled = isLocked || state;
+}
+
+function changeDisabledState(state) {
+ //Set the states of the groupbox children state based on the "javascript enabled" checkbox value
+ setDisableState("allowWindowMoveResize", state);
+ setDisableState("allowWindowStatusChange", state);
+ setDisableState("allowWindowFlip", state);
+ setDisableState("allowHideStatusBar", state);
+ setDisableState("allowContextmenuDisable", state);
+}
+
+function javascriptEnabledChange() {
+ var javascriptDisabled = !document.getElementById('javascript.enabled').value;
+ changeDisabledState(javascriptDisabled);
+}
+
+function Startup() {
+ javascriptEnabledChange();
+}
diff --git a/comm/suite/components/pref/content/pref-scripts.xul b/comm/suite/components/pref/content/pref-scripts.xul
new file mode 100644
index 0000000000..9018f29cc4
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-scripts.xul
@@ -0,0 +1,92 @@
+<?xml version="1.0"?><!-- -*- Mode: HTML -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-scripts.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="scripts_pane"
+ label="&pref.scripts2.title;"
+ script="chrome://communicator/content/pref/pref-scripts.js">
+
+ <preferences id="scripts_preferences">
+ <preference id="javascript.enabled"
+ name="javascript.enabled"
+ type="bool"
+ onchange="javascriptEnabledChange();"/>
+ <preference id="dom.disable_window_move_resize"
+ name="dom.disable_window_move_resize"
+ type="bool" inverted="true"/>
+ <preference id="dom.disable_window_flip"
+ name="dom.disable_window_flip"
+ type="bool" inverted="true"/>
+ <preference id="dom.disable_window_open_feature.status"
+ name="dom.disable_window_open_feature.status"
+ type="bool" inverted="true"/>
+ <preference id="dom.disable_window_status_change"
+ name="dom.disable_window_status_change"
+ type="bool" inverted="true"/>
+ <preference id="dom.event.contextmenu.enabled"
+ name="dom.event.contextmenu.enabled"
+ type="bool"/>
+ <preference id="browser.dom.window.dump.enabled"
+ name="browser.dom.window.dump.enabled"
+ type="bool"/>
+ <preference id="javascript.options.strict"
+ name="javascript.options.strict"
+ type="bool"/>
+ <preference id="javascript.options.showInConsole"
+ name="javascript.options.showInConsole"
+ type="bool"/>
+ </preferences>
+
+ <groupbox id="javascriptPreferences" flex="1">
+ <caption label="&enableJavaScript.label;"/>
+
+ <checkbox id="javascriptAllowNavigator"
+ label="&navigator.label;"
+ accesskey="&navigator.accesskey;"
+ preference="javascript.enabled"/>
+
+ <label control="AllowList"
+ class="indent"
+ value="&allowScripts.label;"
+ accesskey="&allowScripts.accesskey;"/>
+
+ <listbox id="AllowList" class="indent" flex="1">
+ <listitem type="checkbox" id="allowWindowMoveResize"
+ label="&allowWindowMoveResize.label;"
+ preference="dom.disable_window_move_resize"/>
+ <listitem type="checkbox" id="allowWindowFlip"
+ label="&allowWindowFlip.label;"
+ preference="dom.disable_window_flip"/>
+ <listitem type="checkbox" id="allowHideStatusBar"
+ label="&allowHideStatusBar.label;"
+ preference="dom.disable_window_open_feature.status"/>
+ <listitem type="checkbox" id="allowWindowStatusChange"
+ label="&allowWindowStatusChange.label;"
+ preference="dom.disable_window_status_change"/>
+ <listitem type="checkbox" id="allowContextmenuDisable"
+ label="&allowContextmenuDisable.label;"
+ preference="dom.event.contextmenu.enabled"/>
+ </listbox>
+ </groupbox>
+
+ <groupbox id="debugging">
+ <caption label="&debugging.label;"/>
+ <checkbox id="browserDOMWindowDumpEnabled"
+ label="&debugEnableDump.label;"
+ accesskey="&debugEnableDump.accesskey;"
+ preference="browser.dom.window.dump.enabled"/>
+ <checkbox id="javascriptOptionsStrict"
+ label="&debugStrictJavascript.label;"
+ accesskey="&debugStrictJavascript.accesskey;"
+ preference="javascript.options.strict"/>
+ <checkbox id="javascriptOptionsShowInConsole"
+ label="&debugConsoleJavascript.label;"
+ accesskey="&debugConsoleJavascript.accesskey;"
+ preference="javascript.options.showInConsole"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-search.js b/comm/suite/components/pref/content/pref-search.js
new file mode 100755
index 0000000000..8f17af63d2
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-search.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function Startup() {
+ MakeList();
+ SearchObserver.init();
+}
+
+var SearchObserver = {
+ init: function searchEngineListObserver_init() {
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ window.addEventListener("unload", this);
+ },
+
+ observe: function searchEngineListObj_observe(aEngine, aTopic, aVerb) {
+ if (aTopic != "browser-search-engine-modified")
+ return;
+ MakeList();
+ },
+
+ handleEvent: function searchEngineListEvent(aEvent) {
+ if (aEvent.type == "unload") {
+ window.removeEventListener("unload", this);
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ }
+ }
+};
+
+function MakeList() {
+ var menulist = document.getElementById("engineList");
+ var currentEngineName = Services.search.currentEngine.name;
+
+ // Make sure the popup is empty.
+ menulist.removeAllItems();
+
+ var engines = Services.search.getVisibleEngines();
+ for (let engine of engines) {
+ let name = engine.name;
+ let menuitem = menulist.appendItem(name, name);
+ menuitem.setAttribute("class", "menuitem-iconic");
+ if (engine.iconURI)
+ menuitem.setAttribute("image", engine.iconURI.spec);
+ menuitem.engine = engine;
+ if (engine.name == currentEngineName) {
+ // Set selection to the current default engine.
+ menulist.selectedItem = menuitem;
+ }
+ }
+ // If the current engine isn't in the list any more, select the first item.
+ if (menulist.selectedIndex < 0)
+ menulist.selectedIndex = 0;
+}
+
+function UpdateDefaultEngine(selectedItem) {
+ Services.search.currentEngine = selectedItem.engine;
+ Services.obs.notifyObservers(null, "browser-search-engine-modified", "engine-current");
+}
diff --git a/comm/suite/components/pref/content/pref-search.xul b/comm/suite/components/pref/content/pref-search.xul
new file mode 100755
index 0000000000..e3eaa61701
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-search.xul
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-search.dtd">
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="search_pane"
+ label="&pref.search.title;"
+ script="chrome://communicator/content/pref/pref-search.js">
+
+ <preferences id="search_preferences">
+ <preference id="browser.search.openintab"
+ name="browser.search.openintab"
+ type="bool"/>
+ <preference id="browser.search.opentabforcontextsearch"
+ name="browser.search.opentabforcontextsearch"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&legendHeader;"/>
+
+ <hbox align="center">
+ <label value="&defaultSearchEngine.label;"
+ accesskey="&defaultSearchEngine.accesskey;"
+ control="engineList"/>
+ <menulist id="engineList"
+ oncommand="UpdateDefaultEngine(this.selectedItem)"/>
+ </hbox>
+ <hbox pack="end">
+ <button id="managerButton"
+ label="&engineManager.label;"
+ oncommand="OpenSearchEngineManager();"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&searchResults.label;"/>
+ <checkbox id="openSearchTab"
+ label="&openInTab.label;"
+ accesskey="&openInTab.accesskey;"
+ preference="browser.search.openintab"/>
+ <checkbox id="openContextSearchTab"
+ label="&openContextSearchTab.label;"
+ accesskey="&openContextSearchTab.accesskey;"
+ preference="browser.search.opentabforcontextsearch"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-security.js b/comm/suite/components/pref/content/pref-security.js
new file mode 100644
index 0000000000..31dba56b7a
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-security.js
@@ -0,0 +1,15 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ var prefTrackProtect = document.getElementById("privacy.trackingprotection.enabled");
+ SetWarnTrackEnabled(prefTrackProtect.value);
+}
+
+function SetWarnTrackEnabled(aEnable)
+{
+ EnableElementById("warnTrackContent", aEnable, false);
+}
diff --git a/comm/suite/components/pref/content/pref-security.xul b/comm/suite/components/pref/content/pref-security.xul
new file mode 100644
index 0000000000..6823df7f9e
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-security.xul
@@ -0,0 +1,108 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefSecurityDTD SYSTEM "chrome://communicator/locale/pref/pref-security.dtd">
+%prefSecurityDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="security_pane" label="&pref.security.title;"
+ script="chrome://communicator/content/pref/pref-security.js">
+ <preferences id="security_preferences">
+ <!-- User Tracking -->
+ <preference id="privacy.donottrackheader.enabled"
+ name="privacy.donottrackheader.enabled"
+ type="bool"/>
+ <preference id="privacy.trackingprotection.enabled"
+ name="privacy.trackingprotection.enabled"
+ type="bool"
+ onchange="SetWarnTrackEnabled(this.value);"/>
+ <preference id="privacy.warn_tracking_content"
+ name="privacy.warn_tracking_content"
+ type="bool"/>
+
+ <!-- Location Aware Browsing -->
+ <preference id="geo.enabled"
+ name="geo.enabled"
+ type="bool"/>
+
+ <!-- Safe Browsing -->
+ <preference id="browser.safebrowsing.malware.enabled"
+ name="browser.safebrowsing.malware.enabled"
+ type="bool"/>
+ <preference id="browser.safebrowsing.phishing.enabled"
+ name="browser.safebrowsing.phishing.enabled"
+ type="bool"/>
+
+ <preference id="accessibility.blockautorefresh"
+ name="accessibility.blockautorefresh"
+ type="bool"/>
+ </preferences>
+
+ <!-- User Tracking -->
+ <groupbox id="trackingGroup">
+ <caption label="&tracking.label;"/>
+
+ <description>&trackingIntro.label;</description>
+ <checkbox id="doNotTrack"
+ label="&doNotTrack.label;"
+ accesskey="&doNotTrack.accesskey;"
+ preference="privacy.donottrackheader.enabled"/>
+ <checkbox id="trackProtect"
+ label="&trackProtect.label;"
+ accesskey="&trackProtect.accesskey;"
+ preference="privacy.trackingprotection.enabled"/>
+ <checkbox id="warnTrackContent"
+ class="indent"
+ label="&warnTrackContent.label;"
+ accesskey="&warnTrackContent.accesskey;"
+ preference="privacy.warn_tracking_content"/>
+ </groupbox>
+
+ <!-- Location Aware Browsing -->
+ <groupbox id="geoLocationGroup">
+ <caption label="&geoLocation.label;"/>
+
+ <description>&geoIntro.label;</description>
+ <radiogroup id="geoSelection"
+ preference="geo.enabled">
+ <radio id="geoEnabled"
+ value="true"
+ label="&geoEnabled.label;"
+ accesskey="&geoEnabled.accesskey;"/>
+ <radio id="geoDisabled"
+ value="false"
+ label="&geoDisabled.label;"
+ accesskey="&geoDisabled.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <!-- Safe Browsing -->
+ <groupbox id="safeBrowsingGroup">
+ <caption label="&safeBrowsing.label;"/>
+
+ <description>&safeBrowsingIntro.label;</description>
+ <checkbox id="blockAttackSites"
+ label="&blockAttackSites.label;"
+ accesskey="&blockAttackSites.accesskey;"
+ preference="browser.safebrowsing.malware.enabled"/>
+ <checkbox id="blockWebForgeries"
+ label="&blockWebForgeries.label;"
+ accesskey="&blockWebForgeries.accesskey;"
+ preference="browser.safebrowsing.phishing.enabled"/>
+ </groupbox>
+
+ <vbox class="box-padded" align="start">
+ <checkbox id="blockAutoRefresh"
+ label="&blockAutoRefresh.label;"
+ accesskey="&blockAutoRefresh.accesskey;"
+ preference="accessibility.blockautorefresh"/>
+ </vbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-smartupdate.js b/comm/suite/components/pref/content/pref-smartupdate.js
new file mode 100644
index 0000000000..8e9712a936
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-smartupdate.js
@@ -0,0 +1,87 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gCanCheckForUpdates;
+
+function Startup()
+{
+ var hasUpdater = "nsIApplicationUpdateService" in Ci;
+
+ if (hasUpdater)
+ {
+ var aus = Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService);
+ gCanCheckForUpdates = aus.canCheckForUpdates;
+
+ UpdateAddonsItems();
+ UpdateAppItems();
+ }
+ else
+ {
+ var appGroupBox = document.getElementById("appUpdatesGroupBox");
+ appGroupBox.hidden = true;
+ }
+}
+
+/*
+ * Preferences:
+ *
+ * app.update.enabled
+ * - boolean:
+ * - true if updates to the application are enabled, false otherwise
+ * extensions.update.enabled
+ * - boolean:
+ * - true if updates to extensions and themes are enabled, false otherwise
+ * app.update.auto
+ * - true if updates should be automatically downloaded and installed,
+ * false if the user should be asked what he wants to do when an
+ * update is available
+ */
+function UpdateAddonsItems()
+{
+ var addOnsCheck = !document.getElementById("xpinstall.enabled").value;
+
+ document.getElementById("addOnsUpdatesEnabled").disabled =
+ addOnsCheck ||
+ document.getElementById("extensions.update.enabled").locked;
+
+ document.getElementById("addOnsUpdateFrequency").disabled =
+ !document.getElementById("xpinstall.enabled").value ||
+ !document.getElementById("extensions.update.enabled").value ||
+ document.getElementById("extensions.update.interval").locked;
+
+ document.getElementById("allowedSitesLink").disabled =
+ addOnsCheck;
+
+ document.getElementById("addOnsModeAutoEnabled").disabled =
+ addOnsCheck ||
+ !document.getElementById("extensions.update.enabled").value ||
+ document.getElementById("extensions.update.enabled").locked;
+}
+
+function UpdateAppItems()
+{
+ var enabledPref = document.getElementById("app.update.enabled");
+
+ document.getElementById("appUpdatesEnabled").disabled =
+ !gCanCheckForUpdates || enabledPref.locked;
+
+ document.getElementById("appUpdateFrequency").disabled =
+ !enabledPref.value || !gCanCheckForUpdates ||
+ document.getElementById("app.update.interval").locked;
+
+ document.getElementById("appModeAutoEnabled").disabled =
+ !enabledPref.value || !gCanCheckForUpdates;
+}
+
+/**
+ * Displays the history of installed updates.
+ */
+function ShowUpdateHistory()
+{
+ Cc["@mozilla.org/updates/update-prompt;1"]
+ .createInstance(Ci.nsIUpdatePrompt)
+ .showUpdateHistory(window);
+}
diff --git a/comm/suite/components/pref/content/pref-smartupdate.xul b/comm/suite/components/pref/content/pref-smartupdate.xul
new file mode 100644
index 0000000000..a9b9546c31
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-smartupdate.xul
@@ -0,0 +1,139 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefSmartUpdateDTD SYSTEM "chrome://communicator/locale/pref/pref-smartupdate.dtd">
+%prefSmartUpdateDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="smartupdate_pane"
+ label="&pref.smartUpdate.title;"
+ script="chrome://communicator/content/pref/pref-smartupdate.js">
+
+ <preferences id="smartupdate_preferences">
+ <preference id="xpinstall.enabled"
+ name="xpinstall.enabled"
+ type="bool"
+ onchange="UpdateAddonsItems();"/>
+ <preference id="extensions.update.enabled"
+ name="extensions.update.enabled"
+ type="bool"
+ onchange="UpdateAddonsItems();"/>
+ <preference id="extensions.update.interval"
+ name="extensions.update.interval"
+ type="int"/>
+ <preference id="extensions.update.autoUpdateDefault"
+ name="extensions.update.autoUpdateDefault"
+ type="bool"/>
+ <preference id="extensions.getAddons.cache.enabled"
+ name="extensions.getAddons.cache.enabled"
+ type="bool"/>
+ <preference id="app.update.enabled"
+ name="app.update.enabled"
+ type="bool"
+ onchange="UpdateAppItems();"/>
+ <preference id="app.update.auto"
+ name="app.update.auto"
+ type="bool"
+ onchange="UpdateAppItems();"/>
+ <preference id="app.update.interval"
+ name="app.update.interval"
+ type="int"/>
+ <preference id="app.update.disable_button.showUpdateHistory"
+ name="app.update.disable_button.showUpdateHistory"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&addOnsTitle.label;"/>
+ <hbox align="center">
+ <checkbox id="XPInstallEnabled"
+ label="&addOnsAllow.label;"
+ flex="1"
+ accesskey="&addOnsAllow.accesskey;"
+ preference="xpinstall.enabled"/>
+ <label id="allowedSitesLink"
+ class="text-link"
+ value="&allowedSitesLink.label;"
+ onclick="toDataManager('|permissions');"/>
+ </hbox>
+ <hbox class="indent">
+ <checkbox id="addOnsUpdatesEnabled"
+ label="&autoAddOnsUpdates.label;"
+ accesskey="&autoAddOnsUpdates.accesskey;"
+ preference="extensions.update.enabled"/>
+ <radiogroup id="addOnsUpdateFrequency"
+ orient="horizontal"
+ preference="extensions.update.interval">
+ <radio id="addOnsFreqDaily"
+ label="&daily.label;"
+ accesskey="&addOnsDaily.accesskey;"
+ value="86400"/>
+ <radio id="addOnsFreqWeekly"
+ label="&weekly.label;"
+ accesskey="&addOnsWeekly.accesskey;"
+ value="604800"/>
+ </radiogroup>
+ </hbox>
+ <hbox class="indent">
+ <checkbox id="addOnsModeAutoEnabled"
+ class="indent"
+ label="&addOnsModeAutomatic.label;"
+ flex="1"
+ accesskey="&addOnsModeAutomatic.accesskey;"
+ preference="extensions.update.autoUpdateDefault"/>
+ </hbox>
+ <hbox align="center">
+ <checkbox id="enablePersonalized"
+ flex="1"
+ label="&enablePersonalized.label;"
+ accesskey="&enablePersonalized.accesskey;"
+ preference="extensions.getAddons.cache.enabled"/>
+ <label id="addonManagerLink"
+ class="text-link"
+ onclick="toEM('addons://list/extension');"
+ value="&addonManagerLink.label;"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox id="appUpdatesGroupBox">
+ <caption label="&appUpdates.caption;"/>
+ <hbox>
+ <checkbox id="appUpdatesEnabled"
+ label="&autoAppUpdates.label;"
+ accesskey="&autoAppUpdates.accesskey;"
+ preference="app.update.enabled"/>
+ <radiogroup id="appUpdateFrequency"
+ orient="horizontal"
+ preference="app.update.interval">
+ <radio id="appFreqDaily"
+ label="&daily.label;"
+ accesskey="&appDaily.accesskey;"
+ value="86400"/>
+ <radio id="appFreqWeekly"
+ label="&weekly.label;"
+ accesskey="&appWeekly.accesskey;"
+ value="604800"/>
+ </radiogroup>
+ </hbox>
+ <checkbox id="appModeAutoEnabled"
+ class="indent"
+ label="&appModeAutomatic.label;"
+ flex="1"
+ accesskey="&appModeAutomatic.accesskey;"
+ preference="app.update.auto"/>
+ <hbox pack="end">
+ <button id="showUpdateHistory"
+ label="&updateHistoryButton.label;"
+ accesskey="&updateHistoryButton.accesskey;"
+ preference="app.update.disable_button.showUpdateHistory"
+ oncommand="ShowUpdateHistory();"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-spelling.js b/comm/suite/components/pref/content/pref-spelling.js
new file mode 100644
index 0000000000..6c214af3fc
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-spelling.js
@@ -0,0 +1,119 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gDictCount = 0;
+var gLastSelectedLang;
+
+function Startup() {
+ if ("@mozilla.org/spellchecker;1" in Cc)
+ InitLanguageMenu();
+ else
+ {
+ document.getElementById("generalSpelling").hidden = true;
+ document.getElementById("mailSpelling").hidden = true;
+ document.getElementById("noSpellCheckLabel").hidden = false;
+ }
+}
+
+function InitLanguageMenu() {
+ var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Get the list of dictionaries from the spellchecker.
+ var dictList = spellChecker.getDictionaryList();
+ var count = dictList.length;
+
+ // If dictionary count hasn't changed then no need to update the menu.
+ if (gDictCount == count)
+ return;
+
+ // Store current dictionary count.
+ gDictCount = count;
+
+ // Load the string bundles that will help us map
+ // RFC 1766 strings to UI strings.
+
+ // Load the language string bundle.
+ var languageBundle = document.getElementById("languageNamesBundle");
+ var regionBundle = null;
+ // If we have a language string bundle, load the region string bundle.
+ if (languageBundle)
+ regionBundle = document.getElementById("regionNamesBundle");
+
+ var menuStr2;
+ var isoStrArray;
+ var langId;
+ var langLabel;
+
+ for (let i = 0; i < count; i++) {
+ try {
+ langId = dictList[i];
+ isoStrArray = dictList[i].split(/[-_]/);
+
+ if (languageBundle && isoStrArray[0])
+ langLabel = languageBundle.getString(isoStrArray[0].toLowerCase());
+
+ if (regionBundle && langLabel && isoStrArray.length > 1 && isoStrArray[1]) {
+ menuStr2 = regionBundle.getString(isoStrArray[1].toLowerCase());
+ if (menuStr2)
+ langLabel += "/" + menuStr2;
+ }
+
+ if (langLabel && isoStrArray.length > 2 && isoStrArray[2])
+ langLabel += " (" + isoStrArray[2] + ")";
+
+ if (!langLabel)
+ langLabel = langId;
+ } catch (ex) {
+ // getString throws an exception when a key is not found in the
+ // bundle. In that case, just use the original dictList string.
+ langLabel = langId;
+ }
+ dictList[i] = [langLabel, langId];
+ }
+
+ // sort by locale-aware collation
+ dictList.sort(
+ function compareFn(a, b) {
+ return a[0].localeCompare(b[0]);
+ }
+ );
+
+ var languageMenuList = document.getElementById("languageMenuList");
+ // Remove any languages from the list.
+ var languageMenuPopup = languageMenuList.menupopup;
+ while (languageMenuPopup.firstChild.localName != "menuseparator")
+ languageMenuPopup.firstChild.remove();
+
+ var curLang = languageMenuList.value;
+ var defaultItem = null;
+
+ for (let i = 0; i < count; i++) {
+ let item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
+ item.setAttribute("label", dictList[i][0]);
+ item.setAttribute("value", dictList[i][1]);
+ let beforeItem = gDialog.LanguageMenulist.getItemAtIndex(i);
+ languageMenuPopup.insertBefore(item, beforeItem);
+
+ if (curLang && dictList[i][1] == curLang)
+ defaultItem = item;
+ }
+
+ // Now make sure the correct item in the menu list is selected.
+ if (defaultItem) {
+ languageMenuList.selectedItem = defaultItem;
+ gLastSelectedLang = defaultItem;
+ }
+}
+
+function SelectLanguage(aTarget) {
+ if (aTarget.value != "more-cmd")
+ gLastSelectedLang = aTarget;
+ else {
+ openDictionaryList();
+ if (gLastSelectedLang)
+ document.getElementById("languageMenuList").selectedItem = gLastSelectedLang;
+ }
+}
diff --git a/comm/suite/components/pref/content/pref-spelling.xul b/comm/suite/components/pref/content/pref-spelling.xul
new file mode 100644
index 0000000000..93fa605fb5
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-spelling.xul
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-spelling.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="spelling_pane"
+ label="&prefSpelling.title;"
+ script="chrome://communicator/content/pref/pref-spelling.js">
+
+ <preferences id="spelling_preferences">
+ <preference id="mail.SpellCheckBeforeSend"
+ name="mail.SpellCheckBeforeSend"
+ type="bool"/>
+ <preference id="mail.spellcheck.inline"
+ name="mail.spellcheck.inline"
+ type="bool"/>
+ <preference id="spellchecker.dictionary"
+ name="spellchecker.dictionary"
+ type="string"
+ onchange="SelectLanguage(event.target)"/>
+ <preference id="layout.spellcheckDefault"
+ name="layout.spellcheckDefault"
+ type="int"/>
+ </preferences>
+
+ <label id="noSpellCheckLabel"
+ value="&noSpellCheckAvailable.label;"
+ hidden="true"/>
+
+ <groupbox id="generalSpelling" align="start">
+ <caption label="&generalSpelling.label;"/>
+ <hbox align="center" pack="start">
+ <label value="&languagePopup.label;"
+ accesskey="&languagePopup.accessKey;"
+ control="languageMenuList"/>
+ <menulist id="languageMenuList"
+ preference="spellchecker.dictionary">
+ <menupopup onpopupshowing="InitLanguageMenu();">
+ <!-- dynamic content populated by JS -->
+ <menuseparator/>
+ <menuitem value="more-cmd" label="&moreDictionaries.label;"/>
+ </menupopup>
+ </menulist>
+ <spring flex="1"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="center">
+ <label value="&checkSpellingWhenTyping.label;"
+ accesskey="&checkSpellingWhenTyping.accesskey;"
+ control="spellcheckDefault"/>
+ <menulist id="spellcheckDefault"
+ preference="layout.spellcheckDefault">
+ <menupopup>
+ <menuitem value="0" label="&dontCheckSpelling.label;"/>
+ <menuitem value="1" label="&multilineCheckSpelling.label;"/>
+ <menuitem value="2" label="&alwaysCheckSpelling.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ <groupbox id="mailSpelling" align="start">
+ <caption label="&spellForMailAndNews.label;"/>
+ <vbox align="start">
+ <checkbox id="spellCheckBeforeSend"
+ label="&checkSpellingBeforeSend.label;"
+ accesskey="&checkSpellingBeforeSend.accesskey;"
+ preference="mail.SpellCheckBeforeSend"/>
+ <checkbox id="inlineSpellCheck"
+ label="&spellCheckInline.label;"
+ accesskey="&spellCheckInline.accesskey;"
+ preference="mail.spellcheck.inline"/>
+ </vbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-sync.js b/comm/suite/components/pref/content/pref-sync.js
new file mode 100644
index 0000000000..06d1825a70
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-sync.js
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+
+const PAGE_NO_ACCOUNT = 0;
+const PAGE_HAS_ACCOUNT = 1;
+const PAGE_NEEDS_UPDATE = 2;
+
+var gSyncPane = {
+ get page() {
+ return document.getElementById("weavePrefsDeck").selectedIndex;
+ },
+
+ set page(val) {
+ document.getElementById("weavePrefsDeck").selectedIndex = val;
+ },
+
+ get _usingCustomServer() {
+ return Weave.Svc.Prefs.isSet("serverURL");
+ },
+
+ needsUpdate: function () {
+ this.page = PAGE_NEEDS_UPDATE;
+ let label = document.getElementById("loginError");
+ label.value = Weave.Utils.getErrorString(Weave.Status.login);
+ label.className = "error";
+ },
+
+ topics: [ "weave:service:ready",
+ "weave:service:login:error",
+ "weave:service:login:finish",
+ "weave:service:start-over",
+ "weave:service:setup-complete",
+ "weave:service:logout:finish"],
+
+ init: function () {
+ for (var topic of this.topics)
+ Services.obs.addObserver(this, topic);
+
+ window.addEventListener("unload", this);
+
+ var xps = Cc["@mozilla.org/weave/service;1"]
+ .getService().wrappedJSObject;
+ if (xps.ready)
+ this.observe(null, "weave:service:ready", null);
+ else
+ xps.ensureLoaded();
+ },
+
+ handleEvent: function (aEvent) {
+ window.removeEventListener("unload", this);
+
+ for (var topic of this.topics)
+ Services.obs.removeObserver(this, topic);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
+ Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
+ this.page = PAGE_NO_ACCOUNT;
+ } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
+ Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
+ this.needsUpdate();
+ } else {
+ this.page = PAGE_HAS_ACCOUNT;
+ document.getElementById("accountName").value = Weave.Service.identity.account;
+ document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
+ document.getElementById("tosPP").hidden = this._usingCustomServer;
+ }
+ },
+
+ startOver: function (showDialog) {
+ if (showDialog) {
+ let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+ let prefutilitiesBundle = document.getElementById("bundle_prefutilities");
+ let buttonChoice =
+ Services.prompt.confirmEx(window,
+ prefutilitiesBundle.getString("syncUnlink.title"),
+ prefutilitiesBundle.getString("syncUnlink.label"),
+ flags,
+ prefutilitiesBundle.getString("syncUnlinkConfirm.label"),
+ null, null, null, {});
+
+ // If the user selects cancel, just bail
+ if (buttonChoice == 1)
+ return;
+ }
+
+ Weave.Service.startOver();
+ this.updateWeavePrefs();
+ },
+
+ updatePass: function () {
+ if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
+ gSyncUtils.changePassword();
+ else
+ gSyncUtils.updatePassphrase();
+ },
+
+ resetPass: function () {
+ if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
+ gSyncUtils.resetPassword();
+ else
+ gSyncUtils.resetPassphrase();
+ },
+
+ openSetup: function (resetSync) {
+ var win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
+ if (win)
+ win.focus();
+ else {
+ window.openDialog("chrome://communicator/content/sync/syncSetup.xul",
+ "weaveSetup", "centerscreen,chrome,resizable=no", resetSync);
+ }
+ },
+
+ openQuotaDialog: function () {
+ let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
+ if (win)
+ win.focus();
+ else
+ window.openDialog("chrome://communicator/content/sync/syncQuota.xul", "",
+ "centerscreen,chrome,dialog,modal");
+ },
+
+ openAddDevice: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return;
+ let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
+ if (win)
+ win.focus();
+ else
+ window.openDialog("chrome://communicator/content/sync/syncAddDevice.xul",
+ "syncAddDevice", "centerscreen,chrome,resizable=no");
+ },
+
+ resetSync: function () {
+ this.openSetup(true);
+ }
+};
diff --git a/comm/suite/components/pref/content/pref-sync.xul b/comm/suite/components/pref/content/pref-sync.xul
new file mode 100644
index 0000000000..4163c0de70
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-sync.xul
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+<!ENTITY % syncDTD SYSTEM "chrome://communicator/locale/pref/pref-sync.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncDTD;
+]>
+
+<overlay id="SyncPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <prefpane id="sync_pane"
+ helpTopic="sync_prefs"
+ onpaneload="gSyncPane.init();">
+
+ <preferences>
+ <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/>
+ <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/>
+ <preference id="engine.history" name="services.sync.engine.history" type="bool"/>
+ <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
+ <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/>
+ <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/>
+ </preferences>
+
+ <script src="chrome://communicator/content/pref/pref-sync.js"/>
+ <script src="chrome://communicator/content/sync/syncUtils.js"/>
+
+ <deck id="weavePrefsDeck">
+ <vbox id="noAccount" align="center">
+ <spacer flex="1"/>
+ <description id="syncDesc" flex="1">
+ &weaveDesc.label;
+ </description>
+ <button id="setupButton"
+ label="&setupButton.label;"
+ accesskey="&setupButton.accesskey;"
+ oncommand="gSyncPane.openSetup();"/>
+ <separator/>
+ <spacer flex="3"/>
+ </vbox>
+ <vbox id="hasAccount">
+ <groupbox>
+ <caption label="&accountGroupboxCaption.label;"/>
+ <hbox align="center">
+ <label value="&accountName.label;" control="accountName"/>
+ <textbox id="accountName" flex="1" readonly="true"/>
+ <button type="menu"
+ label="&manageAccount.label;"
+ accesskey="&manageAccount.accesskey;">
+ <menupopup>
+ <menuitem label="&viewQuota.label;"
+ accesskey="&viewQuota.accesskey;"
+ oncommand="gSyncPane.openQuotaDialog();"/>
+ <menuseparator/>
+ <menuitem label="&changePassword.label;"
+ accesskey="&changePassword.accesskey;"
+ oncommand="gSyncUtils.changePassword();"/>
+ <menuitem label="&myRecoveryKey.label;"
+ accesskey="&myRecoveryKey.accesskey;"
+ oncommand="gSyncUtils.resetPassphrase();"/>
+ <menuseparator/>
+ <menuitem label="&resetSync.label;"
+ accesskey="&resetSync.accesskey;"
+ oncommand="gSyncPane.resetSync();"/>
+ <menuitem label="&unlinkDevice.label;"
+ accesskey="&unlinkDevice.accesskey;"
+ oncommand="gSyncPane.startOver(true);"/>
+ <menuseparator/>
+ <menuitem label="&addDevice.label;"
+ accesskey="&addDevice.accesskey;"
+ oncommand="gSyncPane.openAddDevice();"/>
+ </menupopup>
+ </button>
+ </hbox>
+ <vbox>
+ <label value="&syncMy2.label;"/>
+ <listbox id="syncEnginesList" flex="1">
+ <listitem type="checkbox"
+ label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ preference="engine.addons"/>
+ <listitem type="checkbox"
+ label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ preference="engine.bookmarks"/>
+ <listitem type="checkbox"
+ label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ preference="engine.history"/>
+ <listitem type="checkbox"
+ label="&engine.passwords.label;"
+ accesskey="&engine.passwords.accesskey;"
+ preference="engine.passwords"/>
+ <listitem type="checkbox"
+ label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+ preference="engine.prefs"/>
+ <listitem type="checkbox"
+ label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ preference="engine.tabs"/>
+ </listbox>
+ </vbox>
+ </groupbox>
+ <groupbox>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&syncComputerName.label;"
+ accesskey="&syncComputerName.accesskey;"
+ control="syncComputerName"/>
+ <textbox id="syncComputerName"
+ onchange="gSyncUtils.changeName(this);"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+ <hbox id="tosPP" pack="center">
+ <label class="text-link"
+ onclick="event.stopPropagation(); gSyncUtils.openToS();"
+ value="&prefs.tosLink.label;"/>
+ <label class="text-link"
+ onclick="event.stopPropagation(); gSyncUtils.openPrivacyPolicy();"
+ value="&prefs.ppLink.label;"/>
+ </hbox>
+ </vbox>
+ <vbox id="needsUpdate" align="center" pack="center">
+ <hbox>
+ <label id="loginError" value=""/>
+ <button label="&updatePass.label;"
+ accesskey="&updatePass.accesskey;"
+ oncommand="gSyncPane.updatePass(); return false;"
+ id="updatePassButton"/>
+ <button label="&resetPass.label;"
+ accesskey="&resetPass.accesskey;"
+ oncommand="gSyncPane.resetPass(); return false;"
+ id="resetPassButton"/>
+ </hbox>
+ <button label="&unlinkDevice.label;"
+ accesskey="&unlinkDevice.accesskey;"
+ oncommand="gSyncPane.startOver(true); return false;"
+ id="unlinkDeviceButton"/>
+ </vbox>
+ </deck>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/pref-tabs.xul b/comm/suite/components/pref/content/pref-tabs.xul
new file mode 100644
index 0000000000..24361fba85
--- /dev/null
+++ b/comm/suite/components/pref/content/pref-tabs.xul
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/pref/pref-tabs.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="tabs_pane"
+ label="&tabHeader.label;">
+
+ <preferences id="tabs_preferences">
+ <preference id="browser.tabs.autoHide"
+ name="browser.tabs.autoHide"
+ type="bool"/>
+ <preference id="browser.tabs.loadInBackground"
+ name="browser.tabs.loadInBackground"
+ type="bool"
+ inverted="true"/>
+ <preference id="browser.tabs.loadDivertedInBackground"
+ name="browser.tabs.loadDivertedInBackground"
+ type="bool"
+ inverted="true"/>
+ <preference id="browser.tabs.avoidBrowserFocus"
+ name="browser.tabs.avoidBrowserFocus"
+ type="bool"
+ inverted="true"/>
+ <preference id="browser.tabs.warnOnClose"
+ name="browser.tabs.warnOnClose"
+ type="bool"/>
+ <preference id="browser.tabs.insertRelatedAfterCurrent"
+ name="browser.tabs.insertRelatedAfterCurrent"
+ type="bool"/>
+ <preference id="browser.tabs.opentabfor.middleclick"
+ name="browser.tabs.opentabfor.middleclick"
+ type="bool"/>
+ <preference id="browser.tabs.opentabfor.urlbar"
+ name="browser.tabs.opentabfor.urlbar"
+ type="bool"/>
+ <preference id="suite.manager.dataman.openAsDialog"
+ name="suite.manager.dataman.openAsDialog"
+ inverted="true"
+ type="bool"/>
+ <preference id="suite.manager.addons.openAsDialog"
+ name="suite.manager.addons.openAsDialog"
+ inverted="true"
+ type="bool"/>
+ </preferences>
+
+ <groupbox id="generalTabPreferences" align="start">
+ <caption label="&tabDisplay.label;"/>
+ <checkbox id="tabStrip"
+ label="&autoHide.label;"
+ accesskey="&autoHide.accesskey2;"
+ preference="browser.tabs.autoHide"/>
+ <checkbox id="tabBackground"
+ label="&background.label;"
+ accesskey="&background.accesskey;"
+ preference="browser.tabs.loadInBackground"/>
+ <checkbox id="tabDivertedBackground"
+ label="&diverted.label;"
+ accesskey="&diverted.accesskey;"
+ preference="browser.tabs.loadDivertedInBackground"/>
+ <checkbox id="tabAvoidBrowserFocus"
+ label="&browserFocus.label;"
+ accesskey="&browserFocus.accesskey;"
+ preference="browser.tabs.avoidBrowserFocus"/>
+ <checkbox id="tabWarnOnClose"
+ label="&warnOnClose.label;"
+ accesskey="&warnOnClose.accesskey;"
+ preference="browser.tabs.warnOnClose"/>
+ <checkbox id="tabRelatedAfterCurrent"
+ label="&relatedAfterCurrent.label;"
+ accesskey="&relatedAfterCurrent.accesskey;"
+ preference="browser.tabs.insertRelatedAfterCurrent"/>
+ </groupbox>
+
+ <groupbox id="useTabPreferences" align="start">
+ <caption label="&openTabs.label;"/>
+ <checkbox id="middleClick"
+#ifndef XP_MACOSX
+ label="&middleClick.label;"
+ accesskey="&middleClick.accesskey;"
+#else
+ label="&middleClickMac.label;"
+ accesskey="&middleClickMac.accesskey;"
+#endif
+ preference="browser.tabs.opentabfor.middleclick"/>
+ <checkbox id="urlBar"
+#ifndef XP_MACOSX
+ label="&urlbar.label;"
+ accesskey="&urlbar.accesskey;"
+#else
+ label="&urlbarMac.label;"
+ accesskey="&urlbarMac.accesskey;"
+#endif
+ preference="browser.tabs.opentabfor.urlbar"/>
+ </groupbox>
+
+ <groupbox id="useManagersPreferences" align="start">
+ <caption label="&openManagers.label;"/>
+ <checkbox id="openDataManager"
+ label="&openDataManager.label;"
+ accesskey="&openDataManager.accesskey;"
+ preference="suite.manager.dataman.openAsDialog"/>
+ <checkbox id="openAddOnsManager"
+ label="&openAddOnsManager.label;"
+ accesskey="&openAddOnsManager.accesskey;"
+ preference="suite.manager.addons.openAsDialog"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/pref/content/preferences.js b/comm/suite/components/pref/content/preferences.js
new file mode 100644
index 0000000000..091a3904fc
--- /dev/null
+++ b/comm/suite/components/pref/content/preferences.js
@@ -0,0 +1,99 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The content of this file is loaded into the scope of the
+// prefwindow and will be available to all prefpanes!
+
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+function OnLoad()
+{
+ // Make sure that the preferences window fits the screen.
+ let dialog = document.documentElement;
+ let curHeight = dialog.scrollHeight;
+ let curWidth = dialog.scrollWidth;
+
+ // Leave some space for desktop toolbar and window decoration.
+ let maxHeight = window.screen.availHeight - 48;
+ let maxWidth = window.screen.availWidth - 24;
+
+ // Trigger overflow situation within 40px for bug 868495 expansions.
+ let setHeight = curHeight > maxHeight - 40 ? maxHeight : curHeight;
+ let setWidth = curWidth > maxWidth ? maxWidth : curWidth;
+
+ if (setHeight == curHeight && setWidth == curWidth)
+ dialog.setAttribute("overflow", "visible");
+
+ window.innerHeight = setHeight;
+ window.innerWidth = setWidth;
+}
+
+function EnableElementById(aElementId, aEnable, aFocus)
+{
+ EnableElement(document.getElementById(aElementId), aEnable, aFocus);
+}
+
+function EnableElement(aElement, aEnable, aFocus)
+{
+ let pref = document.getElementById(aElement.getAttribute("preference"));
+ let enabled = aEnable && !pref.locked;
+
+ aElement.disabled = !enabled;
+
+ if (enabled && aFocus)
+ aElement.focus();
+}
+
+function WriteSoundField(aField, aValue)
+{
+ var file = GetFileFromString(aValue);
+ if (file)
+ {
+ aField.file = file;
+ aField.label = (AppConstants.platform == "macosx") ? file.leafName : file.path;
+ }
+}
+
+function SelectSound(aSoundUrlPref)
+{
+ var soundUrlPref = aSoundUrlPref;
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ var prefutilitiesBundle = document.getElementById("bundle_prefutilities");
+ fp.init(window, prefutilitiesBundle.getString("choosesound"),
+ Ci.nsIFilePicker.modeOpen);
+
+ let file = GetFileFromString(soundUrlPref.value);
+ if (file && file.parent && file.parent.exists())
+ fp.displayDirectory = file.parent;
+
+ let filterExts = "*.wav; *.wave";
+ // On Mac, allow AIFF and CAF files too.
+ if (AppConstants.platform == "macosx") {
+ filterExts += "; *.aif; *.aiff; *.caf";
+ }
+ fp.appendFilter(prefutilitiesBundle.getString("SoundFiles"), filterExts);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK && fp.fileURL.spec &&
+ fp.fileURL.spec.length > 0) {
+ soundUrlPref.value = fp.fileURL.spec;
+ }
+ });
+}
+
+function PlaySound(aValue, aMail)
+{
+ const nsISound = Ci.nsISound;
+ var sound = Cc["@mozilla.org/sound;1"]
+ .createInstance(nsISound);
+
+ if (aValue)
+ sound.play(Services.io.newURI(aValue));
+ else if (aMail && (AppConstants.platform != "macosx"))
+ sound.playEventSound(nsISound.EVENT_NEW_MAIL_RECEIVED);
+ else
+ sound.beep();
+}
diff --git a/comm/suite/components/pref/content/preferences.xul b/comm/suite/components/pref/content/preferences.xul
new file mode 100644
index 0000000000..787d086af5
--- /dev/null
+++ b/comm/suite/components/pref/content/preferences.xul
@@ -0,0 +1,264 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://communicator/skin/"?>
+<?xml-stylesheet type="text/css" href="chrome://communicator/content/communicator.css"?>
+<?xml-stylesheet type="text/css" href="chrome://communicator/content/pref/prefpanels.css"?>
+<?xml-stylesheet type="text/css" href="chrome://communicator/skin/prefpanels.css"?>
+<?xml-stylesheet type="text/css" href="chrome://communicator/skin/preferences.css"?>
+
+<!DOCTYPE prefwindow SYSTEM "chrome://communicator/locale/pref/preferences.dtd">
+
+<prefwindow id="prefDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&prefWindow.title;"
+#ifndef XP_WIN
+#ifndef XP_MACOSX
+ style="&prefWindow.size;"
+#else
+ style="&prefWindowMac2.size;"
+#endif
+#else
+ style="&prefWindowWin2.size;"
+#endif
+ overflow="auto"
+ onload="OnLoad();"
+ windowtype="mozilla:preferences"
+ buttons="accept,cancel,help"
+ autopanes="true">
+
+ <script src="chrome://communicator/content/pref/preferences.js"/>
+ <!-- Used by pref-smartupdate, pref-privatedata, pref-cookies, pref-images, pref-popups and pref-passwords, as well as pref-sync (gSyncUtils.*open* -> openUILinkIn) -->
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+ <script src="chrome://communicator/content/tasksOverlay.js"/>
+
+ <stringbundleset id="prefBundleset">
+ <stringbundle id="bundle_prefutilities"
+ src="chrome://communicator/locale/pref/prefutilities.properties"/>
+ <stringbundle id="languageNamesBundle"
+ src="chrome://global/locale/languageNames.properties"/>
+ <stringbundle id="regionNamesBundle"
+ src="chrome://global/locale/regionNames.properties"/>
+ </stringbundleset>
+
+ <tree id="prefsTree"
+ style="width: 13em;"
+ seltype="single"
+ hidecolumnpicker="true"
+ hidden="true"
+ flex="1">
+ <treecols>
+ <treecol id="categoryCol"
+ label="&categoryHeader;"
+ primary="true"
+ flex="1"/>
+ </treecols>
+
+ <treechildren id="prefsPanelChildren">
+ <!-- Appearance items -->
+ <treeitem container="true"
+ id="appearanceItem"
+ label="&appear.label;"
+ prefpane="appearance_pane"
+ helpTopic="appearance_pref"
+ url="chrome://communicator/content/pref/pref-appearance.xul">
+ <treechildren id="appearanceChildren">
+ <treeitem id="contentItem"
+ label="&content.label;"
+ prefpane="content_pane"
+ helpTopic="appearance_pref_content"
+ url="chrome://communicator/content/pref/pref-content.xul"/>
+ <treeitem id="fontsItem"
+ label="&fonts.label;"
+ prefpane="fonts_pane"
+ helpTopic="appearance_pref_fonts"
+ url="chrome://communicator/content/pref/pref-fonts.xul"/>
+ <treeitem id="colorsItem"
+ label="&colors.label;"
+ prefpane="colors_pane"
+ helpTopic="appearance_pref_colors"
+ url="chrome://communicator/content/pref/pref-colors.xul"/>
+ <treeitem id="mediaItem"
+ label="&media.label;"
+ prefpane="media_pane"
+ helpTopic="appearance_pref_media"
+ url="chrome://communicator/content/pref/pref-media.xul"/>
+ <treeitem id="spellingItem"
+ label="&spellingPane.label;"
+ prefpane="spelling_pane"
+ helpTopic="appearance_pref_spelling"
+ url="chrome://communicator/content/pref/pref-spelling.xul"/>
+ </treechildren>
+ </treeitem>
+
+ <!-- Browser items -->
+ <treeitem container="true"
+ id="navigatorItem"
+ label="&navigator.label;"
+ prefpane="navigator_pane"
+ helpTopic="navigator_pref_navigator"
+ url="chrome://communicator/content/pref/pref-navigator.xul">
+ <treechildren id="navigatorChildren">
+ <treeitem id="historyItem"
+ label="&history.label;"
+ prefpane="history_pane"
+ helpTopic="navigator_pref_history"
+ url="chrome://communicator/content/pref/pref-history.xul"/>
+ <treeitem id="languagesItem"
+ label="&languages.label;"
+ prefpane="languages_pane"
+ helpTopic="navigator_pref_languages"
+ url="chrome://communicator/content/pref/pref-languages.xul"/>
+ <treeitem id="applicationsItem"
+ label="&applications.label;"
+ prefpane="applications_pane"
+ helpTopic="navigator_pref_helper_applications"
+ url="chrome://communicator/content/pref/pref-applications.xul"/>
+ <treeitem id="locationBarItem"
+ label="&locationBar.label;"
+ prefpane="locationBar_pane"
+ helpTopic="navigator_pref_location_bar"
+ url="chrome://communicator/content/pref/pref-locationbar.xul"/>
+ <treeitem id="searchItem"
+ label="&search.label;"
+ prefpane="search_pane"
+ helpTopic="navigator_pref_internet_searching"
+ url="chrome://communicator/content/pref/pref-search.xul"/>
+ <treeitem id="tabsItem"
+ label="&tabWindows.label;"
+ prefpane="tabs_pane"
+ helpTopic="navigator_pref_tabbed_browsing"
+ url="chrome://communicator/content/pref/pref-tabs.xul"/>
+ <treeitem id="linksItem"
+ label="&links.label;"
+ prefpane="links_pane"
+ helpTopic="navigator_pref_link_behavior"
+ url="chrome://communicator/content/pref/pref-links.xul"/>
+ <treeitem id="downloadItem"
+ label="&download.label;"
+ prefpane="download_pane"
+ helpTopic="navigator_pref_downloads"
+ url="chrome://communicator/content/pref/pref-download.xul"/>
+ </treechildren>
+ </treeitem>
+
+ <!-- Privacy & Security items -->
+ <treeitem container="true"
+ id="securityItem"
+ prefpane="security_pane"
+ label="&security.label;"
+ helpTopic="sec_gen"
+ url="chrome://communicator/content/pref/pref-security.xul">
+ <treechildren id="securityChildren">
+ <treeitem id="privatedataItem"
+ label="&privatedata.label;"
+ prefpane="privatedata_pane"
+ helpTopic="privatedata_prefs"
+ url="chrome://communicator/content/pref/pref-privatedata.xul"/>
+ <treeitem id="cookiesItem"
+ label="&cookies.label;"
+ prefpane="cookies_pane"
+ helpTopic="cookies_prefs"
+ url="chrome://communicator/content/pref/pref-cookies.xul"/>
+ <treeitem id="imagesItem"
+ label="&images.label;"
+ prefpane="images_pane"
+ helpTopic="images_prefs"
+ url="chrome://communicator/content/pref/pref-images.xul"/>
+ <treeitem id="popupsItem"
+ label="&popups.label;"
+ prefpane="popups_pane"
+ helpTopic="pop_up_blocking_prefs"
+ url="chrome://communicator/content/pref/pref-popups.xul"/>
+ <treeitem id="passwordsItem"
+ label="&passwords.label;"
+ prefpane="passwords_pane"
+ url="chrome://pippki/content/pref-passwords.xul"
+ helpTopic="passwords_prefs"/>
+ <treeitem id="sslItem"
+ label="&ssltls.label;"
+ prefpane="ssl_pane"
+ url="chrome://pippki/content/pref-ssl.xul"
+ helpTopic="ssl_prefs"/>
+ <treeitem id="certItem"
+ label="&certs.label;"
+ prefpane="certs_pane"
+ url="chrome://pippki/content/pref-certs.xul"
+ helpTopic="certs_prefs"/>
+ </treechildren>
+ </treeitem>
+
+ <!-- Sync
+ <treeitem id="syncItem"
+ label="&sync.label;"
+ prefpane="sync_pane"
+ url="chrome://communicator/content/pref/pref-sync.xul"
+ helpTopic="sync_prefs"/> -->
+
+ <!-- Advanced items -->
+ <treeitem container="true"
+ id="advancedItem"
+ label="&advance.label;"
+ prefpane="advanced_pane"
+ helpTopic="advanced_pref_advanced"
+ url="chrome://communicator/content/pref/pref-advanced.xul">
+ <treechildren id="advancedChildren">
+ <treeitem id="scriptsItem"
+ label="&scriptsAndWindows2.label;"
+ prefpane="scripts_pane"
+ helpTopic="advanced_pref_scripts"
+ url="chrome://communicator/content/pref/pref-scripts.xul"/>
+ <treeitem id="keynavItem"
+ label="&keynav.label;"
+ prefpane="keynav_pane"
+ helpTopic="advanced_pref_keyboard_nav"
+ url="chrome://communicator/content/pref/pref-keynav.xul"/>
+ <treeitem id="findasyoutypeItem"
+ label="&findAsYouType.label;"
+ prefpane="findasyoutype_pane"
+ helpTopic="advanced_pref_find_as_you_type"
+ url="chrome://communicator/content/pref/pref-findasyoutype.xul"/>
+ <treeitem id="cacheItem"
+ label="&cache.label;"
+ prefpane="cache_pane"
+ helpTopic="advanced_pref_cache"
+ url="chrome://communicator/content/pref/pref-cache.xul"/>
+ <treeitem id="offlineAppsItem"
+ label="&offlineApps.label;"
+ prefpane="offlineapps_pane"
+ helpTopic="advanced_pref_offlineapps"
+ url="chrome://communicator/content/pref/pref-offlineapps.xul"/>
+ <treeitem id="proxiesItem"
+ label="&proxies.label;"
+ prefpane="proxies_pane"
+ helpTopic="advanced_pref_proxies"
+ url="chrome://communicator/content/pref/pref-proxies.xul"/>
+ <treeitem id="httpItem"
+ label="&httpnetworking.label;"
+ prefpane="http_pane"
+ helpTopic="advanced_http_networking"
+ url="chrome://communicator/content/pref/pref-http.xul"/>
+ <treeitem id="smartupdateItem"
+ label="&smart.label;"
+ prefpane="smartupdate_pane"
+ helpTopic="advanced_pref_installation"
+ url="chrome://communicator/content/pref/pref-smartupdate.xul"/>
+ <treeitem id="mousewheelItem"
+ label="&mousewheel.label;"
+ prefpane="mousewheel_pane"
+ helpTopic="advanced_pref_mouse_wheel"
+ url="chrome://communicator/content/pref/pref-mousewheel.xul"/>
+ <treeitem id="debuggingItem"
+ label="&debugging.label;"
+ prefpane="debugging_pane"
+ helpTopic="advanced_pref_debugging"
+ url="chrome://communicator/content/pref/pref-debugging.xul"/>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </tree>
+
+</prefwindow>
diff --git a/comm/suite/components/pref/content/prefpanels.css b/comm/suite/components/pref/content/prefpanels.css
new file mode 100755
index 0000000000..c38509d590
--- /dev/null
+++ b/comm/suite/components/pref/content/prefpanels.css
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#handlersView > listitem {
+ -moz-binding: url("chrome://communicator/content/pref/prefpanels.xml#handler");
+}
+
+.listcell-iconic.handler-action[selected="true"] {
+ -moz-binding: url("chrome://communicator/content/pref/prefpanels.xml#handler-action-selected");
+}
+
+#offlineAppsList > listitem {
+ -moz-binding: url("chrome://communicator/content/pref/prefpanels.xml#offlineapp");
+}
+
+/*
+ * Font dialog menulist fixes
+ */
+#defaultFontType,
+#serif,
+#sans-serif,
+#monospace {
+ min-width: 30ch;
+}
+
+/*
+ * Calendar Event/Tasks menulist fix
+ */
+
+#defaults-itemtype-menulist {
+ min-width: 20ch;
+}
diff --git a/comm/suite/components/pref/content/prefpanels.xml b/comm/suite/components/pref/content/prefpanels.xml
new file mode 100644
index 0000000000..347eac9755
--- /dev/null
+++ b/comm/suite/components/pref/content/prefpanels.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % applicationsDTD SYSTEM "chrome://communicator/locale/pref/pref-applications.dtd">
+ %applicationsDTD;
+]>
+
+<bindings id="handlerBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="handler" extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <implementation>
+ <constructor>
+ this.doCommand();
+ </constructor>
+ <property name="type" readonly="true">
+ <getter>
+ return this.getAttribute("type");
+ </getter>
+ </property>
+ </implementation>
+ <content>
+ <xul:listcell class="listcell-iconic handler-type" align="center" crop="end"
+ xbl:inherits="tooltiptext=typeDescription,label=typeDescription,image=typeIcon,typeClass"/>
+ <xul:listcell anonid="action-cell" class="listcell-iconic handler-action" align="center" crop="end"
+ xbl:inherits="tooltiptext=actionDescription,label=actionDescription,image=actionIcon,appHandlerIcon,selected"/>
+ </content>
+ </binding>
+
+ <binding id="handler-action-selected" extends="chrome://global/content/bindings/listbox.xml#listcell">
+ <content>
+ <xul:menulist anonid="action-menu" class="actionsMenu" flex="1" crop="end" selectedIndex="1">
+ <xul:menupopup/>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <constructor>
+ this.doCommand();
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="offlineapp" extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <xul:listcell xbl:inherits="label=host"/>
+ <xul:listcell xbl:inherits="label=usage"/>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/pref/jar.mn b/comm/suite/components/pref/jar.mn
new file mode 100644
index 0000000000..86f1472dc6
--- /dev/null
+++ b/comm/suite/components/pref/jar.mn
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+* content/communicator/pref/preferences.xul (content/preferences.xul)
+ content/communicator/pref/preferences.js (content/preferences.js)
+ content/communicator/pref/prefpanels.css (content/prefpanels.css)
+ content/communicator/pref/prefpanels.xml (content/prefpanels.xml)
+ content/communicator/pref/pref-advanced.js (content/pref-advanced.js)
+ content/communicator/pref/pref-advanced.xul (content/pref-advanced.xul)
+ content/communicator/pref/pref-appearance.js (content/pref-appearance.js)
+* content/communicator/pref/pref-appearance.xul (content/pref-appearance.xul)
+* content/communicator/pref/pref-applications.xul (content/pref-applications.xul)
+ content/communicator/pref/pref-applications.js (content/pref-applications.js)
+ content/communicator/pref/pref-applicationManager.js (content/pref-applicationManager.js)
+ content/communicator/pref/pref-applicationManager.xul (content/pref-applicationManager.xul)
+ content/communicator/pref/pref-cache.js (content/pref-cache.js)
+ content/communicator/pref/pref-cache.xul (content/pref-cache.xul)
+ content/communicator/pref/pref-colors.js (content/pref-colors.js)
+ content/communicator/pref/pref-colors.xul (content/pref-colors.xul)
+ content/communicator/pref/pref-content.js (content/pref-content.js)
+ content/communicator/pref/pref-content.xul (content/pref-content.xul)
+ content/communicator/pref/pref-cookies.js (content/pref-cookies.js)
+ content/communicator/pref/pref-cookies.xul (content/pref-cookies.xul)
+ content/communicator/pref/pref-debugging.js (content/pref-debugging.js)
+ content/communicator/pref/pref-debugging.xul (content/pref-debugging.xul)
+ content/communicator/pref/pref-download.js (content/pref-download.js)
+ content/communicator/pref/pref-download.xul (content/pref-download.xul)
+ content/communicator/pref/pref-findasyoutype.js (content/pref-findasyoutype.js)
+ content/communicator/pref/pref-findasyoutype.xul (content/pref-findasyoutype.xul)
+ content/communicator/pref/pref-fonts.js (content/pref-fonts.js)
+ content/communicator/pref/pref-fonts.xul (content/pref-fonts.xul)
+ content/communicator/pref/pref-history.js (content/pref-history.js)
+ content/communicator/pref/pref-history.xul (content/pref-history.xul)
+ content/communicator/pref/pref-http.js (content/pref-http.js)
+ content/communicator/pref/pref-http.xul (content/pref-http.xul)
+ content/communicator/pref/pref-images.xul (content/pref-images.xul)
+ content/communicator/pref/pref-keynav.js (content/pref-keynav.js)
+ content/communicator/pref/pref-keynav.xul (content/pref-keynav.xul)
+ content/communicator/pref/pref-languages.js (content/pref-languages.js)
+ content/communicator/pref/pref-languages.xul (content/pref-languages.xul)
+ content/communicator/pref/pref-languages-add.xul (content/pref-languages-add.xul)
+ content/communicator/pref/pref-languages-add.js (content/pref-languages-add.js)
+ content/communicator/pref/pref-links.js (content/pref-links.js)
+ content/communicator/pref/pref-links.xul (content/pref-links.xul)
+ content/communicator/pref/pref-locationbar.js (content/pref-locationbar.js)
+ content/communicator/pref/pref-locationbar.xul (content/pref-locationbar.xul)
+* content/communicator/pref/pref-media.xul (content/pref-media.xul)
+ content/communicator/pref/pref-mousewheel.js (content/pref-mousewheel.js)
+* content/communicator/pref/pref-mousewheel.xul (content/pref-mousewheel.xul)
+ content/communicator/pref/pref-navigator.js (content/pref-navigator.js)
+ content/communicator/pref/pref-navigator.xul (content/pref-navigator.xul)
+ content/communicator/pref/pref-offlineapps.js (content/pref-offlineapps.js)
+ content/communicator/pref/pref-offlineapps.xul (content/pref-offlineapps.xul)
+ content/communicator/pref/pref-popups.js (content/pref-popups.js)
+ content/communicator/pref/pref-popups.xul (content/pref-popups.xul)
+ content/communicator/pref/pref-privatedata.js (content/pref-privatedata.js)
+ content/communicator/pref/pref-privatedata.xul (content/pref-privatedata.xul)
+ content/communicator/pref/pref-proxies.js (content/pref-proxies.js)
+ content/communicator/pref/pref-proxies.xul (content/pref-proxies.xul)
+ content/communicator/pref/pref-proxies-advanced.xul (content/pref-proxies-advanced.xul)
+ content/communicator/pref/pref-scripts.js (content/pref-scripts.js)
+ content/communicator/pref/pref-scripts.xul (content/pref-scripts.xul)
+ content/communicator/pref/pref-search.js (content/pref-search.js)
+ content/communicator/pref/pref-search.xul (content/pref-search.xul)
+ content/communicator/pref/pref-security.js (content/pref-security.js)
+ content/communicator/pref/pref-security.xul (content/pref-security.xul)
+ content/communicator/pref/pref-smartupdate.js (content/pref-smartupdate.js)
+ content/communicator/pref/pref-smartupdate.xul (content/pref-smartupdate.xul)
+ content/communicator/pref/pref-spelling.js (content/pref-spelling.js)
+ content/communicator/pref/pref-spelling.xul (content/pref-spelling.xul)
+* content/communicator/pref/pref-tabs.xul (content/pref-tabs.xul)
diff --git a/comm/suite/components/pref/moz.build b/comm/suite/components/pref/moz.build
new file mode 100644
index 0000000000..7c2a1e68e6
--- /dev/null
+++ b/comm/suite/components/pref/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser/browser.ini",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/pref/tests/browser/browser.ini b/comm/suite/components/pref/tests/browser/browser.ini
new file mode 100644
index 0000000000..04cb66aaaa
--- /dev/null
+++ b/comm/suite/components/pref/tests/browser/browser.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[browser_bug410900.js]
diff --git a/comm/suite/components/pref/tests/browser/browser_bug410900.js b/comm/suite/components/pref/tests/browser/browser_bug410900.js
new file mode 100644
index 0000000000..97275dd6b2
--- /dev/null
+++ b/comm/suite/components/pref/tests/browser/browser_bug410900.js
@@ -0,0 +1,55 @@
+function test() {
+ waitForExplicitFinish();
+
+ // Setup a phony handler to ensure the app pane will be populated.
+ var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]
+ .createInstance(Ci.nsIWebHandlerApp);
+ handler.name = "App pane alive test";
+ handler.uriTemplate = "http://test.mozilla.org/%s";
+
+ var extps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ var info = extps.getProtocolHandlerInfo("apppanetest");
+ info.possibleApplicationHandlers.appendElement(handler);
+
+ var hserv = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ hserv.store(info);
+
+ var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+
+ function observer(win, topic, data) {
+ if (topic != "app-handler-pane-loaded")
+ return;
+
+ obs.removeObserver(observer, "app-handler-pane-loaded");
+ runTest(win);
+ }
+ obs.addObserver(observer, "app-handler-pane-loaded");
+
+ openDialog("chrome://communicator/content/pref/preferences.xul",
+ "PrefWindow", "chrome,titlebar,dialog=no,resizable",
+ "applications_pane");
+}
+
+function runTest(win) {
+ var sel = win.document.documentElement.getAttribute("lastSelected");
+ ok(sel == "applications_pane", "Specified pane was opened");
+
+ var rbox = win.document.getElementById("handlersView");
+ ok(rbox, "handlersView is present");
+
+ var items = rbox && rbox.getElementsByTagName("listitem");
+ ok(items && items.length > 0, "App handler list populated");
+
+ var handlerAdded = false;
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "apppanetest")
+ handlerAdded = true;
+ }
+ ok(handlerAdded, "apppanetest protocol handler was succesfully added");
+
+ win.close();
+ finish();
+}
diff --git a/comm/suite/components/profile/content/profileSelection.js b/comm/suite/components/profile/content/profileSelection.js
new file mode 100644
index 0000000000..6400d73359
--- /dev/null
+++ b/comm/suite/components/profile/content/profileSelection.js
@@ -0,0 +1,344 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var gProfileBundle;
+var gBrandBundle;
+var gProfileService;
+var gProfileManagerMode = "selection";
+var gDialogParams = window.arguments[0]
+ .QueryInterface(Ci.nsIDialogParamBlock);
+
+function StartUp()
+{
+ gProfileBundle = document.getElementById("bundle_profile");
+ gBrandBundle = document.getElementById("bundle_brand");
+ if (gDialogParams.objects) {
+ document.documentElement.getButton("accept").setAttribute("label",
+ document.documentElement.getAttribute("buttonlabelstart"));
+ document.documentElement.getButton("cancel").setAttribute("label",
+ document.documentElement.getAttribute("buttonlabelexit"));
+ document.getElementById('intro').textContent =
+ document.getElementById('intro').getAttribute("start");
+ document.getElementById('offlineState').hidden = false;
+ gDialogParams.SetInt(0, 0);
+ }
+
+ gProfileService = Cc["@mozilla.org/toolkit/profile-service;1"]
+ .getService(Ci.nsIToolkitProfileService);
+ var profileEnum = gProfileService.profiles;
+ var selectedProfile = null;
+ try {
+ selectedProfile = gProfileService.selectedProfile;
+ }
+ catch (ex) {
+ }
+ while (profileEnum.hasMoreElements()) {
+ AddItem(profileEnum.getNext().QueryInterface(Ci.nsIToolkitProfile),
+ selectedProfile);
+ }
+
+ var autoSelect = document.getElementById("autoSelect");
+ if (Services.prefs.getBoolPref("profile.manage_only_at_launch"))
+ autoSelect.hidden = true;
+ else
+ autoSelect.checked = gProfileService.startWithLastProfile;
+
+ DoEnabling();
+}
+
+// function : <profileSelection.js>::AddItem();
+// purpose : utility function for adding items to a tree.
+function AddItem(aProfile, aProfileToSelect)
+{
+ var tree = document.getElementById("profiles");
+ var treeitem = document.createElement("treeitem");
+ var treerow = document.createElement("treerow");
+ var treecell = document.createElement("treecell");
+ var treetip = document.getElementById("treetip");
+ var profileDir = gProfileService.getProfileByName(aProfile.name).rootDir;
+
+ treecell.setAttribute("label", aProfile.name);
+ treerow.appendChild(treecell);
+ treeitem.appendChild(treerow);
+ treeitem.setAttribute("tooltip", profileDir.path);
+ treetip.setAttribute("value", profileDir.path);
+ tree.lastChild.appendChild(treeitem);
+ treeitem.profile = aProfile;
+ if (aProfile == aProfileToSelect) {
+ var profileIndex = tree.view.getIndexOfItem(treeitem);
+ tree.view.selection.select(profileIndex);
+ tree.treeBoxObject.ensureRowIsVisible(profileIndex);
+ }
+}
+
+// function : <profileSelection.js>::AcceptDialog();
+// purpose : sets the current profile to the selected profile (user choice: "Start Mozilla")
+function AcceptDialog()
+{
+ var autoSelect = document.getElementById("autoSelect");
+ if (!autoSelect.hidden) {
+ gProfileService.startWithLastProfile = autoSelect.checked;
+ gProfileService.flush();
+ }
+
+ var profileTree = document.getElementById("profiles");
+ var selected = profileTree.view.getItemAtIndex(profileTree.currentIndex);
+
+ if (!gDialogParams.objects) {
+ var profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ var profLD = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+
+ if (selected.profile.rootDir.equals(profD) &&
+ selected.profile.localDir.equals(profLD))
+ return true;
+ }
+
+ try {
+ var profileLock = selected.profile.lock({});
+ gProfileService.selectedProfile = selected.profile;
+ gProfileService.defaultProfile = selected.profile;
+ gProfileService.flush();
+ if (gDialogParams.objects) {
+ gDialogParams.objects.insertElementAt(profileLock, 0);
+ gProfileService.startOffline = document.getElementById("offlineState").checked;
+ gDialogParams.SetInt(0, 1);
+ gDialogParams.SetString(0, selected.profile.name);
+ return true;
+ }
+ profileLock.unlock();
+ } catch (e) {
+ var brandName = gBrandBundle.getString("brandShortName");
+ var message = gProfileBundle.getFormattedString("dirLocked",
+ [brandName, selected.profile.name]);
+ Services.prompt.alert(window, null, message);
+ return false;
+ }
+
+ // Although switching profile works by performing a restart internally,
+ // the user is quitting the old profile, so make it look like a quit.
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ if (cancelQuit.data) {
+ return false;
+ }
+
+ try {
+ var env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ env.set("XRE_PROFILE_NAME", selected.profile.name);
+ env.set("XRE_PROFILE_PATH", selected.profile.rootDir.path);
+ env.set("XRE_PROFILE_LOCAL_PATH", selected.profile.localDir.path);
+ var app = Services.startup;
+ app.quit(app.eAttemptQuit | app.eRestart);
+ return true;
+ }
+ catch (e) {
+ env.set("XRE_PROFILE_NAME", "");
+ env.set("XRE_PROFILE_PATH", "");
+ env.set("XRE_PROFILE_LOCAL_PATH", "");
+ return false;
+ }
+}
+
+// invoke the createProfile Wizard
+function CreateProfileWizard()
+{
+ window.openDialog('chrome://mozapps/content/profile/createProfileWizard.xul',
+ '', 'centerscreen,chrome,modal,titlebar');
+}
+
+// update the display to show the additional profile
+function CreateProfile(aProfile)
+{
+ gProfileService.flush();
+ AddItem(aProfile, aProfile);
+}
+
+// rename the selected profile
+function RenameProfile()
+{
+ var profileTree = document.getElementById("profiles");
+ var selected = profileTree.view.getItemAtIndex(profileTree.currentIndex);
+ var profileName = selected.profile.name;
+ var newName = {value: profileName};
+ var dialogTitle = gProfileBundle.getString("renameProfileTitle");
+ var msg = gProfileBundle.getFormattedString("renameProfilePrompt", [profileName]);
+ var ps = Services.prompt;
+ if (ps.prompt(window, dialogTitle, msg, newName, null, {value: 0}) &&
+ newName.value != profileName) {
+ if (!/\S/.test(newName.value)) {
+ ps.alert(window, gProfileBundle.getString("profileNameInvalidTitle"),
+ gProfileBundle.getString("profileNameEmpty"));
+ return false;
+ }
+
+ if (/([\\*:?<>|\/\"])/.test(newName.value)) {
+ ps.alert(window, gProfileBundle.getString("profileNameInvalidTitle"),
+ gProfileBundle.getFormattedString("invalidChar", [RegExp.$1]));
+ return false;
+ }
+
+ try {
+ gProfileService.getProfileByName(newName.value);
+ ps.alert(window, gProfileBundle.getString("profileExistsTitle"),
+ gProfileBundle.getString("profileExists"));
+ return false;
+ }
+ catch (e) {
+ }
+
+ selected.profile.name = newName.value;
+ gProfileService.flush();
+ selected.firstChild.firstChild.setAttribute("label", newName.value);
+ }
+}
+
+function ConfirmDelete()
+{
+ var profileTree = document.getElementById("profiles");
+ var selected = profileTree.view.getItemAtIndex(profileTree.currentIndex);
+ if (!selected.profile.rootDir.exists()) {
+ DeleteProfile(false);
+ return;
+ }
+
+ try {
+ var profileLock = selected.profile.lock({});
+ var dialogTitle = gProfileBundle.getString("deleteTitle");
+ var dialogText;
+
+ var path = selected.profile.rootDir.path;
+ dialogText = gProfileBundle.getFormattedString("deleteProfile", [path]);
+ var ps = Services.prompt;
+ var buttonPressed = ps.confirmEx(window, dialogTitle, dialogText,
+ (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
+ (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1) +
+ (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_2),
+ gProfileBundle.getString("dontDeleteFiles"), null,
+ gProfileBundle.getString("deleteFiles"), null, {value: 0});
+ profileLock.unlock();
+ if (buttonPressed != 1)
+ DeleteProfile(buttonPressed == 2);
+ } catch (e) {
+ var dialogTitle = gProfileBundle.getString("deleteTitle");
+ var brandName = gBrandBundle.getString("brandShortName");
+ var dialogText = gProfileBundle.getFormattedString("deleteLocked",
+ [brandName, selected.profile.name]);
+ ps.alert(window, dialogTitle, dialogText);
+ }
+}
+
+// Delete the profile, with the delete flag set as per instruction above.
+function DeleteProfile(aDeleteFiles)
+{
+ var profileTree = document.getElementById("profiles");
+ var selected = profileTree.view.getItemAtIndex(profileTree.currentIndex);
+ var previous = profileTree.currentIndex && profileTree.currentIndex - 1;
+
+ try {
+ selected.profile.remove(aDeleteFiles);
+ gProfileService.flush();
+ selected.remove();
+
+ if (profileTree.view.rowCount != 0) {
+ profileTree.view.selection.select(previous);
+ profileTree.treeBoxObject.ensureRowIsVisible(previous);
+ }
+
+ // set the button state
+ DoEnabling();
+ }
+ catch (ex) {
+ dump("Exception during profile deletion.\n");
+ }
+}
+
+function SwitchProfileManagerMode()
+{
+ var captionLine;
+ var prattleIndex;
+
+ if (gProfileManagerMode == "selection") {
+ prattleIndex = 1;
+ captionLine = gProfileBundle.getString("manageTitle");
+
+ document.getElementById("profiles").focus();
+
+ // hide the manage profiles button...
+ document.documentElement.getButton("extra2").hidden = true;
+ gProfileManagerMode = "manager";
+ }
+ else {
+ prattleIndex = 0;
+ captionLine = gProfileBundle.getString("selectTitle");
+ gProfileManagerMode = "selection";
+ }
+
+ // swap deck
+ document.getElementById("prattle").selectedIndex = prattleIndex;
+
+ // change the title of the profile manager/selection window.
+ document.getElementById("header").setAttribute("description", captionLine);
+ document.title = captionLine;
+}
+
+// do button enabling based on tree selection
+function DoEnabling()
+{
+ var acceptButton = document.documentElement.getButton("accept");
+ var deleteButton = document.getElementById("deleteButton");
+ var renameButton = document.getElementById("renameButton");
+
+ var disabled = document.getElementById("profiles").view.selection.count == 0;
+ acceptButton.disabled = disabled;
+ deleteButton.disabled = disabled;
+ renameButton.disabled = disabled;
+}
+
+// handle key event on tree
+function HandleKeyEvent(aEvent)
+{
+ if (gProfileManagerMode != "manager")
+ return;
+
+ switch (aEvent.keyCode)
+ {
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ case KeyEvent.DOM_VK_DELETE:
+ if (!document.getElementById("deleteButton").disabled)
+ ConfirmDelete();
+ break;
+ case KeyEvent.DOM_VK_F2:
+ if (!document.getElementById("renameButton").disabled)
+ RenameProfile();
+ }
+}
+
+function HandleClickEvent(aEvent)
+{
+ if (aEvent.button == 0 && aEvent.target.parentNode.view.selection.count != 0 && AcceptDialog()) {
+ window.close();
+ return true;
+ }
+
+ return false;
+}
+
+function HandleToolTipEvent(aEvent)
+{
+ var treeTip = document.getElementById("treetip");
+ var tree = document.getElementById("profiles");
+
+ var cell = tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row < 0)
+ aEvent.preventDefault();
+ else
+ treeTip.label = tree.view.getItemAtIndex(cell.row).tooltip;
+}
diff --git a/comm/suite/components/profile/content/profileSelection.xul b/comm/suite/components/profile/content/profileSelection.xul
new file mode 100644
index 0000000000..dd62b4a7dd
--- /dev/null
+++ b/comm/suite/components/profile/content/profileSelection.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/profile/profile.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % profileDTD SYSTEM "chrome://communicator/locale/profile/profileSelection.dtd">
+%profileDTD;
+]>
+
+<dialog id="profileWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&windowTitle.label;"
+ windowtype="mozilla:profileSelection"
+ orient="vertical"
+ style="width: 42em;"
+ buttons="accept,cancel,extra2"
+ buttonlabelaccept="&select.label;"
+ buttonlabelstart="&start.label;"
+ buttonlabelexit="&exit.label;"
+ buttonlabelextra2="&manage.label;"
+ buttonaccesskeyextra2="&manage.accesskey;"
+ ondialogaccept="return AcceptDialog();"
+ ondialogextra2="SwitchProfileManagerMode();"
+ onload="StartUp();">
+
+ <stringbundle id="bundle_profile"
+ src="chrome://communicator/locale/profile/profileSelection.properties"/>
+ <stringbundle id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"/>
+
+ <script src="chrome://communicator/content/profile/profileSelection.js"/>
+ <script src="chrome://mozapps/content/profile/createProfileWizard.js"/>
+
+ <dialogheader id="header" title="&profileManager.title;" description="&windowTitle.label;"/>
+
+ <hbox class="wizard-box" flex="1">
+
+ <!-- instructions -->
+ <deck id="prattle">
+ <description id="intro" start="&introStart.label;">&introSwitch.label;</description>
+ <vbox>
+ <description id="label">&profileManagerText.label;</description>
+ <separator/>
+ <hbox>
+ <vbox flex="1" id="managebuttons">
+ <button id="newButton" label="&newButton.label;" accesskey="&newButton.accesskey;" oncommand="CreateProfileWizard();"/>
+ <button id="renameButton" label="&renameButton.label;" accesskey="&renameButton.accesskey;" oncommand="RenameProfile();"/>
+ <button id="deleteButton" label="&deleteButton.label;" accesskey="&deleteButton.accesskey;" oncommand="ConfirmDelete();"/>
+ </vbox>
+ <spacer flex="2"/>
+ </hbox>
+ </vbox>
+ </deck>
+
+ <separator class="thin" orient="vertical"/>
+
+ <vbox flex="1">
+ <tooltip id="treetip"
+ onpopupshowing="HandleToolTipEvent(event);">
+ </tooltip>
+ <tree id="profiles" flex="1" seltype="single"
+ hidecolumnpicker="true"
+ onselect="DoEnabling();"
+ onkeypress="HandleKeyEvent(event);">
+ <treecols>
+ <treecol label="&availableProfiles.label;" flex="1" sortLocked="true"/>
+ </treecols>
+ <treechildren tooltip="treetip"
+ ondblclick="HandleClickEvent(event);"/>
+ </tree>
+ <checkbox id="offlineState" label="&offlineState.label;" accesskey="&offlineState.accesskey;" hidden="true"/>
+ <checkbox id="autoSelect" label="&autoSelect.label;" accesskey="&autoSelect.accesskey;"/>
+ </vbox>
+ </hbox>
+
+</dialog>
diff --git a/comm/suite/components/profile/jar.mn b/comm/suite/components/profile/jar.mn
new file mode 100644
index 0000000000..bf9706ba55
--- /dev/null
+++ b/comm/suite/components/profile/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+% override chrome://mozapps/content/profile/profileSelection.xul chrome://communicator/content/profile/profileSelection.xul
+ content/communicator/profile/profileSelection.js (content/profileSelection.js)
+ content/communicator/profile/profileSelection.xul (content/profileSelection.xul)
diff --git a/comm/suite/components/profile/moz.build b/comm/suite/components/profile/moz.build
new file mode 100644
index 0000000000..0f2d51ba64
--- /dev/null
+++ b/comm/suite/components/profile/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "nsSuiteDirectoryProvider.cpp",
+]
+
+FINAL_LIBRARY = "suite"
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/profile/nsSuiteDirectoryProvider.cpp b/comm/suite/components/profile/nsSuiteDirectoryProvider.cpp
new file mode 100755
index 0000000000..9d19615625
--- /dev/null
+++ b/comm/suite/components/profile/nsSuiteDirectoryProvider.cpp
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSuiteDirectoryProvider.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCategoryManagerUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIPrefBranch.h"
+#include "nsDirectoryServiceDefs.h"
+#include "mozilla/intl/LocaleService.h"
+#include "nsIPrefService.h"
+#include "nsArrayEnumerator.h"
+#include "nsEnumeratorUtils.h"
+
+using mozilla::intl::LocaleService;
+
+NS_IMPL_ISUPPORTS(nsSuiteDirectoryProvider,
+ nsIDirectoryServiceProvider,
+ nsIDirectoryServiceProvider2)
+
+NS_IMETHODIMP
+nsSuiteDirectoryProvider::GetFile(const char *aKey,
+ bool *aPersist,
+ nsIFile* *aResult)
+{
+ // NOTE: This function can be reentrant through the NS_GetSpecialDirectory
+ // call, so be careful not to cause infinite recursion.
+ // i.e. the check for supported files must come first.
+ const char* leafName = nullptr;
+
+ if (!strcmp(aKey, NS_APP_BOOKMARKS_50_FILE))
+ leafName = "bookmarks.html";
+ else if (!strcmp(aKey, NS_APP_USER_PANELS_50_FILE))
+ leafName = "panels.rdf";
+ else
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIFile> parentDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(parentDir));
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsCOMPtr<nsIFile> file;
+ rv = parentDir->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsDependentCString leafStr(leafName);
+ file->AppendNative(leafStr);
+
+ bool exists;
+ if (NS_SUCCEEDED(file->Exists(&exists)) && !exists)
+ EnsureProfileFile(leafStr, parentDir, file);
+
+ *aPersist = true;
+ NS_IF_ADDREF(*aResult = file);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSuiteDirectoryProvider::GetFiles(const char *aKey,
+ nsISimpleEnumerator* *aResult)
+{
+ nsresult rv;
+ nsCOMPtr<nsIProperties> dirSvc(do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsCOMArray<nsIFile> baseFiles;
+ AppendDistroSearchDirs(dirSvc, baseFiles);
+
+ nsCOMPtr<nsISimpleEnumerator> baseEnum;
+ rv = NS_NewArrayEnumerator(getter_AddRefs(baseEnum), baseFiles);
+ if (NS_FAILED(rv))
+ return rv;
+
+ return NS_ERROR_FAILURE;
+}
+
+void
+nsSuiteDirectoryProvider::EnsureProfileFile(const nsACString& aLeafName,
+ nsIFile* aParentDir,
+ nsIFile* aTarget)
+{
+ nsCOMPtr<nsIFile> defaultsDir;
+
+ NS_GetSpecialDirectory(NS_APP_DEFAULTS_50_DIR,
+ getter_AddRefs(defaultsDir));
+ if (!defaultsDir)
+ return;
+
+ nsresult rv = defaultsDir->AppendNative("profile"_ns);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ defaultsDir->AppendNative(aLeafName);
+
+ defaultsDir->CopyToNative(aParentDir, aLeafName);
+}
+
+NS_IMPL_ISUPPORTS(nsSuiteDirectoryProvider::AppendingEnumerator,
+ nsISimpleEnumerator)
+
+NS_IMETHODIMP
+nsSuiteDirectoryProvider::AppendingEnumerator::HasMoreElements(bool *aResult)
+{
+ *aResult = mNext != nullptr;
+ return NS_OK;
+}
+
+void
+nsSuiteDirectoryProvider::AppendingEnumerator::GetNext()
+{
+ // Ignore all errors
+
+ bool more;
+ while (NS_SUCCEEDED(mBase->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsISupports> nextSupports;
+ mBase->GetNext(getter_AddRefs(nextSupports));
+
+ mNext = do_QueryInterface(nextSupports);
+ if (!mNext)
+ continue;
+
+ mNext->AppendNative(mLeafName);
+
+ bool exists;
+ if (NS_SUCCEEDED(mNext->Exists(&exists)) && exists)
+ return;
+ }
+
+ mNext = nullptr;
+}
+
+NS_IMETHODIMP
+nsSuiteDirectoryProvider::AppendingEnumerator::GetNext(nsISupports* *aResult)
+{
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!mNext) {
+ *aResult = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ADDREF(*aResult = mNext);
+
+ GetNext();
+
+ return NS_OK;
+}
+
+nsSuiteDirectoryProvider::AppendingEnumerator::AppendingEnumerator
+ (nsISimpleEnumerator* aBase, const char* const aLeafName) :
+ mBase(aBase), mLeafName(aLeafName)
+{
+ // Initialize mNext to begin.
+ GetNext();
+}
+
+// Appends the distribution-specific search engine directories to the
+// array. The directory structure is as follows:
+
+// appdir/
+// \- distribution/
+// \- searchplugins/
+// |- common/
+// \- locale/
+// |- <locale 1>/
+// ...
+// \- <locale N>/
+
+// common engines are loaded for all locales. If there is no locale
+// directory for the current locale, there is a pref:
+// "distribution.searchplugins.defaultLocale"
+// which specifies a default locale to use.
+
+void
+nsSuiteDirectoryProvider::AppendDistroSearchDirs(nsIProperties* aDirSvc,
+ nsCOMArray<nsIFile> &array)
+{
+ nsCOMPtr<nsIFile> searchPlugins;
+ nsresult rv = aDirSvc->Get(NS_XPCOM_CURRENT_PROCESS_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(searchPlugins));
+ if (NS_FAILED(rv))
+ return;
+ searchPlugins->AppendNative("distribution"_ns);
+ searchPlugins->AppendNative("searchplugins"_ns);
+
+ bool exists;
+ rv = searchPlugins->Exists(&exists);
+ if (NS_FAILED(rv) || !exists)
+ return;
+
+ nsCOMPtr<nsIFile> commonPlugins;
+ rv = searchPlugins->Clone(getter_AddRefs(commonPlugins));
+ if (NS_SUCCEEDED(rv)) {
+ commonPlugins->AppendNative("common"_ns);
+ rv = commonPlugins->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists)
+ array.AppendObject(commonPlugins);
+ }
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ nsCOMPtr<nsIFile> localePlugins;
+ rv = searchPlugins->Clone(getter_AddRefs(localePlugins));
+ if (NS_FAILED(rv))
+ return;
+
+ localePlugins->AppendNative("locale"_ns);
+
+ // we didn't append the locale dir - try the default one
+ nsCString defLocale;
+ rv = prefs->GetCharPref("distribution.searchplugins.defaultLocale",
+ defLocale);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> defLocalePlugins;
+ rv = localePlugins->Clone(getter_AddRefs(defLocalePlugins));
+ if (NS_SUCCEEDED(rv)) {
+ defLocalePlugins->AppendNative(defLocale);
+ rv = defLocalePlugins->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ array.AppendObject(defLocalePlugins);
+ return; // all done
+ }
+ }
+ }
+
+ // we didn't have a defaultLocale, use the user agent locale
+ nsAutoCString locale;
+ LocaleService::GetInstance()->GetAppLocaleAsLangTag(locale);
+
+ nsCOMPtr<nsIFile> curLocalePlugins;
+ rv = localePlugins->Clone(getter_AddRefs(curLocalePlugins));
+ if (NS_SUCCEEDED(rv)) {
+ curLocalePlugins->AppendNative(locale);
+ rv = curLocalePlugins->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ array.AppendObject(curLocalePlugins);
+ return; // all done
+ }
+ }
+ }
+}
diff --git a/comm/suite/components/profile/nsSuiteDirectoryProvider.h b/comm/suite/components/profile/nsSuiteDirectoryProvider.h
new file mode 100644
index 0000000000..6a06be3c30
--- /dev/null
+++ b/comm/suite/components/profile/nsSuiteDirectoryProvider.h
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SuiteDirectoryProvider_h__
+#define SuiteDirectoryProvider_h__
+
+#include "nsCOMArray.h"
+#include "nsIDirectoryService.h"
+#include "nsIFile.h"
+#include "nsISimpleEnumerator.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIProperties.h"
+#include "mozilla/Attributes.h"
+#include "nsSuiteCID.h"
+
+#define NS_APP_BOOKMARKS_50_FILE "BMarks"
+
+class nsSuiteDirectoryProvider final : public nsIDirectoryServiceProvider2
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER2
+
+private:
+ ~nsSuiteDirectoryProvider() {}
+
+ void EnsureProfileFile(const nsACString& aLeafName,
+ nsIFile* aParentDir, nsIFile* aTarget);
+
+ void AppendDistroSearchDirs(nsIProperties* aDirSvc,
+ nsCOMArray<nsIFile> &array);
+
+ void AppendFileKey(const char *key, nsIProperties* aDirSvc,
+ nsCOMArray<nsIFile> &array);
+
+ class AppendingEnumerator final : public nsISimpleEnumerator
+ {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ AppendingEnumerator(nsISimpleEnumerator* aBase,
+ const char* const aLeafName);
+
+ private:
+ ~AppendingEnumerator() {}
+ void GetNext();
+
+ nsCOMPtr<nsISimpleEnumerator> mBase;
+ nsDependentCString mLeafName;
+ nsCOMPtr<nsIFile> mNext;
+ };
+};
+
+#endif
diff --git a/comm/suite/components/sanitize/Sanitizer.jsm b/comm/suite/components/sanitize/Sanitizer.jsm
new file mode 100644
index 0000000000..1207827423
--- /dev/null
+++ b/comm/suite/components/sanitize/Sanitizer.jsm
@@ -0,0 +1,947 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["Sanitizer"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ console: "resource://gre/modules/Console.jsm",
+ Downloads: "resource://gre/modules/Downloads.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager");
+XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+ "@mozilla.org/dom/quota-manager-service;1",
+ "nsIQuotaManagerService");
+
+// Used as unique id for pending sanitizations.
+var gPendingSanitizationSerial = 0;
+
+/**
+ * A number of iterations after which to yield time back
+ * to the system.
+ */
+const YIELD_PERIOD = 10;
+
+var Sanitizer = {
+ /**
+ * Whether we should sanitize on shutdown.
+ */
+ PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
+
+ /**
+ * During a sanitization this is set to a JSON containing an array of the
+ * pending sanitizations. This allows to retry sanitizations on startup in
+ * case they dind't run or were interrupted by a crash.
+ * Use addPendingSanitization and removePendingSanitization to manage it.
+ */
+ PREF_PENDING_SANITIZATIONS: "privacy.sanitize.pending",
+
+ /**
+ * Pref branches to fetch sanitization options from.
+ */
+ PREF_CPD_BRANCH: "privacy.cpd.",
+ PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown.",
+
+ /**
+ * The fallback timestamp used when no argument is given to
+ * Sanitizer.getClearRange.
+ */
+ PREF_TIMESPAN: "privacy.sanitize.timeSpan",
+
+ /**
+ * Time span constants corresponding to values of the preference
+ * privacy.sanitize.timeSpan It is used to determine how much history
+ * to clear, for various items.
+ */
+ TIMESPAN_EVERYTHING: 0,
+ TIMESPAN_HOUR: 1,
+ TIMESPAN_2HOURS: 2,
+ TIMESPAN_4HOURS: 3,
+ TIMESPAN_TODAY: 4,
+ TIMESPAN_5MIN: 5,
+ TIMESPAN_24HOURS: 6,
+
+ /**
+ * Whether we should sanitize on shutdown.
+ * When this is set, a pending sanitization should also be added and removed
+ * when shutdown sanitization is complete. This allows to retry incomplete
+ * sanitizations on startup.
+ */
+ shouldSanitizeOnShutdown: false,
+
+ /**
+ * Shows a sanitization dialog to the user.
+ *
+ * @param [optional] parentWindow the window to use as
+ * parent for the created dialog.
+ */
+ showUI(parentWindow) {
+ let win = AppConstants.platform == "macosx" ?
+ null : // make this an app-modal window on Mac
+ parentWindow;
+ Services.ww.openWindow(win,
+ "chrome://communicator/content/sanitizeDialog.xul",
+ "Sanitize",
+ "chrome,titlebar,centerscreen,dialog,modal",
+ null);
+ },
+
+ /**
+ * Performs startup tasks:
+ * - Checks if sanitizations were not completed during the last session.
+ * - Registers sanitize-on-shutdown.
+ */
+ async onStartup() {
+ // First, collect pending sanitizations from the last session, before we
+ // add pending sanitizations for this session.
+ let pendingSanitizations = getAndClearPendingSanitizations();
+
+ // Check if we should sanitize on shutdown.
+ this.shouldSanitizeOnShutdown =
+ Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
+ Services.prefs.addObserver(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, this,
+ true);
+ // Add a pending shutdown sanitization, if necessary.
+ if (this.shouldSanitizeOnShutdown) {
+ let itemsToClear =
+ getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+ addPendingSanitization("shutdown", itemsToClear, {});
+ }
+ // Shutdown sanitization is always pending, but the user may change the
+ // sanitize on shutdown prefs during the session. Then the pending
+ // sanitization would become stale and must be updated.
+ Services.prefs.addObserver(Sanitizer.PREF_SHUTDOWN_BRANCH, this, true);
+
+ // Make sure that we are triggered during shutdown.
+ let shutdownClient = PlacesUtils.history.shutdownClient.jsclient;
+ // We need to pass to sanitize() (through sanitizeOnShutdown) a state
+ // object that tracks the status of the shutdown blocker. This 'progress'
+ // object will be updated during sanitization and reported with the crash
+ // in case of a shutdown timeout.
+ // We use the `options` argument to pass the `progress` object to
+ // sanitize().
+ let progress = { isShutdown: true };
+ shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+ () => sanitizeOnShutdown(progress),
+ {fetchState: () => ({ progress })}
+ );
+
+ // Finally, run the sanitizations that were left pending, because we
+ // crashed before completing them.
+ for (let {itemsToClear, options} of pendingSanitizations) {
+ try {
+ await this.sanitize(itemsToClear, options);
+ } catch (ex) {
+ Cu.reportError("A previously pending sanitization failed: " +
+ itemsToClear + "\n" + ex);
+ }
+ }
+ },
+
+ /**
+ * Returns a 2 element array representing the start and end times,
+ * in the uSec-since-epoch format that PRTime likes. If we should
+ * clear everything, this function returns null.
+ *
+ * @param ts [optional] a timespan to convert to start and end time.
+ * Falls back to the privacy.sanitize.timeSpan
+ * preference if this argument is omitted.
+ * If this argument is provided, it has to be one of the
+ * Sanitizer.TIMESPAN_* constants. This function will
+ * throw an error otherwise.
+ *
+ * @return {Array} a 2-element Array containing the start and end times.
+ */
+ getClearRange(ts) {
+ if (ts === undefined)
+ ts = Services.prefs.getIntPref(Sanitizer.PREF_TIMESPAN);
+ if (ts === Sanitizer.TIMESPAN_EVERYTHING)
+ return null;
+
+ // PRTime is microseconds while JS time is milliseconds
+ var endDate = Date.now() * 1000;
+ switch (ts) {
+ case Sanitizer.TIMESPAN_5MIN :
+ // 5*60*1000000
+ var startDate = endDate - 300000000;
+ break;
+ case Sanitizer.TIMESPAN_HOUR :
+ // 1*60*60*1000000
+ startDate = endDate - 3600000000;
+ break;
+ case Sanitizer.TIMESPAN_2HOURS :
+ // 2*60*60*1000000
+ startDate = endDate - 7200000000;
+ break;
+ case Sanitizer.TIMESPAN_4HOURS :
+ // 4*60*60*1000000
+ startDate = endDate - 14400000000;
+ break;
+ case Sanitizer.TIMESPAN_TODAY :
+ // Start with today
+ var d = new Date();
+ // zero us back to midnight...
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ // convert to epoch usec
+ startDate = d.valueOf() * 1000;
+ break;
+ case Sanitizer.TIMESPAN_24HOURS :
+ // 24*60*60*1000000
+ startDate = endDate - 86400000000;
+ break;
+ default:
+ throw "Invalid time span for clear private data: " + ts;
+ }
+ return [startDate, endDate];
+ },
+
+ /**
+ * Deletes privacy sensitive data in a batch, according to user preferences.
+ * Returns a promise which is resolved if no errors occurred. If an error
+ * occurs, a message is reported to the console and all other items are still
+ * cleared before the promise is finally rejected.
+ *
+ * @param [optional] itemsToClear
+ * Array of items to be cleared. if specified only those
+ * items get cleared, irrespectively of the preference settings.
+ * @param [optional] options
+ * Object whose properties are options for this sanitization:
+ * - ignoreTimespan (default: true): Time span only makes sense in
+ * certain cases. Consumers who want to only clear some private
+ * data can opt in by setting this to false, and can optionally
+ * specify a specific range.
+ * If timespan is not ignored, and range is not set, sanitize()
+ * will use the value of the timespan pref to determine a range.
+ * - range (default: null)
+ * - privateStateForNewWindow (default: "non-private"): when clearing
+ * open windows, defines the private state for the newly opened
+ * window.
+ */
+ async sanitize(itemsToClear = null, options = {}) {
+ let progress = options.progress || {};
+ if (!itemsToClear)
+ itemsToClear = getItemsToClearFromPrefBranch(this.PREF_CPD_BRANCH);
+ let promise = sanitizeInternal(this.items, itemsToClear, progress,
+ options);
+
+ // Depending on preferences, the sanitizer may perform asynchronous
+ // work before it starts cleaning up the Places database (e.g. closing
+ // windows). We need to make sure that the connection to that database
+ // hasn't been closed by the time we use it.
+ // Though, if this is a sanitize on shutdown, we already have a blocker.
+ if (!progress.isShutdown) {
+ let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsPIPlacesDatabase)
+ .shutdownClient
+ .jsclient;
+ shutdownClient.addBlocker("sanitize.js: Sanitize",
+ promise,
+ {
+ fetchState: () => ({ progress })
+ }
+ );
+ }
+
+ try {
+ await promise;
+ } finally {
+ Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ if (data.startsWith(this.PREF_SHUTDOWN_BRANCH) &&
+ this.shouldSanitizeOnShutdown) {
+ // Update the pending shutdown sanitization.
+ removePendingSanitization("shutdown");
+ let itemsToClear =
+ getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+ addPendingSanitization("shutdown", itemsToClear, {});
+ } else if (data == this.PREF_SANITIZE_ON_SHUTDOWN) {
+ this.shouldSanitizeOnShutdown =
+ Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN,
+ false);
+ removePendingSanitization("shutdown");
+ if (this.shouldSanitizeOnShutdown) {
+ let itemsToClear =
+ getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+ addPendingSanitization("shutdown", itemsToClear, {});
+ }
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsiObserver,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ items: {
+ cache: {
+ async clear(range) {
+ let seenException;
+
+ try {
+ // Cache doesn't consult timespan, nor does it have the
+ // facility for timespan-based eviction. Wipe it.
+ Services.cache2.clear();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let imageCache = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ // clearCache: true=chrome, false=content.
+ imageCache.clearCache(false);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ }
+ },
+
+ cookies: {
+ async clear(range) {
+ let seenException;
+ let yieldCounter = 0;
+
+ // Clear cookies.
+ try {
+ if (range) {
+ // Iterate through the cookies and delete any created after our
+ // cutoff.
+ let cookiesEnum = Services.cookies.enumerator;
+ while (cookiesEnum.hasMoreElements()) {
+ let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+
+ if (cookie.creationTime > range[0]) {
+ // This cookie was created after our cutoff, clear it
+ Services.cookies.remove(cookie.host, cookie.name, cookie.path,
+ false, cookie.originAttributes);
+
+ if (++yieldCounter % YIELD_PERIOD == 0) {
+ // Don't block the main thread too long
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+ } else {
+ // Remove everything
+ Services.cookies.removeAll();
+ // Don't block the main thread too long
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ // Clear deviceIds. Done asynchronously (returns before complete).
+ try {
+ let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"]
+ .getService(Ci.nsIMediaManagerService);
+ mediaMgr.sanitizeDeviceIds(range && range[0]);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ },
+ },
+
+ offlineApps: {
+ async clear(range) {
+ // AppCache
+ ChromeUtils.import("resource:///modules/OfflineAppCacheHelper.jsm");
+ // This doesn't wait for the cleanup to be complete.
+ OfflineAppCacheHelper.clear();
+
+ // LocalStorage
+ Services.obs.notifyObservers(null, "extension:purge-localStorage");
+
+ // ServiceWorkers
+ let promises = [];
+ let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+ for (let i = 0; i < serviceWorkers.length; i++) {
+ let sw = serviceWorkers
+ .queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+
+ promises.push(new Promise(resolve => {
+ let unregisterCallback = {
+ unregisterSucceeded: () => { resolve(true); },
+ // We don't care about failures.
+ unregisterFailed: () => { resolve(true); },
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIServiceWorkerUnregisterCallback])
+ };
+
+ serviceWorkerManager.propagateUnregister(sw.principal,
+ unregisterCallback,
+ sw.scope);
+ }));
+ }
+
+ await Promise.all(promises);
+
+ // QuotaManager
+ promises = [];
+ await new Promise(resolve => {
+ quotaManagerService.getUsage(request => {
+ if (request.resultCode != Cr.NS_OK) {
+ // We are probably shutting down. We don't want to propagate the
+ // error, rejecting the promise.
+ resolve();
+ return;
+ }
+
+ for (let item of request.result) {
+ let principal =
+ Services.scriptSecurityManager
+ .createCodebasePrincipalFromOrigin(item.origin);
+ let uri = principal.URI;
+ if (uri.scheme == "http" || uri.scheme == "https" ||
+ uri.scheme == "file") {
+ promises.push(new Promise(r => {
+ let req =
+ quotaManagerService.clearStoragesForPrincipal(principal,
+ null, false);
+ req.callback = () => { r(); };
+ }));
+ }
+ }
+ resolve();
+ });
+ });
+
+ return Promise.all(promises);
+ }
+ },
+
+ history: {
+ async clear(range) {
+ let seenException;
+ try {
+ if (range) {
+ await PlacesUtils.history.removeVisitsByFilter({
+ beginDate: new Date(range[0] / 1000),
+ endDate: new Date(range[1] / 1000)
+ });
+ } else {
+ // Remove everything.
+ await PlacesUtils.history.clear();
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let clearStartingTime = range ? String(range[0]) : "";
+ Services.obs.notifyObservers(null, "browser:purge-session-history",
+ clearStartingTime);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let predictor = Cc["@mozilla.org/network/predictor;1"]
+ .getService(Ci.nsINetworkPredictor);
+ predictor.reset();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ }
+ },
+
+ urlbar: {
+ async clear(range) {
+ let seenException;
+ // Clear last URL of the Open Web Location dialog
+ try {
+ Services.prefs.clearUserPref("general.open_location.last_url");
+ } catch(ex) {}
+
+ try {
+ // Clear URLbar history (see also pref-history.js)
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("urlbarhistory.sqlite");
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+ if (seenException) {
+ throw seenException;
+ }
+ }
+ },
+
+ formdata: {
+ async clear(range) {
+ let seenException;
+ try {
+ // Clear undo history of all search and find bars.
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let currentDocument = win.document;
+
+ let findBar = currentDocument.getElementById("FindToolbar");
+ if (findBar) {
+ findBar.clear();
+ }
+ // searchBar.textbox may not exist due to the search bar binding
+ // not having been constructed yet if the search bar is in the
+ // overflow or menu panel. It won't have a value or edit history in
+ // that case.
+ let searchBar = currentDocument.getElementById("searchbar");
+ if (searchBar && searchBar.textbox) {
+ searchBar.textbox.reset();
+ }
+
+ let sideSearchBar = win.BrowserSearch.searchSidebar;
+ if (sideSearchBar) {
+ sideSearchBar.reset();
+ }
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let change = { op: "remove" };
+ if (range) {
+ [ change.firstUsedStart, change.firstUsedEnd ] = range;
+ }
+ await new Promise(resolve => {
+ FormHistory.update(change, {
+ handleError(e) {
+ seenException = new Error("Error " + e.result + ": " +
+ e.message);
+ },
+ handleCompletion() {
+ resolve();
+ }
+ });
+ });
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ }
+ },
+
+ downloads: {
+ async clear(range) {
+ try {
+ let filterByTime = null;
+ if (range) {
+ // Convert microseconds back to milliseconds for date comparisons.
+ let rangeBeginMs = range[0] / 1000;
+ let rangeEndMs = range[1] / 1000;
+ filterByTime = download => download.startTime >= rangeBeginMs &&
+ download.startTime <= rangeEndMs;
+ }
+
+ // Clear all completed/cancelled downloads
+ let list = await Downloads.getList(Downloads.ALL);
+ list.removeFinished(filterByTime);
+ } catch (ex) {}
+ }
+ },
+
+ passwords: {
+ async clear(range) {
+ try {
+ Services.logins.removeAllLogins();
+ } catch (ex) {}
+ }
+ },
+
+ sessions: {
+ async clear(range) {
+ try {
+ // clear all auth tokens
+ let sdr = Cc["@mozilla.org/security/sdr;1"]
+ .getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+
+ // clear FTP and plain HTTP auth sessions
+ Services.obs.notifyObservers(null, "net:clear-active-logins");
+ } catch (ex) {}
+ }
+ },
+
+ siteSettings: {
+ async clear(range) {
+ let seenException;
+
+ let startDateMS = range ? range[0] / 1000 : null;
+
+ try {
+ // Clear site-specific permissions like
+ // "Allow this site to open popups".
+ // We ignore the "end" range and hope it is now() - none of the
+ // interfaces used here support a true range anyway.
+ if (startDateMS == null) {
+ Services.perms.removeAll();
+ } else {
+ Services.perms.removeAllSince(startDateMS);
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ // Clear site-specific settings like page-zoom level
+ let cps = Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2);
+ if (startDateMS == null) {
+ cps.removeAllDomains(null);
+ } else {
+ cps.removeAllDomainsSince(startDateMS, null);
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ // Clear site security settings - no support for ranges in this
+ // interface either, so we clearAll().
+ let sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ sss.clearAll();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ // Clear all push notification subscriptions
+ try {
+ await new Promise((resolve, reject) => {
+ let push = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService);
+ push.clearForDomain("*", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(new Error("Error clearing push subscriptions: " +
+ status));
+ }
+ });
+ });
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ }
+ },
+
+ openWindows: {
+ _canCloseWindow(win) {
+ if (win.CanCloseWindow()) {
+ // We already showed PermitUnload for the window, so let's
+ // make sure we don't do it again when we actually close the
+ // window.
+ win.skipNextCanClose = true;
+ return true;
+ }
+ return false;
+ },
+ _resetAllWindowClosures(windowList) {
+ for (let win of windowList) {
+ win.skipNextCanClose = false;
+ }
+ },
+ async clear(range, privateStateForNewWindow = "non-private") {
+ // NB: this closes all *browser* windows, not other windows like the
+ // library, about window, browser console, etc.
+
+ // Keep track of the time in case we get stuck in la-la-land because of
+ // onbeforeunload dialogs.
+ let existingWindow = Services.appShell.hiddenDOMWindow;
+ let startDate = existingWindow.performance.now();
+
+ // First check if all these windows are OK with being closed:
+ let windowEnumerator = Services.wm.getEnumerator("navigator:browser");
+ let windowList = [];
+ while (windowEnumerator.hasMoreElements()) {
+ let someWin = windowEnumerator.getNext();
+ windowList.push(someWin);
+ // If someone says "no" to a beforeunload prompt, we abort here:
+ if (!this._canCloseWindow(someWin)) {
+ this._resetAllWindowClosures(windowList);
+ throw new Error("Sanitize could not close windows: " +
+ "cancelled by user");
+ }
+
+ // ...however, beforeunload prompts spin the event loop, and so the
+ // code here won't get hit until the prompt has been dismissed.
+ // If more than 1 minute has elapsed since we started prompting,
+ // stop, because the user might not even remember initiating the
+ // 'forget', and the timespans will be all wrong by now anyway:
+ if (existingWindow.performance.now() > (startDate + 60 * 1000)) {
+ this._resetAllWindowClosures(windowList);
+ throw new Error("Sanitize could not close windows: timeout");
+ }
+ }
+
+ // If/once we get here, we should actually be able to close all
+ // windows.
+
+ // First create a new window. We do this first so that on non-mac, we
+ // don't accidentally close the app by closing all the windows.
+ let handler = Cc["@mozilla.org/browser/clh;1"]
+ .getService(Ci.nsIBrowserHandler);
+ let defaultArgs = handler.defaultArgs;
+ let features = "chrome,all,dialog=no," + privateStateForNewWindow;
+ let newWindow = existingWindow.openDialog("chrome://browser/content/",
+ "_blank", features,
+ defaultArgs);
+
+ let onFullScreen = null;
+ if (AppConstants.platform == "macosx") {
+ onFullScreen = function(e) {
+ newWindow.removeEventListener("fullscreen", onFullScreen);
+ let docEl = newWindow.document.documentElement;
+ let sizemode = docEl.getAttribute("sizemode");
+ if (!newWindow.fullScreen && sizemode == "fullscreen") {
+ docEl.setAttribute("sizemode", "normal");
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ return undefined;
+ };
+ newWindow.addEventListener("fullscreen", onFullScreen);
+ }
+
+ let promiseReady = new Promise(resolve => {
+ // Window creation and destruction is asynchronous. We need to wait
+ // until all existing windows are fully closed, and the new window is
+ // fully open, before continuing. Otherwise the rest of the sanitizer
+ // could run too early (and miss new cookies being set when a page
+ // closes) and/or run too late (and not have a fully-formed window
+ // yet in existence). See bug 1088137.
+ let newWindowOpened = false;
+ let onWindowOpened = function(subject, topic, data) {
+ if (subject != newWindow)
+ return;
+
+ Services.obs.removeObserver(onWindowOpened,
+ "browser-delayed-startup-finished");
+ if (AppConstants.platform == "macosx") {
+ newWindow.removeEventListener("fullscreen", onFullScreen);
+ }
+ newWindowOpened = true;
+ // If we're the last thing to happen, invoke callback.
+ if (numWindowsClosing == 0) {
+ resolve();
+ }
+ };
+
+ let numWindowsClosing = windowList.length;
+ let onWindowClosed = function() {
+ numWindowsClosing--;
+ if (numWindowsClosing == 0) {
+ Services.obs.removeObserver(onWindowClosed,
+ "xul-window-destroyed");
+ // If we're the last thing to happen, invoke callback.
+ if (newWindowOpened) {
+ resolve();
+ }
+ }
+ };
+ Services.obs.addObserver(onWindowOpened,
+ "browser-delayed-startup-finished");
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+
+ // Start the process of closing windows
+ while (windowList.length) {
+ windowList.pop().close();
+ }
+ newWindow.focus();
+ await promiseReady;
+ }
+ },
+ },
+};
+
+async function sanitizeInternal(items, aItemsToClear, progress, options = {}) {
+ let { ignoreTimespan = true, range } = options;
+ let seenError = false;
+ // Shallow copy the array, as we are going to modify it in place later.
+ if (!Array.isArray(aItemsToClear))
+ throw new Error("Must pass an array of items to clear.");
+ let itemsToClear = [...aItemsToClear];
+
+ // Store the list of items to clear, in case we are killed before we
+ // get a chance to complete.
+ let uid = gPendingSanitizationSerial++;
+ // Shutdown sanitization is managed outside.
+ if (!progress.isShutdown)
+ addPendingSanitization(uid, itemsToClear, options);
+
+ // Store the list of items to clear, for debugging/forensics purposes
+ for (let k of itemsToClear) {
+ progress[k] = "ready";
+ }
+
+ // Ensure open windows get cleared first, if they're in our list, so that
+ // they don't stick around in the recently closed windows list, and so we
+ // can cancel the whole thing if the user selects to keep a window open
+ // from a beforeunload prompt.
+ let openWindowsIndex = itemsToClear.indexOf("openWindows");
+ if (openWindowsIndex != -1) {
+ itemsToClear.splice(openWindowsIndex, 1);
+ await items.openWindows.clear(null, options);
+ progress.openWindows = "cleared";
+ }
+
+ // If we ignore timespan, clear everything,
+ // otherwise, pick a range.
+ if (!ignoreTimespan && !range) {
+ range = Sanitizer.getClearRange();
+ }
+
+ // For performance reasons we start all the clear tasks at once, then wait
+ // for their promises later.
+ // Some of the clear() calls may raise exceptions (for example bug 265028),
+ // we catch and store them, but continue to sanitize as much as possible.
+ // Callers should check returned errors and give user feedback
+ // about items that could not be sanitized
+ let annotateError = (name, ex) => {
+ progress[name] = "failed";
+ seenError = true;
+ console.error("Error sanitizing " + name, ex);
+ };
+
+ // Array of objects in form { name, promise }.
+ // `name` is the item's name and `promise` may be a promise, if the
+ // sanitization is asynchronous, or the function return value, otherwise.
+ let handles = [];
+ for (let name of itemsToClear) {
+ let item = items[name];
+ try {
+ // Catch errors here, so later we can just loop through these.
+ handles.push({ name,
+ promise: item.clear(range, options)
+ .then(() => progress[name] = "cleared",
+ ex => annotateError(name, ex))
+ });
+ } catch (ex) {
+ annotateError(name, ex);
+ }
+ }
+ for (let handle of handles) {
+ progress[handle.name] = "blocking";
+ await handle.promise;
+ }
+
+ // Sanitization is complete.
+ if (!progress.isShutdown)
+ removePendingSanitization(uid);
+ progress = {};
+ if (seenError) {
+ throw new Error("Error sanitizing");
+ }
+}
+
+async function sanitizeOnShutdown(progress) {
+ if (!Sanitizer.shouldSanitizeOnShutdown) {
+ return;
+ }
+ // Need to sanitize upon shutdown
+ let itemsToClear =
+ getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+ await Sanitizer.sanitize(itemsToClear, { progress });
+ // We didn't crash during shutdown sanitization, so annotate it to avoid
+ // sanitizing again on startup.
+ removePendingSanitization("shutdown");
+ Services.prefs.savePrefFile(null);
+}
+
+/**
+ * Gets an array of items to clear from the given pref branch.
+ * @param branch The pref branch to fetch.
+ * @return Array of items to clear
+ */
+function getItemsToClearFromPrefBranch(branch) {
+ branch = Services.prefs.getBranch(branch);
+ return Object.keys(Sanitizer.items).filter(itemName => {
+ try {
+ return branch.getBoolPref(itemName);
+ } catch (ex) {
+ return false;
+ }
+ });
+}
+
+/**
+ * These functions are used to track pending sanitization on the next
+ * startup in case of a crash before a sanitization could happen.
+ * @param id A unique id identifying the sanitization
+ * @param itemsToClear The items to clear
+ * @param options The Sanitize options
+ */
+function addPendingSanitization(id, itemsToClear, options) {
+ let pendingSanitizations = safeGetPendingSanitizations();
+ pendingSanitizations.push({id, itemsToClear, options});
+ Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
+ JSON.stringify(pendingSanitizations));
+}
+function removePendingSanitization(id) {
+ let pendingSanitizations = safeGetPendingSanitizations();
+ let i = pendingSanitizations.findIndex(s => s.id == id);
+ let [s] = pendingSanitizations.splice(i, 1);
+ Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
+ JSON.stringify(pendingSanitizations));
+ return s;
+}
+function getAndClearPendingSanitizations() {
+ let pendingSanitizations = safeGetPendingSanitizations();
+ if (pendingSanitizations.length)
+ Services.prefs.clearUserPref(Sanitizer.PREF_PENDING_SANITIZATIONS);
+ return pendingSanitizations;
+}
+function safeGetPendingSanitizations() {
+ try {
+ return JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
+ "[]"));
+ } catch (ex) {
+ Cu.reportError("Invalid JSON value for pending sanitizations: " + ex);
+ return [];
+ }
+}
diff --git a/comm/suite/components/sanitize/content/sanitizeDialog.js b/comm/suite/components/sanitize/content/sanitizeDialog.js
new file mode 100644
index 0000000000..aadc73ad10
--- /dev/null
+++ b/comm/suite/components/sanitize/content/sanitizeDialog.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+
+var gSanitizePromptDialog = {
+
+ get bundleSanitize() {
+ if (!this._bundleSanitize)
+ this._bundleSanitize = document.getElementById("bundleSanitize");
+ return this._bundleSanitize;
+ },
+
+ get selectedTimespan() {
+ var durList = document.getElementById("sanitizeDurationChoice");
+ return parseInt(durList.value);
+ },
+
+ get sanitizePreferences() {
+ if (!this._sanitizePreferences) {
+ this._sanitizePreferences =
+ document.getElementById("sanitizePreferences");
+ }
+ return this._sanitizePreferences;
+ },
+
+ init() {
+ document.documentElement.getButton("accept").label =
+ this.bundleSanitize.getString("sanitizeButtonOK");
+ },
+
+ sanitize() {
+ // Update pref values before handing off to the sanitizer.
+ this.updatePrefs();
+
+ // As the sanitize is async, we disable the buttons, update the label on
+ // the 'accept' button to indicate things are happening and return false.
+ // Once the async operation completes (either with or without errors)
+ // we close the window.
+ let docElt = document.documentElement;
+ let acceptButton = docElt.getButton("accept");
+ acceptButton.disabled = true;
+ acceptButton.setAttribute("label",
+ this.bundleSanitize
+ .getString("sanitizeButtonClearing"));
+ docElt.getButton("cancel").disabled = true;
+
+ try {
+ let range = Sanitizer.getClearRange(this.selectedTimespan);
+ let options = {
+ ignoreTimespan: !range,
+ range,
+ };
+ Sanitizer.sanitize(null, options)
+ .catch(Cu.reportError)
+ .then(() => window.close())
+ .catch(Cu.reportError);
+ return false;
+ } catch (er) {
+ Cu.reportError("Exception during sanitize: " + er);
+ return true; // We *do* want to close immediately on error.
+ }
+ },
+
+ /**
+ * Called when the value of a preference element is synced from the actual
+ * pref. Enables or disables the OK button appropriately.
+ */
+ onReadGeneric() {
+ var found = false;
+
+ // Find any other pref that's checked and enabled.
+ var i = 0;
+ while (!found && i < this.sanitizePreferences.childNodes.length) {
+ var preference = this.sanitizePreferences.childNodes[i];
+
+ found = !!preference.value &&
+ !preference.disabled;
+ i++;
+ }
+
+ try {
+ document.documentElement.getButton("accept").disabled = !found;
+ } catch (e) { }
+
+ return undefined;
+ },
+
+ /**
+ * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
+ * Because the type of this prefwindow is "child" -- and that's needed
+ * because without it the dialog has no OK and Cancel buttons -- the
+ * prefs are not updated on dialogaccept on platforms that don't support
+ * instant-apply (i.e., Windows). We must therefore manually set the prefs
+ * from their corresponding preference elements.
+ */
+ updatePrefs() {
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan);
+
+ // Now manually set the prefs from their corresponding preference
+ // elements.
+ var prefs = this.sanitizePreferences.rootBranch;
+ for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
+ var p = this.sanitizePreferences.childNodes[i];
+ prefs.setBoolPref(p.name, p.value);
+ }
+ },
+};
diff --git a/comm/suite/components/sanitize/content/sanitizeDialog.xul b/comm/suite/components/sanitize/content/sanitizeDialog.xul
new file mode 100644
index 0000000000..86c641751c
--- /dev/null
+++ b/comm/suite/components/sanitize/content/sanitizeDialog.xul
@@ -0,0 +1,154 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://communicator/skin/sanitizeDialog.css"?>
+
+<!DOCTYPE prefwindow [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % sanitizeDTD SYSTEM "chrome://communicator/locale/sanitize.dtd">
+ %sanitizeDTD;
+]>
+
+<dialog id="SanitizeDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&sanitizeDialog.title;"
+ dlgbuttons="accept,cancel"
+ style="width: &sanitizeDialog.width;;"
+ onload="gSanitizePromptDialog.init();"
+ ondialogaccept="return gSanitizePromptDialog.sanitize();">
+
+ <script src="chrome://communicator/content/sanitizeDialog.js"/>
+ <stringbundle id="bundleSanitize"
+ src="chrome://communicator/locale/sanitize.properties"/>
+
+ <prefpane id="SanitizeDialogPane">
+ <preferences id="sanitizePreferences">
+ <preference id="privacy.cpd.history"
+ name="privacy.cpd.history"
+ type="bool"/>
+ <preference id="privacy.cpd.urlbar"
+ name="privacy.cpd.urlbar"
+ type="bool"/>
+ <preference id="privacy.cpd.downloads"
+ name="privacy.cpd.downloads"
+ type="bool"/>
+ <preference id="privacy.cpd.formdata"
+ name="privacy.cpd.formdata"
+ type="bool"/>
+ <preference id="privacy.cpd.cache"
+ name="privacy.cpd.cache"
+ type="bool"/>
+ <preference id="privacy.cpd.cookies"
+ name="privacy.cpd.cookies"
+ type="bool"/>
+ <preference id="privacy.cpd.offlineApps"
+ name="privacy.cpd.offlineApps"
+ type="bool"/>
+ <preference id="privacy.cpd.passwords"
+ name="privacy.cpd.passwords"
+ type="bool"/>
+ <preference id="privacy.cpd.sessions"
+ name="privacy.cpd.sessions"
+ type="bool"/>
+ <preference id="privacy.cpd.siteSettings"
+ name="privacy.cpd.siteSettings"
+ type="bool"/>
+ </preferences>
+
+ <preferences id="nonItemPreferences">
+ <preference id="privacy.sanitize.timeSpan"
+ name="privacy.sanitize.timeSpan"
+ type="int"/>
+ </preferences>
+
+ <vbox id="sanitizeWarningBox">
+ <spacer flex="1"/>
+ <hbox align="center">
+ <image id="sanitizeWarningIcon"/>
+ <vbox id="sanitizeWarningDescBox" flex="1">
+ <description id="sanitizeSelectedWarning">
+ &sanitizeSelectedWarning;
+ </description>
+ <description id="sanitizeUndoWarning">
+ &sanitizeUndoWarning;
+ </description>
+ </vbox>
+ </hbox>
+ <spacer flex="1"/>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <hbox id="SanitizeDurationBox" align="center">
+ <label id="sanitizeDurationLabel"
+ value="&clearTimeDuration.label;"
+ accesskey="&clearTimeDuration.accesskey;"
+ control="sanitizeDurationChoice"/>
+ <menulist id="sanitizeDurationChoice"
+ preference="privacy.sanitize.timeSpan"
+ flex="1">
+ <menupopup id="sanitizeDurationPopup">
+ <menuitem label="&clearTimeDuration.lastHour;" value="1"/>
+ <menuitem label="&clearTimeDuration.last2Hours;" value="2"/>
+ <menuitem label="&clearTimeDuration.last4Hours;" value="3"/>
+ <menuitem label="&clearTimeDuration.today;" value="4"/>
+ <menuseparator/>
+ <menuitem label="&clearTimeDuration.everything;" value="0"/>
+ </menupopup>
+ </menulist>
+ <label id="sanitizeDurationSuffixLabel"
+ value="&clearTimeDuration.suffix;"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <groupbox id="itemList" flex="1">
+ <caption label="&sanitizeItems.label;"/>
+ <checkbox label="&itemHistory.label;"
+ accesskey="&itemHistory.accesskey;"
+ preference="privacy.cpd.history"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemUrlBar.label;"
+ accesskey="&itemUrlBar.accesskey;"
+ preference="privacy.cpd.urlbar"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemDownloads.label;"
+ accesskey="&itemDownloads.accesskey;"
+ preference="privacy.cpd.downloads"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemFormSearchHistory.label;"
+ accesskey="&itemFormSearchHistory.accesskey;"
+ preference="privacy.cpd.formdata"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemCache.label;"
+ accesskey="&itemCache.accesskey;"
+ preference="privacy.cpd.cache"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemCookies.label;"
+ accesskey="&itemCookies.accesskey;"
+ preference="privacy.cpd.cookies"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemOfflineApps.label;"
+ accesskey="&itemOfflineApps.accesskey;"
+ preference="privacy.cpd.offlineApps"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemPasswords.label;"
+ accesskey="&itemPasswords.accesskey;"
+ preference="privacy.cpd.passwords"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemSessions.label;"
+ accesskey="&itemSessions.accesskey;"
+ preference="privacy.cpd.sessions"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <checkbox label="&itemSitePreferences.label;"
+ accesskey="&itemSitePreferences.accesskey;"
+ preference="privacy.cpd.siteSettings"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ </groupbox>
+ </prefpane>
+</dialog>
diff --git a/comm/suite/components/sanitize/jar.mn b/comm/suite/components/sanitize/jar.mn
new file mode 100644
index 0000000000..c4255defd7
--- /dev/null
+++ b/comm/suite/components/sanitize/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.comm.jar:
+
+comm.jar:
+% content communicator %content/communicator/ contentaccessible=yes
+ content/communicator/sanitizeDialog.js (content/sanitizeDialog.js)
+ content/communicator/sanitizeDialog.xul (content/sanitizeDialog.xul)
diff --git a/comm/suite/components/sanitize/moz.build b/comm/suite/components/sanitize/moz.build
new file mode 100644
index 0000000000..be07f2ece6
--- /dev/null
+++ b/comm/suite/components/sanitize/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+EXTRA_JS_MODULES += [
+ "Sanitizer.jsm",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("SeaMonkey", "Bookmarks & History")
diff --git a/comm/suite/components/search/content/engineManager.js b/comm/suite/components/search/content/engineManager.js
new file mode 100644
index 0000000000..c505bff7fc
--- /dev/null
+++ b/comm/suite/components/search/content/engineManager.js
@@ -0,0 +1,508 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+const ENGINE_FLAVOR = "text/x-moz-search-engine";
+
+const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
+
+var gEngineView = null;
+
+var gEngineManagerDialog = {
+ init: function engineManager_init() {
+ gEngineView = new EngineView(new EngineStore());
+
+ var suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
+ document.getElementById("enableSuggest").checked = suggestEnabled;
+
+ var tree = document.getElementById("engineList");
+ tree.view = gEngineView;
+
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ },
+
+ destroy: function engineManager_destroy() {
+ // Remove the observer
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ },
+
+ observe: function engineManager_observe(aEngine, aTopic, aVerb) {
+ if (aTopic == "browser-search-engine-modified") {
+ aEngine.QueryInterface(Ci.nsISearchEngine);
+ switch (aVerb) {
+ case "engine-added":
+ gEngineView._engineStore.addEngine(aEngine);
+ gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
+ break;
+ case "engine-changed":
+ gEngineView._engineStore.reloadIcons();
+ gEngineView.invalidate();
+ break;
+ case "engine-removed":
+ case "engine-current":
+ case "engine-default":
+ // Not relevant
+ break;
+ }
+ }
+ },
+
+ onOK: function engineManager_onOK() {
+ // Set the preference
+ var newSuggestEnabled = document.getElementById("enableSuggest").checked;
+ Services.prefs.setBoolPref(BROWSER_SUGGEST_PREF, newSuggestEnabled);
+
+ // Commit the changes
+ gEngineView._engineStore.commit();
+ },
+
+ onRestoreDefaults: function engineManager_onRestoreDefaults() {
+ var num = gEngineView._engineStore.restoreDefaultEngines();
+ gEngineView.rowCountChanged(0, num);
+ gEngineView.invalidate();
+ },
+
+ showRestoreDefaults: function engineManager_showRestoreDefaults(val) {
+ document.documentElement.getButton("extra2").disabled = !val;
+ },
+
+ loadAddEngines: function engineManager_loadAddEngines() {
+ this.onOK();
+ window.arguments[0].value = true; // see OpenSearchEngineManager()
+ window.close();
+ },
+
+ remove: function engineManager_remove() {
+ gEngineView._engineStore.removeEngine(gEngineView.selectedEngine);
+ var index = gEngineView.selectedIndex;
+ gEngineView.rowCountChanged(index, -1);
+ gEngineView.invalidate();
+ gEngineView.selection.select(Math.min(index, gEngineView.lastIndex));
+ gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
+ document.getElementById("engineList").focus();
+ },
+
+ /**
+ * Moves the selected engine either up or down in the engine list
+ * @param aDir
+ * -1 to move the selected engine down, +1 to move it up.
+ */
+ bump: function engineManager_move(aDir) {
+ var selectedEngine = gEngineView.selectedEngine;
+ var newIndex = gEngineView.selectedIndex - aDir;
+
+ gEngineView._engineStore.moveEngine(selectedEngine, newIndex);
+
+ gEngineView.invalidate();
+ gEngineView.selection.select(newIndex);
+ gEngineView.ensureRowIsVisible(newIndex);
+ this.showRestoreDefaults(true);
+ document.getElementById("engineList").focus();
+ },
+
+ selectEditKeyword: function engineManager_selectEditKeyword() {
+ let index = gEngineView.selectedIndex;
+ // No engine selected.
+ if (index == -1)
+ return;
+
+ let tree = document.getElementById("engineList");
+ let column = tree.columns.getColumnFor(document.getElementById("engineKeyword"));
+ tree.startEditing(index, column);
+ },
+
+ async editKeyword(aEngine, aNewKeyword) {
+ let keyword = aNewKeyword.trim();
+ if (keyword) {
+ let eduplicate = false;
+ let dupName = "";
+
+ // Check for duplicates in Places keywords.
+ let bduplicate = !!(await PlacesUtils.keywords.fetch(keyword));
+
+ // Check for duplicates in changes we haven't committed yet
+ let engines = gEngineView._engineStore.engines;
+ for (let engine of engines) {
+ if (engine.alias == keyword &&
+ engine.name != aEngine.name) {
+ eduplicate = true;
+ dupName = engine.name;
+ break;
+ }
+ }
+
+ // Notify the user if they have chosen an existing engine/bookmark keyword
+ if (eduplicate || bduplicate) {
+ let strings = document.getElementById("engineManagerBundle");
+ let dtitle = strings.getString("duplicateTitle");
+ let bmsg = strings.getString("duplicateBookmarkMsg");
+ let emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]);
+
+ Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg);
+ return false;
+ }
+ }
+
+ gEngineView._engineStore.changeEngine(aEngine, "alias", keyword);
+ gEngineView.invalidate();
+ return true;
+ },
+
+ onSelect: function engineManager_onSelect() {
+ // Buttons only work if an engine is selected and it's not the last engine,
+ // the latter is true when the selected is first and last at the same time.
+ var lastSelected = (gEngineView.selectedIndex == gEngineView.lastIndex);
+ var firstSelected = (gEngineView.selectedIndex == 0);
+ var noSelection = (gEngineView.selectedIndex == -1);
+
+ document.getElementById("cmd_remove")
+ .setAttribute("disabled", noSelection ||
+ (firstSelected && lastSelected));
+
+ document.getElementById("cmd_moveup")
+ .setAttribute("disabled", noSelection || firstSelected);
+
+ document.getElementById("cmd_movedown")
+ .setAttribute("disabled", noSelection || lastSelected);
+
+ document.getElementById("cmd_editkeyword")
+ .setAttribute("disabled", noSelection);
+ },
+
+ onKeydown: function(aEvent) {
+ var tree = document.getElementById("engineList");
+ if (tree.editingColumn)
+ return;
+
+ if (aEvent.keyCode == (AppConstants.platform == "macosx" ?
+ KeyEvent.DOM_VK_RETURN : KeyEvent.DOM_VK_F2) &&
+ tree.startEditing(gEngineView.selectedIndex,
+ tree.columns.engineKeyword)) {
+ aEvent.preventDefault();
+ }
+ }
+};
+
+function onDragEngineStart(event) {
+ var selectedIndex = gEngineView.selectedIndex;
+ if (selectedIndex >= 0) {
+ event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
+ event.dataTransfer.effectAllowed = "move";
+ }
+}
+
+// "Operation" objects
+function EngineMoveOp(aEngineClone, aNewIndex) {
+ if (!aEngineClone)
+ throw new Error("bad args to new EngineMoveOp!");
+ this._engine = aEngineClone.originalEngine;
+ this._newIndex = aNewIndex;
+}
+EngineMoveOp.prototype = {
+ _engine: null,
+ _newIndex: null,
+ commit: function EMO_commit() {
+ Services.search.moveEngine(this._engine, this._newIndex);
+ }
+}
+
+function EngineRemoveOp(aEngineClone) {
+ if (!aEngineClone)
+ throw new Error("bad args to new EngineRemoveOp!");
+ this._engine = aEngineClone.originalEngine;
+}
+EngineRemoveOp.prototype = {
+ _engine: null,
+ commit: function ERO_commit() {
+ Services.search.removeEngine(this._engine);
+ }
+}
+
+function EngineUnhideOp(aEngineClone, aNewIndex) {
+ if (!aEngineClone)
+ throw new Error("bad args to new EngineUnhideOp!");
+ this._engine = aEngineClone.originalEngine;
+ this._newIndex = aNewIndex;
+}
+EngineUnhideOp.prototype = {
+ _engine: null,
+ _newIndex: null,
+ commit: function EUO_commit() {
+ this._engine.hidden = false;
+ Services.search.moveEngine(this._engine, this._newIndex);
+ }
+}
+
+function EngineChangeOp(aEngineClone, aProp, aValue) {
+ if (!aEngineClone)
+ throw new Error("bad args to new EngineChangeOp!");
+
+ this._engine = aEngineClone.originalEngine;
+ this._prop = aProp;
+ this._newValue = aValue;
+}
+EngineChangeOp.prototype = {
+ _engine: null,
+ _prop: null,
+ _newValue: null,
+ commit: function ECO_commit() {
+ this._engine[this._prop] = this._newValue;
+ }
+}
+
+function EngineStore() {
+ this._engines = Services.search.getVisibleEngines().map(this._cloneEngine);
+ this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine);
+
+ this._ops = [];
+
+ // check if we need to disable the restore defaults button
+ var someHidden = this._defaultEngines.some(e => e.hidden);
+ gEngineManagerDialog.showRestoreDefaults(someHidden);
+}
+EngineStore.prototype = {
+ _engines: null,
+ _defaultEngines: null,
+ _ops: null,
+
+ get engines() {
+ return this._engines;
+ },
+ set engines(val) {
+ this._engines = val;
+ return val;
+ },
+
+ _getIndexForEngine: function ES_getIndexForEngine(aEngine) {
+ return this._engines.indexOf(aEngine);
+ },
+
+ _getEngineByName: function ES_getEngineByName(aName) {
+ for (var engine of this._engines)
+ if (engine.name == aName)
+ return engine;
+
+ return null;
+ },
+
+ _cloneEngine: function ES_cloneEngine(aEngine) {
+ var clonedObj={};
+ for (var i in aEngine)
+ clonedObj[i] = aEngine[i];
+ clonedObj.originalEngine = aEngine;
+ return clonedObj;
+ },
+
+ // Callback for Array's some(). A thisObj must be passed to some()
+ _isSameEngine: function ES_isSameEngine(aEngineClone) {
+ return aEngineClone.originalEngine == this.originalEngine;
+ },
+
+ commit: function ES_commit() {
+ var currentEngine = this._cloneEngine(Services.search.currentEngine);
+ for (var i = 0; i < this._ops.length; i++)
+ this._ops[i].commit();
+
+ // Restore currentEngine if it is a default engine that is still visible.
+ // Needed if the user deletes currentEngine and then restores it.
+ if (this._defaultEngines.some(this._isSameEngine, currentEngine) &&
+ !currentEngine.originalEngine.hidden)
+ Services.search.currentEngine = currentEngine.originalEngine;
+ },
+
+ addEngine: function ES_addEngine(aEngine) {
+ this._engines.push(this._cloneEngine(aEngine));
+ },
+
+ moveEngine: function ES_moveEngine(aEngine, aNewIndex) {
+ if (aNewIndex < 0 || aNewIndex > this._engines.length - 1)
+ throw new Error("ES_moveEngine: invalid aNewIndex!");
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1)
+ throw new Error("ES_moveEngine: invalid engine?");
+
+ if (index == aNewIndex)
+ return; // nothing to do
+
+ // Move the engine in our internal store
+ var removedEngine = this._engines.splice(index, 1)[0];
+ this._engines.splice(aNewIndex, 0, removedEngine);
+
+ this._ops.push(new EngineMoveOp(aEngine, aNewIndex));
+ },
+
+ removeEngine: function ES_removeEngine(aEngine) {
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1)
+ throw new Error("invalid engine?");
+
+ this._engines.splice(index, 1);
+ this._ops.push(new EngineRemoveOp(aEngine));
+ if (this._defaultEngines.some(this._isSameEngine, aEngine))
+ gEngineManagerDialog.showRestoreDefaults(true);
+ },
+
+ restoreDefaultEngines: function ES_restoreDefaultEngines() {
+ var added = 0;
+
+ for (var i = 0; i < this._defaultEngines.length; ++i) {
+ var e = this._defaultEngines[i];
+
+ // If the engine is already in the list, just move it.
+ if (this._engines.some(this._isSameEngine, e)) {
+ this.moveEngine(this._getEngineByName(e.name), i);
+ } else {
+ // Otherwise, add it back to our internal store
+ this._engines.splice(i, 0, e);
+ this._ops.push(new EngineUnhideOp(e, i));
+ added++;
+ }
+ }
+ gEngineManagerDialog.showRestoreDefaults(false);
+ return added;
+ },
+
+ changeEngine: function ES_changeEngine(aEngine, aProp, aNewValue) {
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1)
+ throw new Error("invalid engine?");
+
+ this._engines[index][aProp] = aNewValue;
+ this._ops.push(new EngineChangeOp(aEngine, aProp, aNewValue));
+ },
+
+ reloadIcons: function ES_reloadIcons() {
+ this._engines.forEach(function (e) {
+ e.uri = e.originalEngine.uri;
+ });
+ }
+}
+
+function EngineView(aEngineStore) {
+ this._engineStore = aEngineStore;
+}
+EngineView.prototype = {
+ _engineStore: null,
+ tree: null,
+
+ get lastIndex() {
+ return this.rowCount - 1;
+ },
+ get selectedIndex() {
+ var seln = this.selection;
+ if (seln.getRangeCount() > 0) {
+ var min = {};
+ seln.getRangeAt(0, min, {});
+ return min.value;
+ }
+ return -1;
+ },
+ get selectedEngine() {
+ return this._engineStore.engines[this.selectedIndex];
+ },
+
+ // Helpers
+ rowCountChanged: function (index, count) {
+ this.tree.rowCountChanged(index, count);
+ },
+
+ invalidate: function () {
+ this.tree.invalidate();
+ },
+
+ ensureRowIsVisible: function (index) {
+ this.tree.ensureRowIsVisible(index);
+ },
+
+ getSourceIndexFromDrag: function (dataTransfer) {
+ return parseInt(dataTransfer.getData(ENGINE_FLAVOR));
+ },
+
+ // nsITreeView
+ get rowCount() {
+ return this._engineStore.engines.length;
+ },
+
+ getImageSrc: function(index, column) {
+ if (column.id == "engineName" && this._engineStore.engines[index].iconURI)
+ return this._engineStore.engines[index].iconURI.spec;
+ return "";
+ },
+
+ getCellText: function(index, column) {
+ if (column.id == "engineName")
+ return this._engineStore.engines[index].name;
+ else if (column.id == "engineKeyword")
+ return this._engineStore.engines[index].alias;
+ return "";
+ },
+
+ setCellText: function(index, column, value) {
+ if (column.id == "engineKeyword") {
+ gEngineManagerDialog.editKeyword(this._engineStore.engines[index], value)
+ .then(valid => {
+ if (!valid)
+ document.getElementById("engineList").startEditing(index, column);
+ });
+ }
+ },
+
+ setTree: function(tree) {
+ this.tree = tree;
+ },
+
+ canDrop: function(targetIndex, orientation, dataTransfer) {
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ return (sourceIndex != -1 &&
+ sourceIndex != targetIndex &&
+ sourceIndex != targetIndex + orientation);
+ },
+
+ drop: function(dropIndex, orientation, dataTransfer) {
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ var sourceEngine = this._engineStore.engines[sourceIndex];
+
+ if (dropIndex > sourceIndex) {
+ if (orientation == Ci.nsITreeView.DROP_BEFORE)
+ dropIndex--;
+ } else {
+ if (orientation == Ci.nsITreeView.DROP_AFTER)
+ dropIndex++;
+ }
+
+ this._engineStore.moveEngine(sourceEngine, dropIndex);
+ gEngineManagerDialog.showRestoreDefaults(true);
+
+ // Redraw, and adjust selection
+ this.invalidate();
+ this.selection.select(dropIndex);
+ },
+
+ selection: null,
+ getRowProperties: function(index) { return ""; },
+ getCellProperties: function(index, column) { return ""; },
+ getColumnProperties: function(column) { return ""; },
+ isContainer: function(index) { return false; },
+ isContainerOpen: function(index) { return false; },
+ isContainerEmpty: function(index) { return false; },
+ isSeparator: function(index) { return false; },
+ isSorted: function(index) { return false; },
+ getParentIndex: function(index) { return -1; },
+ hasNextSibling: function(parentIndex, index) { return false; },
+ getLevel: function(index) { return 0; },
+ getProgressMode: function(index, column) { },
+ getCellValue: function(index, column) { },
+ toggleOpenState: function(index) { },
+ cycleHeader: function(column) { },
+ selectionChanged: function() { },
+ cycleCell: function(row, column) { },
+ isEditable: function(index, column) { return column.id == "engineKeyword"; },
+ isSelectable: function(index, column) { return false; },
+ setCellValue: function(index, column, value) { },
+};
diff --git a/comm/suite/components/search/content/engineManager.xul b/comm/suite/components/search/content/engineManager.xul
new file mode 100644
index 0000000000..b160afc4eb
--- /dev/null
+++ b/comm/suite/components/search/content/engineManager.xul
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://communicator/skin/search/engineManager.css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/search/engineManager.dtd">
+
+<dialog id="engineManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="accept,cancel,extra2"
+ buttonlabelextra2="&restoreDefaults.label;"
+ buttonaccesskeyextra2="&restoreDefaults.accesskey;"
+ onload="gEngineManagerDialog.init();"
+ onunload="gEngineManagerDialog.destroy();"
+ ondialogaccept="gEngineManagerDialog.onOK();"
+ ondialogextra2="gEngineManagerDialog.onRestoreDefaults();"
+ title="&engineManager.title;"
+ style="&engineManager.style;"
+ persist="screenX screenY width height"
+ windowtype="Browser:SearchManager">
+
+ <script src="chrome://communicator/content/search/engineManager.js"/>
+
+ <commandset id="engineManagerCommandSet">
+ <command id="cmd_remove"
+ oncommand="gEngineManagerDialog.remove();"
+ disabled="true"/>
+ <command id="cmd_moveup"
+ oncommand="gEngineManagerDialog.bump(1);"
+ disabled="true"/>
+ <command id="cmd_movedown"
+ oncommand="gEngineManagerDialog.bump(-1);"
+ disabled="true"/>
+ <command id="cmd_editkeyword"
+ oncommand="gEngineManagerDialog.selectEditKeyword();"
+ disabled="true"/>
+ </commandset>
+
+ <keyset id="engineManagerKeyset">
+ <key id="delete" keycode="VK_DELETE" command="cmd_remove"/>
+ </keyset>
+
+ <stringbundleset id="engineManagerBundleset">
+ <stringbundle id="engineManagerBundle" src="chrome://communicator/locale/search/engineManager.properties"/>
+ </stringbundleset>
+
+ <description>&engineManager.intro;</description>
+ <separator class="thin"/>
+ <hbox flex="1">
+ <tree id="engineList"
+ flex="1"
+ rows="10"
+ hidecolumnpicker="true"
+ editable="true"
+ seltype="single"
+ onselect="gEngineManagerDialog.onSelect();"
+ onkeydown="gEngineManagerDialog.onKeydown(event);">
+ <treechildren id="engineChildren" flex="1"
+ ondragstart="onDragEngineStart(event);"/>
+ <treecols>
+ <treecol id="engineName" flex="4" label="&columnLabel.name;"/>
+ <treecol id="engineKeyword" flex="1" label="&columnLabel.keyword;"/>
+ </treecols>
+ </tree>
+ <vbox>
+ <spacer flex="1"/>
+ <button id="edit"
+ label="&edit.label;"
+ accesskey="&edit.accesskey;"
+ command="cmd_editkeyword"/>
+ <button id="up"
+ label="&up.label;"
+ accesskey="&up.accesskey;"
+ command="cmd_moveup"/>
+ <button id="down"
+ label="&dn.label;"
+ accesskey="&dn.accesskey;"
+ command="cmd_movedown"/>
+ <spacer flex="1"/>
+ <button id="remove"
+ label="&remove.label;"
+ accesskey="&remove.accesskey;"
+ command="cmd_remove"/>
+ </vbox>
+ </hbox>
+ <hbox>
+ <checkbox id="enableSuggest"
+ label="&enableSuggest.label;"
+ accesskey="&enableSuggest.accesskey;"/>
+ </hbox>
+ <hbox>
+ <label id="addEngines" class="text-link" value="&addEngine.label;"
+ onclick="if (event.button == 0) { gEngineManagerDialog.loadAddEngines(); }"/>
+ </hbox>
+</dialog>
diff --git a/comm/suite/components/search/content/search-panel.js b/comm/suite/components/search/content/search-panel.js
new file mode 100644
index 0000000000..c6ab43c082
--- /dev/null
+++ b/comm/suite/components/search/content/search-panel.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm");
+
+const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+var isPB, menulist, textbox;
+
+function Startup() {
+ menulist = document.getElementById("sidebar-search-engines");
+ textbox = document.getElementById("sidebar-search-text");
+ isPB = top.gPrivate;
+ if (isPB)
+ textbox.searchParam += "|private";
+
+ LoadEngineList();
+ Services.obs.addObserver(engineObserver, SEARCH_ENGINE_TOPIC, true);
+}
+
+function LoadEngineList() {
+ var currentEngineName = Services.search.currentEngine.name;
+ // Make sure the popup is empty.
+ menulist.removeAllItems();
+
+ var engines = Services.search.getVisibleEngines();
+ for (let engine of engines) {
+ let name = engine.name;
+ let menuitem = menulist.appendItem(name, name);
+ menuitem.setAttribute("class", "menuitem-iconic");
+ if (engine.iconURI)
+ menuitem.setAttribute("image", engine.iconURI.spec);
+ menuitem.engine = engine;
+ if (engine.name == currentEngineName) {
+ // Set selection to the current default engine.
+ menulist.selectedItem = menuitem;
+ }
+ }
+ // If the current engine isn't in the list any more, select the first item.
+ if (menulist.selectedIndex < 0)
+ menulist.selectedIndex = 0;
+}
+
+function SelectEngine() {
+ if (menulist.selectedItem)
+ Services.search.currentEngine = menulist.selectedItem.engine;
+ Services.obs.notifyObservers(null, SEARCH_ENGINE_TOPIC, "engine-current");
+}
+
+function doSearch() {
+ var textValue = textbox.value;
+
+ // Save the current value in the form history (shared with the search bar)
+ // except when in Private Browsing mode.
+
+ if (textValue && !isPB) {
+ FormHistory.update({
+ op: "bump",
+ fieldname: "searchbar-history",
+ value: textValue
+ }, {
+ handleError: function(aError) {
+ Cu.reportError("Saving search to form history failed: " + aError.message);
+ }
+ });
+ }
+
+ var where = Services.prefs.getBoolPref("browser.search.openintab") ? "tab" : "current";
+ var submission = Services.search.currentEngine.getSubmission(textValue);
+ openUILinkIn(submission.uri.spec, where, null, submission.postData);
+}
+
+var engineObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe: function(aEngine, aTopic, aVerb) {
+ if (aTopic == SEARCH_ENGINE_TOPIC) {
+ // Right now, always just rebuild the list after any modification.
+ LoadEngineList();
+ }
+ }
+}
diff --git a/comm/suite/components/search/content/search-panel.xul b/comm/suite/components/search/content/search-panel.xul
new file mode 100644
index 0000000000..8f7c86285e
--- /dev/null
+++ b/comm/suite/components/search/content/search-panel.xul
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/content/search/searchbarBindings.css"?>
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/search/search.css" type="text/css"?>
+
+<!DOCTYPE page SYSTEM "chrome://communicator/locale/search/search-panel.dtd" >
+<page id="searchPanel"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="Startup();"
+ elementtofocus="sidebar-search-text">
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://communicator/content/search/search-panel.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+
+ <popupset id="sidebarPopupset">
+ <panel id="PopupAutoComplete"
+ type="autocomplete"
+ noautofocus="true"/>
+ </popupset>
+
+ <menulist id="sidebar-search-engines"
+ oncommand="SelectEngine(this);"/>
+
+ <hbox align="center">
+ <textbox id="sidebar-search-text" flex="1"
+ class="search-textbox padded"
+ ontextentered="doSearch();"
+ placeholder="&search.placeholder;"
+ type="autocomplete"
+ inputtype="search"
+ autocompletepopup="PopupAutoComplete"
+ autocompletesearch="search-autocomplete"
+ autocompletesearchparam="searchbar-history"
+ maxrows="10"
+ completeselectedindex="true"
+ tabscrolling="true"/>
+ <button id="searchButton" label="&search.button.label;"
+ oncommand="doSearch();"/>
+ </hbox>
+ <button id="managerButton"
+ label="&search.engineManager.label;"
+ oncommand="window.top.OpenSearchEngineManager();"/>
+</page>
diff --git a/comm/suite/components/search/content/search.xml b/comm/suite/components/search/content/search.xml
new file mode 100644
index 0000000000..8e89824314
--- /dev/null
+++ b/comm/suite/components/search/content/search.xml
@@ -0,0 +1,739 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % searchBarDTD SYSTEM "chrome://communicator/locale/search/searchbar.dtd">
+ %searchBarDTD;
+ <!ENTITY % textcontextDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd">
+ %textcontextDTD;
+ <!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd">
+ %navigatorDTD;
+]>
+
+<bindings id="SearchBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="searchbar">
+ <resources>
+ <stylesheet src="chrome://communicator/content/search/searchbarBindings.css"/>
+ <stylesheet src="chrome://communicator/skin/search/searchbar.css"/>
+ </resources>
+ <content>
+ <xul:stringbundle src="chrome://communicator/locale/search/search.properties"
+ anonid="searchbar-stringbundle"/>
+ <!--
+ There is a dependency between "maxrows" attribute and
+ "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
+ one of them requires changing the other one.
+ -->
+ <xul:textbox class="searchbar-textbox"
+ anonid="searchbar-textbox"
+ type="autocomplete"
+ inputtype="search"
+ flex="1"
+ autocompletepopup="_child"
+ autocompletesearch="search-autocomplete"
+ autocompletesearchparam="searchbar-history"
+ maxrows="10"
+ completeselectedindex="true"
+ showcommentcolumn="true"
+ tabscrolling="true"
+ xbl:inherits="disabled">
+ <xul:box>
+ <xul:toolbarbutton class="plain searchbar-engine-button"
+ type="menu"
+ anonid="searchbar-engine-button"
+ xbl:inherits="image">
+ <xul:menupopup class="searchbar-popup"
+ anonid="searchbar-popup">
+ <xul:menuseparator/>
+ <xul:menuitem class="open-engine-manager"
+ anonid="open-engine-manager"
+ label="&cmd_engineManager.label;"
+ oncommand="OpenSearchEngineManager();"/>
+ </xul:menupopup>
+ </xul:toolbarbutton>
+ </xul:box>
+ <xul:hbox class="search-go-container">
+ <xul:image class="search-go-button"
+ anonid="search-go-button"
+ onclick="handleSearchCommand(event);"
+ tooltiptext="&searchEndCap.label;"/>
+ </xul:hbox>
+ <xul:panel anonid="searchPopupAutoComplete"
+ type="autocomplete"
+ noautofocus="true"/>
+ </xul:textbox>
+ </content>
+
+ <implementation implements="nsIObserver, nsIBrowserSearchInitObserver, nsISearchInstallCallback">
+ <constructor><![CDATA[
+ if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
+ return;
+
+ if (this.usePrivateBrowsing)
+ this._textbox.searchParam += "|private";
+
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ Services.obs.addObserver(this, "browser-search-service");
+ this._initialized = true;
+
+ Services.search.init(this);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ if (this._initialized) {
+ this._initialized = false;
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ Services.obs.removeObserver(this, "browser-search-service");
+ }
+
+ // Make sure to break the cycle from _textbox to us. Otherwise we leak
+ // the world. But make sure it's actually pointing to us.
+ // Also make sure the textbox has ever been constructed, otherwise the
+ // _textbox getter will cause the textbox constructor to run, add an
+ // observer, and leak the world too.
+ if (this._textboxInitialized && this._textbox.mController.input == this)
+ this._textbox.mController.input = null;
+ ]]></destructor>
+
+ <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-stringbundle");</field>
+ <field name="_textboxInitialized">false</field>
+ <field name="_textbox">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-textbox");</field>
+ <field name="_popup">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-popup");</field>
+ <field name="searchButton">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-engine-button");</field>
+ <field name="usePrivateBrowsing" readonly="true">
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing
+ </field>
+ <field name="_initialized">false</field>
+ <field name="_engines">null</field>
+ <field name="_needToBuildPopup">true</field>
+ <field name="FormHistory" readonly="true"><![CDATA[
+ (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {}))
+ .FormHistory;
+ ]]>
+ </field>
+ <property name="engines" readonly="true">
+ <getter><![CDATA[
+ if (!this._engines)
+ this._engines = Services.search.getVisibleEngines();
+ return this._engines;
+ ]]></getter>
+ </property>
+
+ <property name="currentEngine">
+ <setter><![CDATA[
+ Services.search.currentEngine = val;
+ Services.obs.notifyObservers(null, "browser-search-engine-modified",
+ "engine-current");
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ var currentEngine = Services.search.currentEngine;
+ // Return a dummy engine if there is no currentEngine
+ return currentEngine || {name: "", uri: null};
+ ]]></getter>
+ </property>
+
+ <!-- textbox is used by sanitize.js to clear the undo history when
+ clearing form information. -->
+ <property name="textbox" readonly="true"
+ onget="return this._textbox;"/>
+
+ <property name="value" onget="return this._textbox.value;"
+ onset="return this._textbox.value = val;"/>
+
+ <method name="focus">
+ <body><![CDATA[
+ this._textbox.focus();
+ ]]></body>
+ </method>
+
+ <method name="select">
+ <body><![CDATA[
+ this._textbox.select();
+ ]]></body>
+ </method>
+
+ <method name="onInitComplete">
+ <parameter name="aStatus"/>
+ <body><![CDATA[
+ if (!this._initialized)
+ return;
+ if (Components.isSuccessCode(aStatus)) {
+ // Refresh the display (updating icon, etc)
+ this.updateDisplay();
+ } else {
+ Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
+ }
+ ]]></body>
+ </method>
+
+ <method name="onSuccess">
+ <parameter name="aEngine"/>
+ <body><![CDATA[
+ this.currentEngine = aEngine;
+ ]]></body>
+ </method>
+
+ <method name="onError">
+ <parameter name="aErrorCode"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aEngine"/>
+ <parameter name="aTopic"/>
+ <parameter name="aVerb"/>
+ <body><![CDATA[
+ if (aTopic == "browser-search-engine-modified" ||
+ (aTopic == "browser-search-service" && aVerb == "init-complete")) {
+ switch (aVerb) {
+ case "engine-removed":
+ this.offerNewEngine(aEngine);
+ break;
+ case "engine-added":
+ this.hideNewEngine(aEngine);
+ break;
+ case "engine-current":
+ // The current engine was changed. Rebuilding the menu appears to
+ // confuse its idea of whether it should be open when it's just
+ // been clicked, so we force it to close now.
+ this._popup.hidePopup();
+ break;
+ case "engine-changed":
+ // An engine was removed (or hidden) or added, or an icon was
+ // changed. Do nothing special.
+ }
+
+ // Make sure the engine list is refetched next time it's needed
+ this._engines = null;
+
+ // Rebuild the popup and update the display after any modification.
+ this.rebuildPopup();
+ this.updateDisplay();
+ }
+ ]]></body>
+ </method>
+
+ <!-- There are two seaprate lists of search engines, whose uses intersect
+ in this file. The search service (nsIBrowserSearchService and
+ nsSearchService.js) maintains a list of Engine objects which is used to
+ populate the searchbox list of available engines and to perform queries.
+ That list is accessed here via this.SearchService, and it's that sort of
+ Engine that is passed to this binding's observer as aEngine.
+
+ In addition, navigator.js fills two lists of autodetected search engines
+ (browser.engines and browser.hiddenEngines) as properties of
+ mCurrentBrowser. Those lists contain unnamed JS objects of the form
+ { uri:, title:, icon: }, and that's what the searchbar uses to determine
+ whether to show any "Add <EngineName>" menu items in the drop-down.
+
+ The two types of engines are currently related by their identifying
+ titles (the Engine object's 'name'), although that may change; see bug
+ 335102. -->
+
+ <!-- If the engine that was just removed from the searchbox list was
+ autodetected on this page, move it to each browser's active list so it
+ will be offered to be added again. -->
+ <method name="offerNewEngine">
+ <parameter name="aEngine"/>
+ <body><![CDATA[
+ for (var browser of getBrowser().browsers) {
+ if (browser.hiddenEngines) {
+ // XXX This will need to be changed when engines are identified by
+ // URL rather than title; see bug 335102.
+ var removeTitle = aEngine.wrappedJSObject.name;
+ for (var i = 0; i < browser.hiddenEngines.length; i++) {
+ if (browser.hiddenEngines[i].title == removeTitle) {
+ if (!browser.engines)
+ browser.engines = [];
+ browser.engines.push(browser.hiddenEngines[i]);
+ browser.hiddenEngines.splice(i, 1);
+ break;
+ }
+ }
+ }
+ }
+ this.updateSearchButton();
+ ]]></body>
+ </method>
+
+ <!-- If the engine that was just added to the searchbox list was
+ autodetected on this page, move it to each browser's hidden list so it is
+ no longer offered to be added. -->
+ <method name="hideNewEngine">
+ <parameter name="aEngine"/>
+ <body><![CDATA[
+ for (var browser of getBrowser().browsers) {
+ if (browser.engines) {
+ // XXX This will need to be changed when engines are identified by
+ // URL rather than title; see bug 335102.
+ var removeTitle = aEngine.wrappedJSObject.name;
+ for (var i = 0; i < browser.engines.length; i++) {
+ if (browser.engines[i].title == removeTitle) {
+ if (!browser.hiddenEngines)
+ browser.hiddenEngines = [];
+ browser.hiddenEngines.push(browser.engines[i]);
+ browser.engines.splice(i, 1);
+ break;
+ }
+ }
+ }
+ }
+ this.updateSearchButton();
+ ]]></body>
+ </method>
+
+ <method name="updateSearchButton">
+ <body><![CDATA[
+ var engines = getBrowser().mCurrentBrowser.engines;
+ if (engines && engines.length)
+ this.searchButton.setAttribute("addengines", "true");
+ else
+ this.searchButton.removeAttribute("addengines");
+ ]]></body>
+ </method>
+
+ <method name="updateDisplay">
+ <body><![CDATA[
+ var uri = this.currentEngine.iconURI;
+ this.setAttribute("image", uri ? uri.spec : "");
+
+ var name = this.currentEngine.name;
+ var text = this._stringBundle.getFormattedString("searchtip", [name]);
+ this._textbox.placeholder = name;
+ this._textbox.tooltipText = text;
+ ]]></body>
+ </method>
+
+ <!-- Rebuilds the dynamic portion of the popup menu (i.e., the menu items
+ for new search engines that can be added to the available list). This
+ is called each time the popup is shown.
+ -->
+ <method name="rebuildPopupDynamic">
+ <body><![CDATA[
+ // We might not have added the main popup items yet, do that first
+ // if needed.
+ if (this._needToBuildPopup)
+ this.rebuildPopup();
+
+ var popup = this._popup;
+ // Clear any addengine menuitems, including addengine-item entries and
+ // the addengine-separator. Work backward to avoid invalidating the
+ // indexes as items are removed.
+ var items = popup.childNodes;
+ for (var i = items.length - 1; i >= 0; i--) {
+ if (items[i].classList.contains("addengine-item") ||
+ items[i].classList.contains("addengine-separator"))
+ items[i].remove();
+ }
+
+ var addengines = getBrowser().mCurrentBrowser.engines;
+ if (addengines && addengines.length > 0) {
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ // Find the (first) separator in the remaining menu, or the first item
+ // if no separators are present.
+ var insertLocation = popup.firstChild;
+ while (insertLocation.nextSibling &&
+ insertLocation.localName != "menuseparator") {
+ insertLocation = insertLocation.nextSibling;
+ }
+ if (insertLocation.localName != "menuseparator")
+ insertLocation = popup.firstChild;
+
+ var separator = document.createElementNS(kXULNS, "menuseparator");
+ separator.setAttribute("class", "addengine-separator");
+ popup.insertBefore(separator, insertLocation);
+
+ // Insert the "add this engine" items.
+ for (var i = 0; i < addengines.length; i++) {
+ var engineInfo = addengines[i];
+ var labelStr =
+ this._stringBundle.getFormattedString("cmd_addFoundEngine",
+ [engineInfo.title]);
+ var menuitem = document.createElementNS(kXULNS, "menuitem");
+ menuitem.setAttribute("class", "menuitem-iconic addengine-item");
+ menuitem.setAttribute("label", labelStr);
+ menuitem.setAttribute("tooltiptext", engineInfo.uri);
+ menuitem.setAttribute("uri", engineInfo.uri);
+ if (engineInfo.icon)
+ menuitem.setAttribute("image", engineInfo.icon);
+ menuitem.setAttribute("title", engineInfo.title);
+ popup.insertBefore(menuitem, insertLocation);
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- Rebuilds the list of visible search engines in the menu. Does not remove
+ or update any dynamic entries (i.e., "Add this engine" items) nor the
+ Manage Engines item. This is called by the observer when the list of
+ visible engines, or the currently selected engine, has changed.
+ -->
+ <method name="rebuildPopup">
+ <body><![CDATA[
+ var popup = this._popup;
+
+ // Clear the popup, down to the first separator
+ while (popup.firstChild && popup.firstChild.localName != "menuseparator")
+ popup.firstChild.remove();
+
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var engines = this.engines;
+ for (var i = engines.length - 1; i >= 0; --i) {
+ var menuitem = document.createElementNS(kXULNS, "menuitem");
+ var name = engines[i].name;
+ menuitem.setAttribute("label", name);
+ menuitem.setAttribute("class", "menuitem-iconic searchbar-engine-menuitem menuitem-with-favicon");
+ // Since this menu is rebuilt by the observer method whenever a new
+ // engine is selected, the "selected" attribute does not need to be
+ // explicitly cleared anywhere.
+ if (engines[i] == this.currentEngine)
+ menuitem.setAttribute("selected", "true");
+ var tooltip = this._stringBundle.getFormattedString("searchtip", [name]);
+ menuitem.setAttribute("tooltiptext", tooltip);
+ if (engines[i].iconURI)
+ menuitem.setAttribute("image", engines[i].iconURI.spec);
+ popup.insertBefore(menuitem, popup.firstChild);
+ menuitem.engine = engines[i];
+ }
+
+ this._needToBuildPopup = false;
+ ]]></body>
+ </method>
+
+ <method name="selectEngine">
+ <parameter name="aEvent"/>
+ <parameter name="isNextEngine"/>
+ <body><![CDATA[
+ // Find the new index
+ var newIndex = this.engines.indexOf(this.currentEngine);
+ newIndex += isNextEngine ? 1 : -1;
+
+ if (newIndex >= 0 && newIndex < this.engines.length)
+ this.currentEngine = this.engines[newIndex];
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ ]]></body>
+ </method>
+
+ <method name="handleSearchCommand">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var textBox = this._textbox;
+ var textValue = textBox.value;
+
+ var where = "current";
+ if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
+ if (aEvent.button == 2)
+ return;
+ where = whereToOpenLink(aEvent, false, true);
+ }
+ else {
+ var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+ if ((aEvent && aEvent.altKey) ^ newTabPref)
+ where = "tabfocused";
+ }
+
+ this.doSearch(textValue, where);
+ ]]></body>
+ </method>
+
+ <method name="doSearch">
+ <parameter name="aData"/>
+ <parameter name="aWhere"/>
+ <body><![CDATA[
+ var textBox = this._textbox;
+
+ // Save the current value in the form history.
+ if (aData && !this.usePrivateBrowsing && this.FormHistory.enabled) {
+ this.FormHistory.update(
+ { op: "bump",
+ fieldname: textBox.getAttribute("autocompletesearchparam"),
+ value: aData },
+ { handleError(aError) {
+ Cu.reportError("Saving search to form history failed: " + aError.message);
+ }});
+ }
+
+ // null parameter below specifies HTML response for search
+ var submission = this.currentEngine.getSubmission(aData);
+ openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="command"><![CDATA[
+ const target = event.originalTarget;
+ if (target.engine) {
+ this.currentEngine = target.engine;
+ } else if (target.classList.contains("addengine-item")) {
+ // We only detect OpenSearch files
+ var type = Ci.nsISearchEngine.DATA_XML;
+ // Select the installed engine if the installation succeeds
+ Services.search.addEngine(target.getAttribute("uri"), type,
+ target.getAttribute("image"), false,
+ this);
+ }
+ else
+ return;
+
+ this.focus();
+ this.select();
+ ]]></handler>
+
+ <handler event="popupshowing" action="this.rebuildPopupDynamic();"/>
+
+ <handler event="DOMMouseScroll"
+ phase="capturing"
+ modifiers="accel"
+ action="this.selectEngine(event, (event.detail > 0));"/>
+
+ <handler event="focus"><![CDATA[
+ // Speculatively connect to the current engine's search URI (and
+ // suggest URI, if different) to reduce request latency.
+ this.currentEngine.speculativeConnect({window: window});
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="search-textbox"
+ extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
+ <implementation implements="nsIObserver">
+ <constructor><![CDATA[
+ var bindingParent = document.getBindingParent(this);
+ if (bindingParent && bindingParent.parentNode.parentNode.localName ==
+ "toolbarpaletteitem")
+ return;
+
+ if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
+ this.setAttribute("clickSelectsAll", true);
+
+ // Add items to context menu and attach controller to handle them
+ this.controllers.appendController(this.searchbarController);
+ // bindingParent is not set in the sidebar because there is no
+ // searchbar created in it.
+ if (bindingParent) {
+ bindingParent._textboxInitialized = true;
+ }
+
+ // Add observer for suggest preference
+ Services.prefs.addObserver("browser.search.suggest.enabled", this);
+
+ this._inputBox.setAttribute("suggestchecked", this._suggestEnabled);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ Services.prefs.removeObserver("browser.search.suggest.enabled", this);
+
+ // Because XBL and the customize toolbar code interacts poorly,
+ // there may not be anything to remove here
+ try {
+ this.controllers.removeController(this.searchbarController);
+ } catch (ex) { }
+ ]]></destructor>
+ <field name="_inputBox">
+ document.getAnonymousElementByAttribute(this, "anonid", "textbox-input-box");
+ </field>
+ <field name="_suggestEnabled">
+ Services.prefs.getBoolPref("browser.search.suggest.enabled");
+ </field>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ if (aTopic == "nsPref:changed") {
+ this._suggestEnabled =
+ Services.prefs.getBoolPref("browser.search.suggest.enabled");
+ this._inputBox.setAttribute("suggestchecked", this._suggestEnabled);
+ }
+ ]]></body>
+ </method>
+
+ <field name="FormHistory" readonly="true"><![CDATA[
+ (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {}))
+ .FormHistory;
+ ]]>
+ </field>
+
+ <!-- nsIController -->
+ <field name="searchbarController" readonly="true"><![CDATA[({
+ supportsCommand: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_pasteAndSearch":
+ case "cmd_clearhistory":
+ case "cmd_togglesuggest":
+ return true;
+ }
+ return false;
+ },
+
+ isCommandEnabled: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_pasteAndSearch":
+ return document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .isCommandEnabled("cmd_paste");
+ case "cmd_clearhistory":
+ case "cmd_togglesuggest":
+ return true;
+ }
+ return false;
+ },
+
+ doCommand: function (aCommand) {
+ switch (aCommand) {
+ case "cmd_pasteAndSearch":
+ this.select();
+ goDoCommand("cmd_paste");
+ this.onTextEntered();
+ break;
+ case "cmd_clearhistory":
+ this.FormHistory.update(
+ { op : "remove", fieldname : "searchbar-history" },
+ null);
+ this.value = "";
+ break;
+ case "cmd_togglesuggest":
+ // The pref observer will update _suggestEnabled and the menu
+ // checkmark.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled",
+ !this._suggestEnabled);
+ break;
+ default:
+ // do nothing with unrecognized command
+ }
+ }.bind(this)
+ })]]></field>
+ </implementation>
+
+ <handlers>
+ <handler event="dragover">
+ <![CDATA[
+ var types = event.dataTransfer.types;
+ if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="drop">
+ <![CDATA[
+ var dataTransfer = event.dataTransfer;
+ var data = dataTransfer.getData("text/plain");
+ if (!data)
+ data = dataTransfer.getData("text/x-moz-text-internal");
+ if (data) {
+ event.preventDefault();
+ this.value = data;
+ this.onTextEntered();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="searchbar-textbox"
+ extends="chrome://communicator/content/search/search.xml#search-textbox">
+ <implementation>
+ <method name="openSearch">
+ <body>
+ <![CDATA[
+ // Don't open search popup if history popup is open
+ if (!this.popupOpen) {
+ document.getBindingParent(this).searchButton.open = true;
+ return false;
+ }
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <!-- override |onTextEntered| in autocomplete.xml -->
+ <method name="onTextEntered">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var evt = aEvent || this.mEnterEvent;
+ document.getBindingParent(this).handleSearchCommand(evt);
+ this.mEnterEvent = null;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_UP" modifiers="accel"
+ phase="capturing"
+ action="document.getBindingParent(this).selectEngine(event, false);"/>
+
+ <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
+ phase="capturing"
+ action="document.getBindingParent(this).selectEngine(event, true);"/>
+
+ <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
+ phase="capturing"
+ action="return this.openSearch();"/>
+
+ <handler event="keypress" keycode="VK_UP" modifiers="alt"
+ phase="capturing"
+ action="return this.openSearch();"/>
+
+ <handler event="keypress" keycode="VK_F4" phase="capturing">
+ <![CDATA[
+ return (AppConstants.platform == "macosx") || this.openSearch()
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="input-box-search" extends="chrome://global/content/bindings/textbox.xml#input-box">
+ <content context="_child">
+ <children/>
+ <xul:menupopup anonid="input-box-contextmenu"
+ class="textbox-contextmenu"
+ onpopupshowing="var input =
+ this.parentNode.getElementsByAttribute('anonid', 'input')[0];
+ if (document.commandDispatcher.focusedElement != input)
+ input.focus();
+ this.parentNode._doPopupItemEnabling(this);"
+ oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if (cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }">
+ <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
+ <xul:menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"/>
+ <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/>
+ <xul:menuitem label="&pasteSearchCmd.label;" accesskey="&pasteSearchCmd.accesskey;" cmd="cmd_pasteAndSearch"/>
+ <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&clearHistoryCmd.label;" accesskey="&clearHistoryCmd.accesskey;" cmd="cmd_clearhistory"/>
+ <xul:menuitem label="&showSuggestionsCmd.label;"
+ accesskey="&showSuggestionsCmd.accesskey;"
+ anonid="toggle-suggest-item"
+ type="checkbox"
+ autocheck="false"
+ xbl:inherits="checked=suggestchecked"
+ cmd="cmd_togglesuggest"/>
+ </xul:menupopup>
+ </content>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/search/content/searchbarBindings.css b/comm/suite/components/search/content/searchbarBindings.css
new file mode 100644
index 0000000000..3fefc9365a
--- /dev/null
+++ b/comm/suite/components/search/content/searchbarBindings.css
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.search-textbox {
+ -moz-binding: url("chrome://communicator/content/search/search.xml#search-textbox");
+}
+
+.searchbar-textbox {
+ -moz-binding: url("chrome://communicator/content/search/search.xml#searchbar-textbox");
+}
+
+.textbox-input-box {
+ -moz-binding: url("chrome://communicator/content/search/search.xml#input-box-search");
+}
+
+.autocomplete-history-dropmarker {
+ display: none;
+}
diff --git a/comm/suite/components/search/jar.mn b/comm/suite/components/search/jar.mn
new file mode 100644
index 0000000000..d6ec19c78e
--- /dev/null
+++ b/comm/suite/components/search/jar.mn
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+% content communicator %content/communicator/ contentaccessible=yes
+ content/communicator/search/engineManager.js (content/engineManager.js)
+ content/communicator/search/engineManager.xul (content/engineManager.xul)
+ content/communicator/search/search.xml (content/search.xml)
+ content/communicator/search/searchbarBindings.css (content/searchbarBindings.css)
+ content/communicator/search/search-panel.js (content/search-panel.js)
+ content/communicator/search/search-panel.xul (content/search-panel.xul)
+
+ searchplugins/ (searchplugins/**)
+
+% resource search-plugins %searchplugins/
diff --git a/comm/suite/components/search/moz.build b/comm/suite/components/search/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/search/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/search/searchplugins/allegro-pl.xml b/comm/suite/components/search/searchplugins/allegro-pl.xml
new file mode 100644
index 0000000000..e6a9f97b4e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/allegro-pl.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Allegro</ShortName>
+<Description>Wyszukiwanie w aukcjach Allegro</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAFr/OgBa/zkAWv84AFr/OABa/zgAWv85AFr/PABa/wn///8A////AP///wD///8A////AABa/xYAWv+4AFr//wBa//8AWv//AFr//wBa//8AWv//AFr//wBa//8AWv+H////AP///wD///8A////AP///wAAWv/wAFr//wBa//8AWv//AFr//wBa//8AWv//AFr//wBa//8AWv//AFr/gv///wD///8A////AP///wAAWv9MAFr//wBa//8AWv//AFr/ewBa/zUAWv82AFr/NABa/2YAWv//AFr//wBa/4H///8A////AP///wD///8AAFr/gwBa//8AWv//AFr/if///wD///8A////AP///wAAWv8yAFr//wBa//8AWv+A////AP///wD///8A////AABa/4MAWv//AFr//wBa/8L///8A////AP///wD///8AAFr/MQBa//8AWv//AFr/gP///wD///8A////AP///wAAWv8oAFr//wBa//8AWv//AFr/xABa/34AWv9/AFr/fQBa/5sAWv//AFr//wBa/4D///8A////AP///wD///8A////AABa/5QAWv//AFr//wBa//8AWv//AFr//wBa//8AWv//AFr//wBa//8AWv+A////AP///wD///8A////AP///wD///8AAFr/NwBa/7UAWv//AFr//wBa//8AWv//AFr//wBa//8AWv//AFr/gf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AABa/2IAWv//AFr//wBa/4L///8A////AP///wD///8A////AP///wAAWv/RAFr/ywBa/0b///8A////AABa/woAWv/mAFr//wBa//8AWv85////AP///wD///8A////AP///wD///8AAFr//wBa//8AWv//AFr//wBa//8AWv//AFr//wBa//8AWv/v////AP///wD///8A////AP///wD///8A////AABa/5gAWv//AFr//wBa//8AWv//AFr//wBa//8AWv//AFr/J////wD///8A////AP///wD///8A////AP///wD///8AAFr/FgBa/14AWv+CAFr/gQBa/4IAWv9eAFr/BP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//8AAP//AADwAwAA4AMAAOPjAADD4wAAw+MAAOHDAADgAwAA+AMAAP/jAADzxwAA8AcAAPAPAAD+PwAA//8AAA==</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://allegro.pl/listing/listing.php"
+ resultdomain="allegro.pl">
+ <Param name="string" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://allegro.pl/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-br.xml b/comm/suite/components/search/searchplugins/amazon-br.xml
new file mode 100644
index 0000000000..4eb17f368d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-br.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.com.br</ShortName>
+<Description>Pesquisa Amazon.com.br</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.com.br/exec/obidos/external-search/"
+ resultdomain="amazon.com.br">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.com.br/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-de.xml b/comm/suite/components/search/searchplugins/amazon-de.xml
new file mode 100644
index 0000000000..b25a056ced
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-de.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.de</ShortName>
+<Description>Amazon.de Suche</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.de/exec/obidos/external-search/"
+ resultdomain="amazon.de">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.de/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-en-GB.xml b/comm/suite/components/search/searchplugins/amazon-en-GB.xml
new file mode 100644
index 0000000000..30f57ff346
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-en-GB.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.co.uk</ShortName>
+<Description>Amazon.co.uk Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.co.uk/exec/obidos/external-search/"
+ resultdomain="amazon.co.uk">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.co.uk/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-es.xml b/comm/suite/components/search/searchplugins/amazon-es.xml
new file mode 100644
index 0000000000..641a44948a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-es.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.es</ShortName>
+<Description>Amazon.es Buscar</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.es/exec/obidos/external-search/"
+ resultdomain="amazon.es">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.es/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-fr.xml b/comm/suite/components/search/searchplugins/amazon-fr.xml
new file mode 100644
index 0000000000..9e3176f067
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-fr.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.fr</ShortName>
+<Description>Recherche Amazon.fr</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.fr/exec/obidos/external-search/"
+ resultdomain="amazon.fr">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.fr/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-it.xml b/comm/suite/components/search/searchplugins/amazon-it.xml
new file mode 100644
index 0000000000..488a41583d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-it.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.it</ShortName>
+<Description>Ricerca Amazon.it</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.it/exec/obidos/external-search/"
+ resultdomain="amazon.it">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.it/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-jp.xml b/comm/suite/components/search/searchplugins/amazon-jp.xml
new file mode 100644
index 0000000000..a1725d74c0
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-jp.xml
@@ -0,0 +1,32 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.co.jp</ShortName>
+<Description>Amazon.co.jp Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.co.jp/exec/obidos/external-search/"
+ resultdomain="amazon.co.jp">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="mode" value="blended"/>
+ <!--
+ <Param name="mode" value="books-jp"/>
+ <Param name="mode" value="books-us"/>
+ -->
+ <Param name="sourceid" value="Mozilla-search"/>
+ <!--
+ <Param name="sz" value="25"/>
+ <Param name="rank" value="+salesrank"/>
+ <Param name="rank" value="+pricerank"/>
+ <Param name="rank" value="+inverse-pricerank"/>
+ <Param name="rank" value="+daterank"/>
+ <Param name="rank" value="+titlerank"/>
+ <Param name="rank" value="-titlerank"/>
+ -->
+</Url>
+<SearchForm>https://www.amazon.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon-zh-CN.xml b/comm/suite/components/search/searchplugins/amazon-zh-CN.xml
new file mode 100644
index 0000000000..bdf814bf71
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon-zh-CN.xml
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>亚马逊</ShortName>
+<Description>亚马逊æœç´¢</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.cn/mn/searchApp"
+ resultdomain="amazon.cn">
+ <Param name="keywords" value="{searchTerms}"/>
+ <Param name="ix" value="sunray"/>
+ <Param name="pageletid" value="headsearch"/>
+ <Param name="searchType" value=""/>
+ <Param name="Go.x" value="0"/>
+ <Param name="Go.y" value="0"/>
+ <Param name="bestSaleNum" value="0"/>
+</Url>
+<SearchForm>https://www.amazon.cn/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/amazon.xml b/comm/suite/components/search/searchplugins/amazon.xml
new file mode 100644
index 0000000000..b84e74a584
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/amazon.xml
@@ -0,0 +1,27 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.com</ShortName>
+<Description>Amazon.com Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://completion.amazon.com/search/complete">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="search-alias" value="aps"/>
+ <Param name="mkt" value="1"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.amazon.com/exec/obidos/external-search/"
+ resultdomain="amazon.com">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="ie" value="{inputEncoding}"/>
+ <Param name="mode" value="blended"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.amazon.com/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/atlas-sk.xml b/comm/suite/components/search/searchplugins/atlas-sk.xml
new file mode 100644
index 0000000000..4caaf67811
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/atlas-sk.xml
@@ -0,0 +1,14 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Atlas</ShortName>
+<Description>Internetovy portal - Atlas.sk</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAALESURBVHjaYvz//z8DMvj7nuHXY4YfLxiA4hziDJzyDIyCyPIAAcSCYP55w/Bi7deHW188e/H5B/tfRg5mVnZuAWEJVXteWR8GVjGIKoAAYoTa8P3mz0tVj27f+8rrIazmxSkgx8TM+v/fr6/vH768vZ+L7aOKZTI7vw5QIUAAAW3+///7ve8H3W+sCHh+59zf/+jgz5//j64fO7e16fPrG0AuQAAxAI35e7n4xVr3L68f/8cNnt27fOXQit+/vgMEEOP/z5d+HMn+r97Eqehw/9Wf5Ue+/vrDyMLCxMLIyMTMKMzDbKPBqi7FBHTMz+/f2Dg4AAKIheH9JUYuTTYZK6DQ07d/Zu36+O0HIzs7MzMTEysrMycH26Zzv0t9OW3Umdk5uYBqAAKI5e+394y8mkysbECOhjRLTzTX9x9/mZiY/v//t+/0g1MvJO+/5t989pe5CicrM8jPAAHExMDE85+ZCxJkInwsqrJ8Pxk43nxl+vybjY2V9d+fn3/+Mjx88+/nb2h0AQQQCxOPIsvvuxDOhpPfGtd8fvWJgY0V6KL/zMyKzBwM//7++/2PCR67AAHExCigwcDMzvDn8/ffDGtOfHvwhoH970c7yftxhi9Nuc/+/vjk9z/mv38Z4dELEEAsDJxijJL2DEws//8w/GdgYmX+//c/44GTDw7/+/Lr509maTEmBsa///4zwKwACCBg0mD8zyPz689v5v8/HDUZTt399+OPMLO8x39GBjZGRqDCn7///vqDSEEAAcQCTBk/vn9/9+79t29fjcV/ZZp9P3Pv7/effxkY/nNyccnJyvAL8EkJMLGxQF0FEEDA+GFgZWVhYWUFJoGvX75JsT4zFXzz7t2nX7//crNwG4rquTiYcnKywm0ACCBGePL+8/fvn99/fv768+Pn71+//gDjAehadnY2QX4eNjZmuAaAAAMA9HBNfsA2E5MAAAAASUVORK5CYII=</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.atlas.sk/search.php"
+ resultdomain="atlas.sk">
+ <Param name="phrase" value="{searchTerms}"/>
+ <Param name="sourceid" value="firefox"/>
+</Url>
+<SearchForm>https://www.atlas.sk/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/azet-sk.xml b/comm/suite/components/search/searchplugins/azet-sk.xml
new file mode 100644
index 0000000000..49b04cf57e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/azet-sk.xml
@@ -0,0 +1,15 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Azet</ShortName>
+<Description>Azet - portal, kde je vzdy najviac ludi</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANinbf/GdRz/xnUc/8qDNv/Yp23/68+u/9inbf/Yp23/2Kdt/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMqDNv/GdRz/xnUc/8Z1HP/Kgzb/xnUc/8Z1HP/GdRz/xnUc/8qDNv8AAAAAAAAAAAAAAAAAAAAAAAAAAOvPrv/GdRz/xnUc/8qDNv8AAAAAAAAAAAAAAADYp23/xnUc/8Z1HP/Kgzb/AAAAAAAAAAAAAAAAAAAAAAAAAADrz67/xnUc/8Z1HP/Yp23/AAAAAAAAAAAAAAAA68+u/8Z1HP/GdRz/yoM2/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMqDNv/GdRz/xnUc/9inbf/rz67/68+u/+vPrv/GdRz/xnUc/8qDNv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2Kdt/8qDNv/GdRz/xnUc/8Z1HP/GdRz/xnUc/8Z1HP/Kgzb/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrz67/2Kdt/8Z1HP/GdRz/yoM2//qu2f/uR5n/7keZ/+5Hmf/uR5n/AAAAAMqDNv/Kgzb/yoM2/wAAAAAAAAAAAAAAAOvPrv/GdRz/xnUc/8qDNv/uR5n/7keZ/+5Hmf/uR5n/7keZ/wAAAADYp23/xnUc/8Z1HP/Kgzb/68+u/+vPrv/Kgzb/xnUc/8Z1HP/Yp23/7keZ/+5Hmf/uR5n/7keZ//qu2f8AAAAAAAAAANinbf/GdRz/xnUc/8Z1HP/GdRz/xnUc/8Z1HP/Yp23/AAAAAAAAAAD6rtn/AAAAAOvPrv/Kgzb/vXsA/9inbf8AAAAAAAAAAOvPrv/rz67/68+u/+vPrv8AAAAAAAAAAAAAAAAAAAAAAAAAAOvPrv/Bfwz/vXsA/717AP/Yp23/AAAAAFnCg/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKgzb/vXsA/717AP/Kgzb/AAAAAFnCg/9ZwoP/WcKD/wAAAAATof//E6H//wAAAAAAAAAAAAAAAAAAAAAAAAAA68+u/8F/DP/Bfwz/68+u/wAAAABZwoP/WcKD/1nCg/8AAAAAE6H//xOh//8Tof//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABZwoP/WcKD/1nCg/9ZwoP/AAAAABOh//8Tof//E6H//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFnCg/9ZwoP/AAAAAAAAAAATof//E6H//xOh//8AAAAAAAAAAP4AAAD8AAAA+HAAAPhwAAD8AAAA/gAAAP/gAAAEcAAABAAAAAYBAAChhwAAwX8AAMInAADCIwAA/CMAAP5jAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAATCwAAEwsAAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAABgIAAAv///wDMZgAFtGQUM7VnEkW1ZxJFs2YSRrNnE0OzZxYvs2YaCv///wD///8Av2AgCLhmFBm1ahUYtWoVGK1mFBmuaBcW1YAABv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8As2YSRrVnE7m0ZxTyvm0V/75tFf+/bRX/u2sU/7NnE+6zZxO9s2YTa79AAAS0ZxNqs2cU2LNnFMy0ZxTLs2cT17RnE665XRcL////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/gAAC////ALNoFFuzZxT2wG0V/7VnFP+zZxP9tGcT+7NmE/m0ZxP7tGcU/7trFP+3aRT/s2cTubNnFNC+bRX/uWoU/7lqFP/CbxX/s2YTr////wD/AAAB////AP///wD///8A////AP///wD///8A////AP///wD///8AqlUAA////wC0aBEstGYT7bpqFP+0ZxP6tGYT/rNmFP+4aRT/wm8V/71sFf+9bBX/vmwV/7dpFP+5ahT/tGcU/7RnE/y0ZxP8s2cT/bZoFP+zZxN5////AICAAAL///8A////AP///wD///8A////AP///wD///8A////AP///wCAgAAC////ALRnE3fBbhX/tGcT+rRnFP+0ZhP+t2kU/7RnFMu0ZxNqs2cSVLVoElO1ZhR/s2YT4bdpFP+0ZhP+tGcU/7NmFP+zZxT/uGkU/7NoE3b///8A/4AAAv///wD///8A////AP///wD///8A////AP///wD///8A/wAAAf///wCqVQADs2YTorxrFP+zZxP9s2cT/bpqFP+zZhTbuGoVJP///wD///8A////AP///wCyZRIrs2cU2LlqFP+zZxP9s2YU/7NnFP+4aRT/tGcTd////wCqVQAD////AP///wD///8A////AP///wD///8A////AP///wD//wAB////ALZtJAezZhOvu2sU/7NnE/20ZxP7wG4V/7NnFLP///8AqnEcCapVAAaZZgAFqlUAA////wCzZhNstmgU/7RnFP+0ZhP+s2cU/7hpFP+0ZxN3////AP//AAH///8A////AP///wD///8A////AP///wD///8A////AP8AAAH///8A////ALNnFJe+bRX/s2YT/LRnE/y7axT/s2cT07tmEQ////8A////AICAAALVgAAG////ALNmEyizZxTzt2kU/7NnE/2zZxT/uGkU/7RnE3f///8A//8AAf///wD///8A////AP///wD///8A////AP///wD///8A////AKpVAAP///8AtGcUZr5sFf+0ZxP7tGYT/rRmE/64aRT/tGcTq7RkFDOxYhQN////AP///wD///8As2YaCrRmE++4aRT/s2cT/bNnFP+4aRT/tGcTd////wD//wAB////AP///wD///8A////AP///wD///8A////AP///wD///8AgIAAAv///wCuaw0TtGcUzMFuFf+zZxT/tGcT+7RmE/y7axT/s2cU9rNnE720ZxScs2YTh7NlFFuzZxI5s2cT7rhpFP+zZxP9s2cU/7hpFP+0ZxN3////AP//AAH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AgIAAAv///wC3aRYutGcUv7VnFP++bRX/u2sU/7VoFP+1ZxT/uWoU/79tFf+/bRX/s2cT/bRnFOezZhP8tGcU/7RmE/6zZxT/uGkU/7RnE3f///8A//8AAf///wD///8A////AAAAAAH/AP8BgACAAoAAgAKAAIACgACAAv8AAAH///8AgIAAAv///wCqgAAGsmgTYLRnE66zZhTRs2cU67RnFP/AbhX/vWwV/7prFP+6ahT/uGkU/7RnFP+zZxT/s2YU/7NnFP+4aRT/tGcTd////wD//wAB////AP///wCAAIACAAAAAf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ALhjDhKzZhQys2cVSrRlFFi1ZhR1tGcUmbNmFNG3aBT/tGcU/7NnFP+zZhT/s2cU/7hpFP+0ZxN3////AP//AAH///8AgACAAv///wD///8AiTtiDYgzZg+SN20OkjdtDogzZg+LLnQL////AJlmAAW7ZhEPu2YRD7RpDxGvYBAQtm0kB////wD///8A////AP///wD/AAABtm0SDrJnFH6zZhT/tGcU/7RmE/6zZxT/uGkU/7JnE3f///8A//8AAYAAgAL///8AizdpLo01bZSMNWu6jTZrvow2bL2MNWu8jDVrxo02a40AAP8Cs2cAJbRnFLC1ZhTAtGcTvLRnE7yzZhPKsmUVSf///wC5dBcLv4AABKpVAAP///8Av2oVDLRmE/C4aRT/s2cT/bNnFP+4aRT/tGcTd////wD//wAB////AI84aimMNWvYmTp1/5I4cP+SOHD/kjhw/5E3b/+XOnT/jDZr1XYAiQ3JeQATtGYW38JvFf+5ahT/uWoU/8ZxFv+0ZxOu////AP///wD///8A////AP///wCzZhJGtGcT+rZoFP+zZxP9s2cU/7dpFP+zZhN2////AP//AAH///8AjDVsh5g6dP+MNWz5jTZr/Iw1bP2MNWz9jDZs+402a/yNNWz4jTlqOv///wCzZheevWwV/7RnFPizZhP8s2YT/LdoFP+zZhOdsmUTNbFpFi63aRYutGYUS7NnE9O4aRT/tGYT/rRmE/6zZxT/tmgU/7RoEkf///8AgIAAAv///wCMNWyVlzp0/4w1a/qNNmz/jDZs/402a/yMNmz/kjdw/5M4cP+NOGpg////ALBmHC2zZxTivWwV/7NmE/y0ZhP8tGcU/7trFP+0ZhP+s2cU67RnE/C4aRT/uWkU/7RmE/6zZxP9tGcT+r1sFf+0ZxPSuGMOEv///wCAgAAC////AIs2a3WXOXT/jDVs/Yw2bPuMNmv8kjhw/5A3bv+NNmvKhy50QoAAgAaqcRwJ////ALZoE0K0ZhPgvWwV/7prFP+0ZhP+tGYT+rNnE/20ZxT/tGcU/7RnE/u0ZxP6tGYT/rhpFP+/bRX/tGcT5LZoFjv///8AgIAAAv///wD///8AkjFtFY02a7WVOXL/lTlz/5U5cv+MNmvcijNtaf///wDmmQAKtGgKhLRnFLexYxUk////ALFpFCeyaBaVsmgV2LZoFP/BbhX/vm0V/75sFP++bBT/vm0U/8FuFf+5ahT/s2cT4rRnFKa0ZBQz////AP8AAAH///8A////AP8A/wH///8AkjdtDow0a12NNWuGjDdsZnAAjxD///8AtGgNYrNnFt28axX/v20V/7VmFNG2ZhJG////AP///wDZQAAUsmUVSbJqGGq0ZhWEs2cihrJoJoexaCeJsWcrcrJnGk+0ZBMp////AP///wCAgAAC////AP///wD///8A////AIAAgAL///8A////AP///wD///8AumwANLNnF8e3aRX/u2sU/7NmE/y0ZhP6wG4V/7NnFcb/AAABQrVaHwDJaxP///8A////AP///wD///8A////AP///wD///8A////AP///wD/AAABgIAAAv///wD///8A////AP///wD///8A////AIAAgAKZM2YF////ALFmG0GzZhXyvWwU/7RnFP+zZhP8tGYU/rRnFP+zaBX/uV0ATQDXeRM6tF3bObRc1T20XZ9As1tXAAD/ASSC8z8khPBVJYLyYCyD7mlxcaoJ////AAAA/wH///8A////AP///wD///8A////AP///wD///8AAAAAAf///wD///8As2YUor1sFf+zZxP7tGYT/rRnFP+zZhP8vW0V/7NnFKT///8ANbZeiz2/Y/85uF//Psho/zm2WsAkbf8HJILzviWJ/f8lhvj/JYv//ySE81X///8AAKr/A////wD///8A////AP///wD///8A////AP///wD/AAAB////AP///wC0ZxS3uGkU/7NmE/yzZxT/s2YT/LdpFP+zZxXyyEUAJQDFZTA7tFz7ObZe/zi1Xfo6umH/ObVaryuA/wYjg/O/JYv//yOE8/gkh/r/I4Py0iCA7xD///8AAID/Av///wD///8A////AP///wD///8A////AP///wD///8A////ALRnE4jBbhX/tGcU/7NnE/21ZxT/v20V/7RmEYT///8AO7Rduzu+Yv84tV38OLRd/Tq8Yv84tVqyK4D/BiOC874liv//JIPz+yOD8/0liv//JIPzgP///wAA//8B////AP///wD///8A////AP///wD///8A////AICAAAL///8As2YRHrRmE8W2aBT/uGkU/7ZoFP+zaBa4/wAAAiu3YFg6tl7/OLZe/zi1Xf44tF39Orxi/zi2WbErgP8GI4LzviWL//8jg/P8I4Py/iSF9/8kg/LtJIbzKv///wAAgP8C////AP///wD///8A////AP///wD///8A////AP8AAAH///8AsWIUDbVnEmG1ZxODtGcTXJ+AIAj///8AObNdsjzBZP84tV37OLVd/zi0Xf06vGL/ObVZtCRt/wcjgvO+JYr//yOD8/wjg/P/I4Pz/SWJ/v8jhPN8////AAAA/wH///8A////AP///wD///8A////AP///wD///8A////AP8AAAH///8A////AP///wD///8A////AP///wA5tV2wPMBj/zi1Xfo4tV3/OLRd/Dq8Yv84tVqfAED/BCOD88Iliv//JIPz+yOD8/8jg/P9JIf6/ySD8o7///8A////AAAA/wH///8A////AP///wD///8A////AP///wD///8A////AP8AAAGqVQADqlUAA/+AAAKAv0AE////ADm1Xm87v2P/Obhf/zi1Xfs5uV//O71i/za1ZFn///8AJIXxlCaO//8jg/P8I4Pz/COD8v8liv//IoPyYf///wAAgIAC////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAAH///8AM7NmCjm2XY84tV33PMBj/zi1XvA5tFyCJLaSB////wAkie0cI4PzvSWL//8lif7/JYj8/yOD9KEagOYK////AAD//wH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAD/AAH///8A////ADm2XjE5tVxIObVbLf///wD///8AAKqqA////wAiiO4PIoL0WiSE9Hgig/FKK4D/Bv///wAA//8B////AP///wD///8A////AP/kAwH/+AAB/9AAAv+gAAL/oAAC/0A8Av9AQgL/YDIC/6AOAv+gAAL/0AAC4CgAAs//AAKwIHgCQAAiAoAAPgKAEAACgBAAAoAIAAWAhAALQQMAN7wAf8/IAABf2AQAX9gAAC/4CAAv6AAAF/QQABf78AAb/BAQF//QEBf/7Ggv</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.azet.sk/katalog/vyhladavanie/firmy/"
+ resultdomain="azet.sk"
+ rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="k" value=""/>
+</Url>
+<SearchForm>https://www.azet.sk/katalog/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/bing.xml b/comm/suite/components/search/searchplugins/bing.xml
new file mode 100644
index 0000000000..bb093f4bb5
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/bing.xml
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Bing</ShortName>
+<Description>Bing. Search by Microsoft.</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAICAAAAEACACoCAAAJgAAABAQAAABAAgAaAUAAM4IAAAoAAAAIAAAAEAAAAABAAgAAAAAAIAEAAAAAAAAAAAAAAABAAAAAAAAhIQMAI+PIwCXlzEAn59BAKioUwCurl8AtbVuAL6+fwDJyZMA0tKlANjYsgDe3r4A5eXMAOzs2QDy8uUA+fnzAP///wAAGi8AAC1QAAA/cAAAUZAAAGOwAAB2zwAAiPAAEZj/ADGm/wBRs/8AccH/AJHP/wCx3f8A0ev/AP///wAAAAAAACwvAABLUAAAaHAAAIaQAAClsAAAw88AAOHwABHv/wAx8f8AUfP/AHH1/wCR9/8Asfn/ANH7/wD///8AAAAAAAAvIQAAUDcAAHBMAACQYwAAsHkAAM+PAADwpgAR/7QAMf++AFH/yABx/9MAkf/cALH/5QDR//AA////AAAAAAAALw4AAFAYAABwIgAAkCwAALA2AADPQAAA8EoAEf9bADH/cQBR/4cAcf+dAJH/sgCx/8kA0f/fAP///wAAAAAAAi8AAARQAAAGcAAACJAAAAqwAAALzwAADvAAACD/EgA9/zEAW/9RAHn/cQCY/5EAtf+xANT/0QD///8AAAAAABQvAAAiUAAAMHAAAD2QAABMsAAAWc8AAGfwAAB4/xEAiv8xAJz/UQCu/3EAwP+RANL/sQDk/9EA////AAAAAAAmLwAAQFAAAFpwAAB0kAAAjrAAAKnPAADC8AAA0f8RANj/MQDe/1EA4/9xAOn/kQDv/7EA9v/RAP///wAAAAAALyYAAFBBAABwWwAAkHQAALCOAADPqQAA8MMAAP/SEQD/2DEA/91RAP/kcQD/6pEA//CxAP/20QD///8AAAAAAC8UAABQIgAAcDAAAJA+AACwTQAAz1sAAPBpAAD/eREA/4oxAP+dUQD/r3EA/8GRAP/SsQD/5dEA////AAAAAAAvAwAAUAQAAHAGAACQCQAAsAoAAM8MAADwDgAA/yASAP8+MQD/XFEA/3pxAP+XkQD/trEA/9TRAP///wAAAAAALwAOAFAAFwBwACEAkAArALAANgDPAEAA8ABJAP8RWgD/MXAA/1GGAP9xnAD/kbIA/7HIAP/R3wD///8AAAAAAC8AIABQADYAcABMAJAAYgCwAHgAzwCOAPAApAD/EbMA/zG+AP9RxwD/cdEA/5HcAP+x5QD/0fAA////AAAAAAAsAC8ASwBQAGkAcACHAJAApQCwAMQAzwDhAPAA8BH/APIx/wD0Uf8A9nH/APeR/wD5sf8A+9H/AP///wAAAAAAGwAvAC0AUAA/AHAAUgCQAGMAsAB2AM8AiADwAJkR/wCmMf8AtFH/AMJx/wDPkf8A3LH/AOvR/wD///8AAAAAAAgALwAOAFAAFQBwABsAkAAhALAAJgDPACwA8AA+Ef8AWDH/AHFR/wCMcf8AppH/AL+x/wDa0f8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBggCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAsQEA0HAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwkPEBAQEBALBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMEBAQEBAQEBAPCQMAAAAAAAAAAAAAAAAAAAAAAAAAAQ0QEBAQEBAQEBAQDQgBAAAAAAAAAAAAAAAAAAAAAAABDRAQEBAQEBAQEBAQEA0GAQAAAAAAAAAAAAAAAAAAAAENEBAQDgcLEBAQEBAQEBALBAAAAAAAAAAAAAAAAAAAAQ0QEBANAQEHDRAQEBAQEBAOCAAAAAAAAAAAAAAAAAABDRAQEA4BAAACCA4QEBAQEBANAQAAAAAAAAAAAAAAAAENEBAQDgEAAAAABAsQEBAQEA0BAAAAAAAAAAAAAAAAAQ0QEBAOAQAAAAABBw4QEBAQDQEAAAAAAAAAAAAAAAABDRAQEA4BAAABBw0QEBAQEBANAQAAAAAAAAAAAAAAAAENEBAQDgEAAAcQEBAQEBAQEA0BAAAAAAAAAAAAAAAAAQ0QEBAOAQABCxAQEBAQEBANBwAAAAAAAAAAAAAAAAABDRAQEA4BAAQPEBAQEA0IBAEAAAAAAAAAAAAAAAAAAAENEBAQDgEACRAQDQkGAQAAAAAAAAAAAAAAAAAAAAAAAQ0QEBAOAQILCgYCAAAAAAAAAAAAAAAAAAAAAAAAAAABDRAQEA4BAQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENEBAQDgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ0QEBAOAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRAQEA4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENEBAQDgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ0QEBAOAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRAQEA0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENEA0IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQcFAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAQAAAAIAAAAAEACAAAAAAAQAEAAAAAAAAAAAAAAAEAAAAAAACEhAAAjIwAAJKSFgCZmSgAqqpRALKyYgC+vnsAxsaKAM3NmQDU1KgA4ODBAOjozwDw8OEA+fnyAP///wD///8AAAAAAAAaLwAALVAAAD9wAABRkAAAY7AAAHbPAACI8AARmP8AMab/AFGz/wBxwf8Akc//ALHd/wDR6/8A////AAAAAAAALC8AAEtQAABocAAAhpAAAKWwAADDzwAA4fAAEe//ADHx/wBR8/8AcfX/AJH3/wCx+f8A0fv/AP///wAAAAAAAC8hAABQNwAAcEwAAJBjAACweQAAz48AAPCmABH/tAAx/74AUf/IAHH/0wCR/9wAsf/lANH/8AD///8AAAAAAAAvDgAAUBgAAHAiAACQLAAAsDYAAM9AAADwSgAR/1sAMf9xAFH/hwBx/50Akf+yALH/yQDR/98A////AAAAAAACLwAABFAAAAZwAAAIkAAACrAAAAvPAAAO8AAAIP8SAD3/MQBb/1EAef9xAJj/kQC1/7EA1P/RAP///wAAAAAAFC8AACJQAAAwcAAAPZAAAEywAABZzwAAZ/AAAHj/EQCK/zEAnP9RAK7/cQDA/5EA0v+xAOT/0QD///8AAAAAACYvAABAUAAAWnAAAHSQAACOsAAAqc8AAMLwAADR/xEA2P8xAN7/UQDj/3EA6f+RAO//sQD2/9EA////AAAAAAAvJgAAUEEAAHBbAACQdAAAsI4AAM+pAADwwwAA/9IRAP/YMQD/3VEA/+RxAP/qkQD/8LEA//bRAP///wAAAAAALxQAAFAiAABwMAAAkD4AALBNAADPWwAA8GkAAP95EQD/ijEA/51RAP+vcQD/wZEA/9KxAP/l0QD///8AAAAAAC8DAABQBAAAcAYAAJAJAACwCgAAzwwAAPAOAAD/IBIA/z4xAP9cUQD/enEA/5eRAP+2sQD/1NEA////AAAAAAAvAA4AUAAXAHAAIQCQACsAsAA2AM8AQADwAEkA/xFaAP8xcAD/UYYA/3GcAP+RsgD/scgA/9HfAP///wAAAAAALwAgAFAANgBwAEwAkABiALAAeADPAI4A8ACkAP8RswD/Mb4A/1HHAP9x0QD/kdwA/7HlAP/R8AD///8AAAAAACwALwBLAFAAaQBwAIcAkAClALAAxADPAOEA8ADwEf8A8jH/APRR/wD2cf8A95H/APmx/wD70f8A////AAAAAAAbAC8ALQBQAD8AcABSAJAAYwCwAHYAzwCIAPAAmRH/AKYx/wC0Uf8AwnH/AM+R/wDcsf8A69H/AP///wAAAAAACAAvAA4AUAAVAHAAGwCQACEAsAAmAM8ALADwAD4R/wBYMf8AcVH/AIxx/wCmkf8Av7H/ANrR/wD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMAAAAAAAAAAAAAAAACBgwMBwIAAAAAAAAAAAAABg4ODg4MBgIAAAAAAAAAAAYODQoODg4KBAAAAAAAAAAGDgwDBQsODg4FAAAAAAAABg4MAgADCg4OBgAAAAAAAAYODAICCg4ODgYAAAAAAAAGDgwCBg4OCwcCAAAAAAAABg4MAwkIBAIAAAAAAAAAAAYODAICAAAAAAAAAAAAAAAGDgwCAAAAAAAAAAAAAAAABg4MAgAAAAAAAAAAAAAAAAYMCAEAAAAAAAAAAAAAAAACAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</Image>
+<Url type="application/x-suggestions+json"
+ template="https://www.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="form" value="OSDJAS"/>
+ <Param name="language" value="{moz:locale}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.bing.com/search"
+ rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/bolcom-nl.xml b/comm/suite/components/search/searchplugins/bolcom-nl.xml
new file mode 100644
index 0000000000..4cbfa4ba1c
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/bolcom-nl.xml
@@ -0,0 +1,16 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>bol.com</ShortName>
+<Description>Zoeken bij bol.com</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAAAABMLAAATCwAAAAEAAAAAAAAAAAAA////ANJtQADot6AA3JJvAPXc0ADkpYYA7sq5ANiAVgD77egAzmMxAO3CqwDmr5IA4Jp8ANR4SgDcimMA+eTeAPHSwwD99/UA0Gk5APPWygD34NYA1nxQAOKggADos5gA7MaxAOCYdQD98+8A6r2oANRzRgDcjGkA2oheAP37+QDOZzUA5rWcAPvx6gD56OAA0nFCANZ4TgD12swA7si1AOq7pADkp4oA89TGAOCefQDiooQA0G09ANZ+VADYimAA3pZzAOrBrADtvacA//39AP359wDOZTMA/fXxANBnNwD78+wA0Gs7ANR1SAD34tgA9d7SANiCWAD12MoA3IxlANyOZwDuxrMA3ph3AOq/qgDquaIA6LWeAOazmgDvyLcA78q1AO/IswDswa0A4Jx7AOKkhgDOZTEA0Gc1APvv6ADQazkA+ejeANJxRADUdkoA1HhMANZ6TgDWflAA89jKANZ8UgDWflIA89bIANyMYwDuxrUA6LWcAOKgggDms5gA4qKCAOaxkgDkp4gA5q2SAOzBqwDrwKoA1HZIANR3SgDgnn4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQESP0IoPSABAQEBAQEBATljLgoKCk47AyABAQEBARBVCkxFAz42CgpcNwEBASMOCkwtJh5EMAoKCkEgAQFpCgpfTAoKCgoKCgoKCwFQOAoKOiclCgoKCgoKClk0SQoKCgoPBVMKCgoKCgo2JF4KNl5dZBoMAmUHKk8KCltHCkYfIUMFViIdNgZINgorSQoDPmAKOhkxEwoKMhY2UglPWlsYYh0cFzwvIV5oVjQBDQpNZhcpBAoISlgsCjMBAVBnCmEUVwoKCgoKCkAgAQEBFVQKVgoKCgoKCh8jAQEBAQEjX1EKCgoKJV41AQEBAQEBAQEbEURLJzUBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.bol.com/nl/s/algemeen/zoekresultaten/Ntt/{searchTerms}/Ntk/media_all/Nty/1/suggestedFor/{searchTerms}/N/0/Ne/0/search/true/searchType/qck/index.html"
+ resultdomain="bol.com">
+</Url>
+<SearchForm>https://www.bol.com/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/chambers-en-GB.xml b/comm/suite/components/search/searchplugins/chambers-en-GB.xml
new file mode 100644
index 0000000000..c0f4617a57
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/chambers-en-GB.xml
@@ -0,0 +1,19 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Chambers (UK)</ShortName>
+<Description>Chambers 21st Century Dictionary Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAICAAAAEAIAAsAwAAJgAAABAQAAABACAAPwIAAFIDAACJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAALzSURBVFiF7ddNqBVlHMfxz5nqHpN8y7DyqjTYG2RwQWKCW5uiRRD2CkGbIl1IOxdBGFKgQRgkWC2iRUG1bBG0iUyCopgKJSyNskYjKiz0Kr1crzanxfMYc+fOOZ5z4tQi/6vhef7z/L4zz8v/9/B/j9agL+RJmuAiLMHF8fk3HMWvOJ6VxalKfgttzGRlUQ4NkCfpJbgFt+J6LMeCOPjJKD6Fb7APu7EHV2MLHs/K4ouBAfIknY97sRFrMYaf8DE+ww9YEaHWRrAEJ5Cjg5uxLiuLXfXxzz+L+DiexAOYH7/0ZezEl1lZnKzkjuHKmLsBl+K22N3BqiaNrgB5kq7E87gjftEMnsXTWVmcqOdnZTGD/XmSPoH38AwmYnerm1bSRXwxtlfE4U1sbxKvgfwZf/V6fF7purwvgLjKN+KeSv9RPJeVxVQv8RrIHjyGI7FpvC8ArIkAY5W2D/Bpv+KVeBuvoMSKPEnbPQHi1z9o9oLpYFdWFn8Mqp6VxWm8hINYink9AYQtdLvZ23Ma+wcVr8S3eEc4tBacDeAGrK61TeH7YdXj6fcCNuHnen99a9xo9twTtt/0sAAR4gAONPX9/Qfi/F/2T4SGieoUtLHyvwTodlq1ceG/AdDBqYacRUKxGTnANL5ryJmHa0YOkJVFB4caclq4KU/SC0YKEOMj/N6QN2nu+TASgL2a9+s47oz2aqQAv+AtoXjU8x425FrIk7SVJ+lknqRX9ASI6+B1fN0wzlXYHL3CoDEpOKm7egLEOIgd5q6FFu7HtjxJl/WjmifpWJ6k6/Ci4B3fqOc0zmk0ok/hEXNrw2m8i634JFqxpvcn8JBgaA9hQ1YWe/sCiIMsxOYIMaeMCk5nt2BWvoptqwQbPiE45CV4H5uaxHsCRIg27sOjglM6ryGtI7hlwrF9ZsxjeE3wkV3LeT/3gpZQpO4WfOJ1WNwFZgY/Cn/mVXxYte5DAdRgFgoH0rVCjViOZcKtqBAW2j4crl7PzkWv+AufXtBu3nNrKQAAAABJRU5ErkJggolQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAgZJREFUOI2Nk7FrU1EUxn/n3PdSFYOCowX7koY4FAdbkUKFig5OIojF1UVwFbcKxsH/QF3sII4RRJwEhSpoadJYHSpYrEYDIlSDUCvSJu8eh7ynaQziN9zpnu9+3+/eC10ykDI4/k/ye0mHBQxgkWhfSzkpWM6QQLB1RJcs3nhohIUMwbuDrHwxEO0drmp0eVNZwuyEiCya2GMPP73ZNTSzrCr3Nmnn+6ao6NDNuhasqtH13rzPKGbnJXr0WvNWIToCUAanZVCAqsud24W7sEr7g3qZBqgxGhpojdFwguXvmDu9jm+YkwLAmdR9jsHtFY1evdVhq2h0O3XvTlBjNOykzF2s6NCVdE/QYRAeAkY2MAwa/ZCP8aIFYF5nMrR2AkyBDwBCZ7nA1FnnPtpb4nWpA3tlDVhL2FkngbEtJSmwG+BuHwMBsz/kDRKAJnyOMTwGJgeSBL5flV4pQBDryxhrtjvW48/JjwjYLJNBTwUnYMnpAqAGOsb7Bsj9AYQQ2RGqTQMc5Um7DM6SgwTiOQb3LrjoPGmFq6m7d6Uf+HoGQbGz8xrdeEMxOwWxJHWqQe6Yk+COh28AJVBJoqmAXyAqesfMgMlEFqVJ/BXjqSFqYvsFAi9cGo/rD0qgJfDdn0kFvDEZ1NzHU97kOGbDCHswVk1sVnzr1mE+NdO9fxFNu/5LvS/0F8COyR9a0q7OAAAAAElFTkSuQmCC</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.chambers.co.uk/search/"
+ resultdomain="chambers.co.uk">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="title" value="21st"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.chambers.co.uk/search/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/cnrtl-tlfi-fr.xml b/comm/suite/components/search/searchplugins/cnrtl-tlfi-fr.xml
new file mode 100644
index 0000000000..0379fe0f9a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/cnrtl-tlfi-fr.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Portail Lexical - CNRTL</ShortName>
+<Description>Centre National de Ressources Textuelles et Lexicales</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAAAAAAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgbUWgbUWfa0SZZECVXzyUXTuUXTuUXTuUXTuUXTuUXTuUXTuUXTuUXTuXYT6eakOgbUWfbESXYD2PVTiMUjiLUTeMUTeMUTeMUTeMUTeMUTeMUTeMUTeLUTePVTebZkGgbUWcaUPAmoTawbTZwbXZwbXZwbXZwbXZwbXZwbXZwbXZwbXZwbXPuKyaaE6aZUCgbUW8kXP639D94dL+4tP+4tP+4tP+4tP+4tP+4tP+4tP+4tP+4tPt08WhcFWbZ0KgbUXXs5z+49T+49T+5df+5tn+5tn+5tn+5tn+5tn+5tn+5tn+5tn74tSvgWGfbESgbUXat6D+49Ty1MS0h2qtflutf1utf1utf1utf1utf1utf1utf1utf1qjcUqgbUWgbUXat6H53tC3i3SXYT6gbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUXat6H12syrfmeXYT6gbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWhbkbat6H02suoe2SSWjmcZ0KdakOdakOdakOdakOdakOdakOdakOdakOea0SgbUWhbkbat6H12syrgW6JTC+OVDWQVzeQVzeQVzeQVzeQVzeQVzeQVzeQVzeUXTudaUOhbkbbuKH74NLXvrGhdmSZaVSaalWaalWaalWaalWaalV3TFNgOUtgOUt9STmaZUChbkbUsZn+5NX739Du08boz8Hoz8Hoz8HozsHozsHozsGFesdCQsxBQchtRVOaZkGgbUWzhmT85dj+5NX+49X+49X+49X+49X+49T+49T+49SPg9BDRNJCQ85yTFidaUOgbUWgbUW8knPnyrft0L/t0L/t0L/t0L/s0L/s0L/s0L+fjLpqYLtoXrqFXV6gbUWgbUWgbkagbUWgbUWhbkahbkahbkahbkagbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUWgbkagbUWgbUWhbkahbkahbkahbkagbUWgbUWgbUWgbUWgbUWgbUWgbUWgbUUAAH9fAADP/gAAz/4AAPr/AADC/gAAwP4AAE5eAAD//wAA//8AAMb+AAD//wAAwP4AAE5eAAD//wAA//8AAMz+</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://www.cnrtl.fr/utilities/OPEN">
+ <Param name="query" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.cnrtl.fr/lexicographie/{searchTerms}">
+</Url>
+<SearchForm>https://www.cnrtl.fr/lexicographie/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/drae.xml b/comm/suite/components/search/searchplugins/drae.xml
new file mode 100644
index 0000000000..504628fbf0
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/drae.xml
@@ -0,0 +1,16 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Diccionario RAE</ShortName>
+<Description>Real Academia Española. Diccionario Usual.</Description>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA////AP///wDP8eMAw83LAKyLlgCshYMAqpKMSIc/QsyeYl2PmVZPPZlpbgB1enQApru/AL/V2wD///8A////AP///wD///8Az/HjAMHJxgCzn6oAmlZUSIk5NrWjgoOhoWRfwH0jINKAKiefi315GMDc4gDL6fEA////AP///wD///8A////AOj//wDCvrkAjEdOTYkvMv2bYWCYoH+CP59wcUSrgX2Ahiwk/5xBRMGTkpIAfYuJAP///wD///8A////AP///wCOqqMAnpCMOZRFQ9WRW164p4KAfpRmYXaGNTWNnnNxa5dZWbKLIij/gVhVbzI0LQD///8A////AP///wD///8AZ0xMH3hGRoiiZ2eNoGNkrJdjYbqeb22Jk01Lo5xlY66bZGWinGBfx7KNjaF+bm1B////AP///wD///8A////ALB7fE+bVlqzhUJCgZRfYIOfZmipjlhXep58d0WbY2XDoXp6cJZWVG+EMDPTkTU6iP///wD///8A////AP///wClYGJjn2BmtotNS7SCRECnooeJTax9fIibV1OeoHl0ZZlobYKVT1Fvjk9Qs38vM4b///8A////AP///wD///8AS0g/CYA+NoqeR0XfmGhno66ZmG6woKFxiFZXsKmHhGujcXCNjT0+6pNUU7ikgnk4////AP///wD///8A////AKPHvwCdoZ8mhykw3Y9MTbW3o6BlmndyT3hRTlq1nJmJmm1xkZ5TVtWAQD+Td2BXAP///wD///8A////AP///wDg2+YAu6qtJIA5MraJOjXTkmBfY4lRVIiVV1eds5uYL49XV52FJCb3q25qY6V3eAH///8A////AP///wD///8AwtzIAKSjmiiDR0x7ml5fzX4iJuiXVFZynGtvaYUeIuGQPDzem1has4JWVEOeYWQA////AP///wD///8A////AM7/6wCgwLcAjFNRK8XEwwCffXyKhigozZRDSa11MSu9oWBfQJJ+fEBle3YAg316AP///wD///8A////AP///wDAz8cAuL65AMCyqwCpiIQLl2RkbY0+P9GHHBn9gkxKfIx3cQByfHQAYIiAAF+DewD///8A////AP///wD///8AmnN0AJ57egCikI4AlGRkrZE/Q+qWTEivi0BArJU2Pv6VbmuKi3p1AJF3cwCQd3MA////AP///wD///8A////AKmcngCpnJ4App6iALeemiqndW1jiU9Pm55ueZefXFlkv7OxItr6/wDT5u0A0+btAP///wD///8A////AP///wCxq7AAsauwAK6stADBrKcAsZWHAJyAg0ewpqVAtIN3ALmrqgDH9f8AxOTsAMTk7AD///8A////APw/AAD4DwAA8A8AAOAHAADAAwAAwAMAAMADAADAAwAA4AcAAOADAADgBwAA9A8AAPg/AAD4HwAA+B8AAP5/AAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAA////AP///wD///8A////AP///wDO8OQAy+HSALmopQDC2vIAs6GoAJ9gXgClcGgAro2IAMTc2SeHRkqQjU5TiaiOkC6MRTxF08K4I2hWTQBTFR0Az5SYAGRdWQB+Zl0A0fP2AL/c3gC/3N4A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AM7w5ADL4dIAuailAMLa8gCzoagAn2BeAKd1bQCxkI0AqIKCi3EIBv+HQ0TinG5qg4g1LLm9hnyAZ1ZMAFMXHwDRlpsAZl9cAH5lXADR8/YAv9zeAL/c3gD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AzvDkAMvh0gC5qKUAwtnyALKhpwCgYWAAr4iBD5NUUG2JLibvk1RWxaRybZyTQj7RkE5O1H8MCv9mDQu+eTc+V8qanC5lYVoFfmVdANDz9gC/3N4Av9zeAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDO8OQAyuDQALutqwDG5P0AtKOpA5xcX0eYUE2eexES0Z12dpbI6e4RrJGPepZMSrqvko1UmG9wn5A4MOuRMirkkkNB2Gw+Om6ahHoA0P//ANb3+QDX+PoA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AM7w5ADM5dYAtZuWAMHP5gC2qK85jTs1x4MoK/GUQ0HRmGhmYaeUlzOSZnJelmdlQ65wYmmwiY1un398hYgnFtp2Dw//m09TtLKHhWS0x8wEjaKjAIqhogD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A0/frANP36gCugHUAlWJtAZVaX5p6AAD+mGFlm6J0dYaYUEqgn1ZWlZldZICOUFByll1dcJpcX6CWU0qipYCBfZRQT8SAGxfnoVBVqZNtaQYAAQAAAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDQ9egAzevfCqh9eUF+JiOrfBwZ/513fJyXWGGZlltbnKyJf5aQcWwjhGJhUaFWT6xWVFcAooOKVZpiWaWfdnSPlkhHtHMICvmAFiHxkGBdlVJIQh1cU0oA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAHBgBfYWIeuIOLxpNJQtaMSkvQs724VpJVT6aPTU/Nu7e3X5JxdWBzLy2ilk1FwXszLZKea3J2qIyGdpVPR8N/HibFiEpTwoo4MtCXUlXgvaGeZ5GEeAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAGFTTjOhamWgnpGRLKVwcnKYVFWuhTk+36aAgI2phH9/o3Boo6Fuco6lcHJRfQUA/4g/Otqsi4dlpXt+loo0O9uje3aYn3FzoLORlIC0rrBDKScmHv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAuNzYAqnFumWkYGcJyODpZoVRXoZ1OUrKaXmCrnHV3kJVYVsWPQTO8rY6GYbCDh2qVTkqhjUpD4qR+hZGrjpBrjURBz5ZaVai1mJNloXB1hppoZLOtkItm////AP///wD///8A////AP///wD///8A////AP///wD///8A////AJh6eR6ea3OSj0BBupxQTYWhbWlxll5bl49KT7KjcXKFlmlroXoaIfmRam1ehnx7MbCAel6bam+2nmputKp8dlucZF2ihjg41JFFRsuLNTbvjkZD0o5NVWn///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Au5OQQZ5gY5aRTk2gt4mJa3FBPz9vLTOmsI2Lfa+ZlVybYWmXnVBNoZuIhD+WoJ0ApFpXjpNWUdaPX1+1nXqAa6Jzc1+tfnhDgTM2a3oyOXOQOTm5fBkeiv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCYU1NXjkVMxJRbZrOSYGivhD8/spA3NMqWZ2mBgU1Pe6+RkGS3mpNVoFNPqJxGQLCQR0OdsIqAWJVcYYSOdHucoHJ6bZtYVDyXXVptj1NfZ4Y7Od5uFB6a////AP///wD///8A////AP///wD///8A////AP///wD///8A////AK5ycV+cW1+gmGBfhY9VVZ+XXVe4jTMr6IFWV3ZzU1p1t5efVp9zb4eba2qyllhVw5hkZH6wj4k3oXRwdp1tZ2CcXmOjhyov345CQ7OfZGOPj1pXyH9JS33///8A////AP///wD///8A////AP///wD///8A////AP///wD///8ASkdGFKyIhHeYPj6ofh0a2aSCf5OISEO9pnVtorSPi3+oiopss5+dXKiXoH+YYmqrmllQnJ18fX2rmJJ2t5CJU4M3Pc2TVVbLm11YnIxHR7GmfHmQr3htU////wD///8A////AP///wD///8A////AP///wD///8A////AP///wAfKCIAgGpbFJA/O6qCGBj/om1qp5ZkZ5+MU1SysqOeWqiLjJTI4+ARq4B/h41BSNmZZ1+jl2JguqJ7hnKph4hzj0hGyaF4cYyMNjXUhiAl+ZhqZpOcfnMN////AP///wD///8A////AP///wD///8A////AP///wD///8A////AFJtXABab10CmW1qjKZdYrmLNTLJmFpgiIpESsOujo2HsI2Ne6R6doy0k4RMNTIzFnNnbEi1i4e3s5GMZJ1jZqGTXmGnpoCFdIk3Pem1bHCxRyEnSgAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A8P//AOz//wylrKooh1Zla30OEv9/KiLjlmZrhayGh4W2iX5WqYJ8WJNSVIhkJihskktGgr6spVqvoJ9/iDw+05NeYJ+nfoOGiCor4rV7d7JfQjg/DRMKAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDI1eAA09zoCItzdSN0MzGklkpO64c9NMKqeXGIgn56FoZWWIyzioxtc1lZK6Jub5a/kohiq5SQY7S2r0WRYGVmhTY0zYk9N89zBQbtnWFhiLeIghu/gX4A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AK6HiQCmfYI1p3NwZptoWGaWWFSxjzs5vZFLSruLYV5mjkxMhYs6PLKCVFSYjllgn5JMUZWMP0OMuKGaEKeIhEl9FxryhC0w1ZBNUZ+eWFdzrn+BUKpzdwX///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AyujVAMXk0gu1komOdERFUppsaVKKNDLsgS4264UoJe2IMDizizw+c6BkYZGZcG20hDEyq5BJUaqVSk6Jijw8wZJNSNeaYnCdllxblb91bapoTEc0BQ0IAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDQ/+cA4v//AIyWlw1uPkl/oV5lgZhpZ4+lhISkjkhHyHcLEv+HLC7DpGtvDbWenwyNQEGZhCUq93EAAP+ENDfwmV1bn6FeX3OucnGhWk1KUgkCAwAAAAAA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AM383wDk//kAdISFAGhgYQCsd3GBnWZncMbi6hayv78giDUxwIQpJPifc3BlpXaATIowO7OFKSX1qmxivplaVn2RRkuXgSklrJSMjUVFX2MAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Azv/iAOX//AB0hYYAYlJVALeZjAKlcWoIxMnEAMbX0AKOREPHdwkH9IczNdyVSlDWdAAA/3suLtx5e3cPkkM+AKNcYSGhnJUwd5KMBEVYWgAAAAAAAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDP/OEA4//3AIKVkwBza2sAwKufAMC6tQCvl4YAk05PVYg9Od2WUlR8iDk51YozMLGaTUKqdSAl5VdIQA5LPTYAp46PALi4swCOm5cATlRVAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AMixuADIsbgAybK5AMmzugDGr7gAwb69DJpuZWSRXGR0qYmFYpRTVpePT0rAjj4+t4wtLrKIKCnXmFBShZmekx9JW1sAHSAgAC8/PgAZJCYAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Aa05YAGpNVwBtUVsAa1BaAG1RXgCfgIJvkVNWt5NXWKyfYmSejTU3vo07N8eUTVC2iDAvwZhZW52JLzvetGln0lxFPyoQAAAAJQsIAC4SDQA9HhUAORsTAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAAAAAAAAAAAAAAAAAAACQ8MAJBpYo2NNTLojkBEypRDQ9B3BgL5k0tDp5xzcYaCLTHnjkZTu5BLTraWZWC3wJuYVc2mogDIoZ0AyKGdAMihnQDIoZ0A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAAAAAAAAAAAAAAAAAAAGBQQAr6erI7yak2qtj4xpsJGQg5E5PK2niotkm2txa4koL7GfYGCcpWdkeqp/e2nE4Ocbz/j/AMzv/ADM7/wAzO/8AMzv/AD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAAAAAAAAAAAAAAAAAAUAAAC0usQA0MnFAMWijwKXgn8yhlZTcqBsb6yleoCbpI6WcLSXkz+3eGoMqHFqAMTf4gDR9P8Azez2AM3s9gDN7PYAzez2AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAAAAAAAAAAAAAAAAAAABQEAALK1vgDJu7cA1KqXAHhhYABcbnAAh1NWlLCMjm686/oAxL+2ALV3aQCmbmkAw93hAM/y/gDM6vQAzOr0AMzq9ADM6vQA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAAAAAAAAAAAAAAAAAAAFAQAAsrW+AMm8twDSqpcAa1JRAISuugDFs7VJq6aYMb7i4gDBt7EAtHZnAKZuagDD3eEAz/L+AMzq9ADM6vQAzOr0AMzq9AD///8A////AP///wD///8A////AP/4H///+B///+AB//+AAf//gAB//wAAf/wAQD/8AAA//AAAH/wAAB/4AAAf+ACAH/gAAB/4AAAf+AAAH/wAAB/8AAA//AAAP/wAAD/8AAAf/AAAP/4AAH//gAD//5AI///wD///wAf//8AD///AA///wAP///AP///+f////n//</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://dle.rae.es/"
+ resultdomain="dle.rae.es"
+ rel="searchform">
+ <Param name="w" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-cs-CZ.xml b/comm/suite/components/search/searchplugins/duckduckgo-cs-CZ.xml
new file mode 100644
index 0000000000..115c842201
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-cs-CZ.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo nabízí vyhledávání na webu s respektem k vašemu soukromí</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="cz-cs"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="cz-cs"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-de-DE.xml b/comm/suite/components/search/searchplugins/duckduckgo-de-DE.xml
new file mode 100644
index 0000000000..12fb8984a6
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-de-DE.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="de-de"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="de-de"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-el-GR.xml b/comm/suite/components/search/searchplugins/duckduckgo-el-GR.xml
new file mode 100644
index 0000000000..cf8b2bfc9d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-el-GR.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="gr-el"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="gr-el"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-en-GB.xml b/comm/suite/components/search/searchplugins/duckduckgo-en-GB.xml
new file mode 100644
index 0000000000..fd7675f76a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-en-GB.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="uk-en"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="uk-en"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-en-US.xml b/comm/suite/components/search/searchplugins/duckduckgo-en-US.xml
new file mode 100644
index 0000000000..6248bee092
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-en-US.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="us-en"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="us-en"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-es-AR.xml b/comm/suite/components/search/searchplugins/duckduckgo-es-AR.xml
new file mode 100644
index 0000000000..b81bc494bf
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-es-AR.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="ar-es"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="ar-es"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-es-ES.xml b/comm/suite/components/search/searchplugins/duckduckgo-es-ES.xml
new file mode 100644
index 0000000000..1ad4d18187
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-es-ES.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="es-es"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="es-es"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-fi-FI.xml b/comm/suite/components/search/searchplugins/duckduckgo-fi-FI.xml
new file mode 100644
index 0000000000..13464cca94
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-fi-FI.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="fi-fi"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="fi-fi"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-fr-FR.xml b/comm/suite/components/search/searchplugins/duckduckgo-fr-FR.xml
new file mode 100644
index 0000000000..1246d22c7c
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-fr-FR.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="fr-fr"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="fr-fr"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-hu-HU.xml b/comm/suite/components/search/searchplugins/duckduckgo-hu-HU.xml
new file mode 100644
index 0000000000..f9687933c3
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-hu-HU.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="hu-hu"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="hu-hu"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-it-IT.xml b/comm/suite/components/search/searchplugins/duckduckgo-it-IT.xml
new file mode 100644
index 0000000000..c11e0535e6
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-it-IT.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="it-it"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="it-it"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-ja-JP.xml b/comm/suite/components/search/searchplugins/duckduckgo-ja-JP.xml
new file mode 100644
index 0000000000..efdd4ce471
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-ja-JP.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="jp-jp"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="jp-jp"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-nb-NO.xml b/comm/suite/components/search/searchplugins/duckduckgo-nb-NO.xml
new file mode 100644
index 0000000000..73178f134b
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-nb-NO.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="no-no"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="no-no"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-nl-NL.xml b/comm/suite/components/search/searchplugins/duckduckgo-nl-NL.xml
new file mode 100644
index 0000000000..d96adc43c9
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-nl-NL.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="nl-nl"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="nl-nl"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-pl-PL.xml b/comm/suite/components/search/searchplugins/duckduckgo-pl-PL.xml
new file mode 100644
index 0000000000..bb254d7093
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-pl-PL.xml
@@ -0,0 +1,27 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>Wyszukiwarka DuckDuckGo</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="pl-pl"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kd" value="-1"/>
+ <Param name="kg" value="p"/>
+ <Param name="kl" value="pl-pl"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-pt-BR.xml b/comm/suite/components/search/searchplugins/duckduckgo-pt-BR.xml
new file mode 100644
index 0000000000..c59346f866
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-pt-BR.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="br-pt"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="br-pt"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-pt-PT.xml b/comm/suite/components/search/searchplugins/duckduckgo-pt-PT.xml
new file mode 100644
index 0000000000..ffef17a097
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-pt-PT.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="pt-pt"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="pt-pt"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-ru-RU.xml b/comm/suite/components/search/searchplugins/duckduckgo-ru-RU.xml
new file mode 100644
index 0000000000..ed09b6d693
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-ru-RU.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>ПоиÑк через DuckDuckGo</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="ru-ru"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="ru-ru"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-sk-SK.xml b/comm/suite/components/search/searchplugins/duckduckgo-sk-SK.xml
new file mode 100644
index 0000000000..1645037fd0
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-sk-SK.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="sk-sk"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="sk-sk"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-sv-SE.xml b/comm/suite/components/search/searchplugins/duckduckgo-sv-SE.xml
new file mode 100644
index 0000000000..39ad76cb1f
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-sv-SE.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="se-sv"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="se-sv"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-zh-CN.xml b/comm/suite/components/search/searchplugins/duckduckgo-zh-CN.xml
new file mode 100644
index 0000000000..4e5ce956da
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-zh-CN.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="cn-zh"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="cn-zh"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo-zh-TW.xml b/comm/suite/components/search/searchplugins/duckduckgo-zh-TW.xml
new file mode 100644
index 0000000000..f4ff8417fc
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo-zh-TW.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="kl" value="tw-tzh"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="kl" value="tw-tzh"/>
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/duckduckgo.xml b/comm/suite/components/search/searchplugins/duckduckgo.xml
new file mode 100644
index 0000000000..f06dd8d852
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/duckduckgo.xml
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo (Global)</ShortName>
+<Description>DuckDuckGo provides a privacy-aware search engine for the web</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/duckduckgo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ac.duckduckgo.com/ac/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://duckduckgo.com/"
+ rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="t" value="seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-de.xml b/comm/suite/components/search/searchplugins/ebay-de.xml
new file mode 100644
index 0000000000..330fc3fbff
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-de.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (de)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/707-53477-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.de/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.de/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-en-GB.xml b/comm/suite/components/search/searchplugins/ebay-en-GB.xml
new file mode 100644
index 0000000000..2c1a3869eb
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-en-GB.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (uk)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/710-53481-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.co.uk/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.co.uk/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-es.xml b/comm/suite/components/search/searchplugins/ebay-es.xml
new file mode 100644
index 0000000000..3ed7b9cd9a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-es.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (es)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/1185-53479-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.es/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.es/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-fr.xml b/comm/suite/components/search/searchplugins/ebay-fr.xml
new file mode 100644
index 0000000000..43035ee7fe
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-fr.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (fr)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/709-53476-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.fr/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.fr/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-it.xml b/comm/suite/components/search/searchplugins/ebay-it.xml
new file mode 100644
index 0000000000..b8a946f4ff
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-it.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (it)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/724-53478-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.it/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.it/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay-nl.xml b/comm/suite/components/search/searchplugins/ebay-nl.xml
new file mode 100644
index 0000000000..8222538b76
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay-nl.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay (nl)</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/1346-53482-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.nl/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.nl/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/ebay.xml b/comm/suite/components/search/searchplugins/ebay.xml
new file mode 100644
index 0000000000..9aae3d2923
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/ebay.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<Description>eBay - Online auctions</Description>
+<Image width="16" height="16">resource://search-plugins/images/ebay.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://rover.ebay.com/rover/1/711-53200-19255-0/1"
+ resultdomain="ebay.com">
+ <Param name="ff3" value="4"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338192028"/>
+ <Param name="customid" value=""/>
+ <Param name="mpre" value="https://www.ebay.com/sch/{searchTerms}" />
+</Url>
+<SearchForm>https://www.ebay.com/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/google-jp.xml b/comm/suite/components/search/searchplugins/google-jp.xml
new file mode 100644
index 0000000000..29e5083514
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/google-jp.xml
@@ -0,0 +1,31 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<Description>Google Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/google.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://www.google.com/complete/search">
+ <Param name="client" value="firefox"/>
+ <Param name="hl" value="=ja"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="aq" value="t"/>
+ <Param name="hl" value="ja"/>
+ <MozParam name="client"
+ condition="defaultEngine"
+ trueValue="seamonkey-a"
+ falseValue="seamonkey"/>
+</Url>
+<SearchForm>https://www.google.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/google.xml b/comm/suite/components/search/searchplugins/google.xml
new file mode 100644
index 0000000000..758b5ee482
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/google.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<Description>Google Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/google.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://www.google.com/complete/search">
+ <Param name="client" value="firefox"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.google.com/search"
+ rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/heureka-cz.xml b/comm/suite/components/search/searchplugins/heureka-cz.xml
new file mode 100644
index 0000000000..1db516b89f
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/heureka-cz.xml
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Heureka</ShortName>
+<Description>Vyhledávání na Heureka.cz</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAATCwAAEwsAAAAAAAAAAAAA9fX13/X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX13/X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//769f/59/X/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/+/y9f8mjP7/lMP5//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/09fX//fn1/6XM+P97t/r/ir/6/9jm9v8ahf7/AHb//7fV+P/19fX/9fX1//X19f/19fX/9fX1//X19f/09fX/0eL2/w1///8AdP//NZP9/wJ5//8AdP//A3n//7HT+P/39vX/9fX1//X19f/19fX/9fX1//X19f/19fX/9PT1/w5///8wkf3/8vP1//Dz9f///vT/gbr6/wBy///I3vf/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/5fF+f8Ad///+ff1/8rf9/8Ab///nMj4///79f85lf3/TZ/8//r49f/19fX/9fX1//X19f/19fX/9fX1//X19f98t/r/N5T9//n39f8Nfv//HIb+/wR6///y9PX/g7v6/xOC////+/T/9fX1//X19f/19fX/9fX1//X19f/19fX/grr6/xqF/v/y9PX/Xqj7//359f8bhv7/W6b8/3Cx+v8pjf3//vr1//X19f/19fX/9fX1//X19f/19fX/9fX1/9fm9v8AcP//mcb5//759f/09fX/wdv3/wBz//8Lfv//g7v6//b19f/19fX/9fX1//X19f/19fX/9fX1//X19f/59/X/eLX6/wBw//9Ypfz/nsn5/3W0+v8Ge///LY/9//r49f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//z59f+s0Pj/MpH9/xOC//8giP7/f7n6//r49f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//359f//+/T///r1//b29f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX13/X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX13wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAA9fX1gPX19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19YD19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/5vH+f9Amf3/bbD7//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f+bx/n/BHr//wR6//8Eev//9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/m8f5/wR6//8Eev//BHr//xOC/v/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/X5vb/bbD7/zGR/f8Eev//BHr//wR6//9Amf3/m8f5/4y/+f8Eev//BHr//wR6//8Tgv7/yN73//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/bbD7/wR6//8Eev//BHr//wR6//8Eev//BHr//wR6//8Eev//BHr//wR6//8Eev//E4L+/8je9//19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/zGR/f8Eev//BHr//wR6//8iif7/Xqj7/324+v9Amf3/BHr//wR6//8Eev//BHr//xOC/v/I3vf/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f9tsPv/BHr//wR6//8Tgv7/m8f5//X19f/19fX/9fX1//X19f/m7fb/bbD7/wR6//8Eev//BHr//8je9//19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/udb4/wR6//8Eev//E4L+/8je9//19fX/9fX1/9fm9v+51vj/9fX1//X19f/19fX/m8f5/wR6//8Eev//QJn9//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f9eqPv/BHr//wR6//+51vj/9fX1//X19f/X5vb/E4L+/wR6//99uPr/9fX1//X19f/19fX/QJn9/wR6//8Eev//yN73//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/xOC/v8Eev//MZH9//X19f/19fX/9fX1/zGR/f8Eev//BHr//xOC/v/X5vb/9fX1//X19f+51vj/BHr//wR6//99uPr/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/BHr//wR6//9tsPv/9fX1//X19f9tsPv/BHr//wR6//8Eev//BHr//0+g/P/19fX/9fX1//X19f8Eev//BHr//0CZ/f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f8Eev//BHr//324+v/19fX/udb4/wR6//8Eev//MZH9/0+g/P8Eev//BHr//6rP+P/19fX/9fX1/xOC/v8Eev//QJn9//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/wR6//8Eev//bbD7//X19f+bx/n/BHr//xOC/v/I3vf/1+b2/xOC/v8Eev//Ion+/+bt9v/19fX/BHr//wR6//9PoPz/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/Ion+/wR6//8iif7/9fX1//X19f/I3vf/1+b2//X19f/19fX/jL/5/wR6//8Eev//bbD7/6rP+P8Eev//BHr//324+v/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f9tsPv/BHr//wR6//+Mv/n/9fX1//X19f/19fX/9fX1//X19f/19fX/MZH9/wR6//8Eev//MZH9/wR6//8Eev//1+b2//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/9fm9v8Eev//BHr//xOC/v+51vj/9fX1//X19f/19fX/9fX1//X19f+51vj/BHr//wR6//8Eev//BHr//0CZ/f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/4y/+f8Eev//BHr//wR6//99uPr/5u32//X19f/19fX/9fX1/7nW+P8iif7/BHr//wR6//8Tgv7/1+b2//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/16o+/8Eev//BHr//wR6//8Eev//MZH9/0CZ/f8Tgv7/BHr//wR6//8Eev//E4L+/8je9//19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/6rP+P8Tgv7/BHr//wR6//8Eev//BHr//wR6//8Eev//BHr//0CZ/f/X5vb/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1/+bt9v+bx/n/T6D8/0CZ/f9Amf3/QJn9/22w+//I3vf/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fWA9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1//X19f/19fX/9fX1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://www.heureka.cz/direct/firefox/autocompleter.php">
+ <Param name="query" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.heureka.cz/"
+ resultdomain="heureka.cz">
+ <Param name="h[fraze]" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.heureka.cz/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/hoepli.xml b/comm/suite/components/search/searchplugins/hoepli.xml
new file mode 100644
index 0000000000..3d26499bc4
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/hoepli.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Hoepli</ShortName>
+<Description>Dizionario della lingua italiana Hoepli</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAEZJREFUeNpi/P//PwMpgAVCMSYehDD+z7dHlsYUZ2IgEQxCDSxofLgvB4+TcMUDLZ2kQKqGBxQ6iZHU1EqyDQAAAAD//wMApAcRQrj9oIAAAAAASUVORK5CYII=</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.grandidizionari.it/Dizionario_Italiano/cerca.aspx"
+ resultdomain="hoepli.it">
+ <Param name="idD" value="1"/>
+ <Param name="utm_source" value="mozilla-firefox"/>
+ <Param name="query" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.grandidizionari.it/Dizionario_Italiano.aspx?idD=1</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/huuto-fi.xml b/comm/suite/components/search/searchplugins/huuto-fi.xml
new file mode 100644
index 0000000000..c20966b761
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/huuto-fi.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Huuto.net</ShortName>
+<Description>Hakukone Huuto.nettiin, suomalaiseen nettihuutokauppaan.</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQABMTEwAfHx8AISEhACkpKQAyMjIAPj4+AElJSQBVVVUAWVlZAGFhYQBubm4AfHx8AICAgACJiYkAmZmZAKSkpACsrKwAtbW1AL+/vwDExMQAy8vLANPT0wDc3NwA4uLiAO7u7gDw8PAA/v7+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AAAAAAAAAABkAAAAAAAAAAAAAAAAZEgwJDRUAAAAAAAAAAAAAFQYAAAkREhYAAAAAAAAAGA0AAAUPEhQXFQAAAAAAABMEAAQKDxIUGxcXAAAAABcKAAMKDQ8XGhscEBUAABkQAQcIDRARGRwaGhkLFgAXBwcKDRATFRcaFxESDw8YDQYKDREUGBsYEBANCRIIEwcKDREUGRscGwwDBgYJCAsKDBEVFxscHBwXAwABAgkJDREVGRscHBwcHBMDAQILDxMYGxscHBwbGhocEgYDDhcaGxsbHBsaGRkYFxcVExccHBoZGBYVFxgZABkZAAAAExMWGBgYGQAAAAAAAAAAAP7////wP///8A///+AH///gA///wAH//4AA//+AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//ACf//wH///8=</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.huuto.net/fi/showlist.php3">
+ <Param name="tits" value="{searchTerms}"/>
+ <Param name="status" value="N"/>
+ <Param name="sellstyle" value="k"/>
+ <Param name="order" value="R"/>
+ <Param name="cat" value="%25"/>
+ <Param name="lcat" value="X"/>
+ <Param name="start" value="0"/>
+ <Param name="num" value="50"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://www.huuto.net/fi/search_index.php3</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/images/amazon.ico b/comm/suite/components/search/searchplugins/images/amazon.ico
new file mode 100644
index 0000000000..1c39eaf8fe
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/amazon.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/duckduckgo.ico b/comm/suite/components/search/searchplugins/images/duckduckgo.ico
new file mode 100644
index 0000000000..dda80dfd88
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/duckduckgo.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/ebay.ico b/comm/suite/components/search/searchplugins/images/ebay.ico
new file mode 100644
index 0000000000..3af7a36484
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/ebay.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/google.ico b/comm/suite/components/search/searchplugins/images/google.ico
new file mode 100644
index 0000000000..82339b3b1d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/google.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/startpage.ico b/comm/suite/components/search/searchplugins/images/startpage.ico
new file mode 100644
index 0000000000..19991e7478
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/startpage.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/wikipedia.ico b/comm/suite/components/search/searchplugins/images/wikipedia.ico
new file mode 100644
index 0000000000..4314071e24
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/wikipedia.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/images/yahoo.ico b/comm/suite/components/search/searchplugins/images/yahoo.ico
new file mode 100644
index 0000000000..9bd1d9f7c0
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/images/yahoo.ico
Binary files differ
diff --git a/comm/suite/components/search/searchplugins/list.json b/comm/suite/components/search/searchplugins/list.json
new file mode 100644
index 0000000000..f08601f7de
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/list.json
@@ -0,0 +1,217 @@
+{
+ "default": {
+ "searchDefault": "DuckDuckGo",
+ "searchOrder": ["DuckDuckGo", "Startpage", "Google", "Yahoo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "google", "startpage", "wikipedia", "yahoo"
+ ]
+ },
+ "regionOverrides": {},
+ "locales": {
+ "en-US": {
+ "default": {
+ "visibleDefaultEngines": [
+ "amazon", "duckduckgo", "duckduckgo-en-US", "ebay", "google", "startpage", "wikipedia", "yahoo"
+ ]
+ }
+ },
+ "cs": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "Seznam", "DuckDuckGo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-cs-CZ", "google", "heureka-cz", "mapy-cz", "seznam-cz", "startpage", "wikipedia-cz"
+ ]
+ }
+ },
+ "de": {
+ "default": {
+ "visibleDefaultEngines": [
+ "amazon-de", "duckduckgo", "duckduckgo-de-DE", "ebay-de", "google", "startpage", "wikipedia-de", "yahoo-de"
+ ]
+ }
+ },
+ "el": {
+ "default": {
+ "searchOrder": ["DuckDuckGo", "Startpage", "Google"],
+ "visibleDefaultEngines": [
+ "amazon-en-GB", "duckduckgo", "duckduckgo-el-GR", "google", "startpage", "wikipedia-el"
+ ]
+ }
+ },
+ "en-GB": {
+ "default": {
+ "searchOrder": ["DuckDuckGo", "Startpage", "Google", "Yahoo.co.uk"],
+ "visibleDefaultEngines": [
+ "amazon-en-GB", "chambers-en-GB", "duckduckgo", "duckduckgo-en-GB", "ebay-en-GB", "google", "startpage", "wikipedia", "yahoo-en-GB"
+ ]
+ }
+ },
+ "es-AR": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo Argentina"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-es-AR", "google", "startpage", "wikipedia-es", "yahoo-ar"
+ ]
+ }
+ },
+ "es-ES": {
+ "default": {
+ "visibleDefaultEngines": [
+ "amazon-es", "drae", "duckduckgo", "duckduckgo-es-ES", "ebay-es", "google", "startpage", "wikipedia-es", "yahoo-es"
+ ]
+ }
+ },
+ "fi": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-fi-FI", "google", "huuto-fi", "startpage", "wikipedia-fi", "yahoo-fi"
+ ]
+ }
+ },
+ "fr": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Startpage", "Yahoo"],
+ "visibleDefaultEngines": [
+ "amazon-fr", "cnrtl-tlfi-fr", "duckduckgo", "duckduckgo-fr-FR", "ebay-fr", "google", "startpage", "wikipedia-fr", "yahoo-fr"
+ ]
+ }
+ },
+ "hu": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-hu-HU", "google", "startpage", "vatera", "wikipedia-hu"
+ ]
+ }
+ },
+ "it": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo"],
+ "visibleDefaultEngines": [
+ "amazon-it", "bing", "duckduckgo", "duckduckgo-it-IT", "ebay-it", "google", "hoepli", "startpage", "wikipedia-it", "yahoo-it"
+ ]
+ }
+ },
+ "ja-JP-macos": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo! JAPAN"],
+ "visibleDefaultEngines": [
+ "amazon-jp", "duckduckgo", "duckduckgo-ja-JP", "google-jp", "startpage", "wikipedia-ja", "yahoo-jp"
+ ]
+ }
+ },
+ "ja": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo! JAPAN"],
+ "visibleDefaultEngines": [
+ "amazon-jp", "duckduckgo", "duckduckgo-ja-JP", "google-jp", "startpage", "wikipedia-ja", "yahoo-jp"
+ ]
+ }
+ },
+ "ka": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo (Global)"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "google", "startpage", "wikipedia-ka"
+ ]
+ }
+ },
+ "nb-NO": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Startpage", "Yahoo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-nb-NO", "google", "startpage", "wikipedia-NO", "yahoo-NO"
+ ]
+ }
+ },
+ "nl": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Startpage", "Yahoo"],
+ "visibleDefaultEngines": [
+ "bolcom-nl", "duckduckgo", "duckduckgo-nl-NL", "ebay-nl", "google", "marktplaats-nl", "startpage", "wikipedia-nl", "yahoo-nl"
+ ]
+ }
+ },
+ "pl": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "Startpage", "DuckDuckGo"],
+ "visibleDefaultEngines": [
+ "allegro-pl", "duckduckgo", "duckduckgo-pl-PL", "google", "pwn-pl", "startpage-pl", "wikipedia-pl", "wolnelektury-pl"
+ ]
+ }
+ },
+ "pt-BR": {
+ "default": {
+ "searchDefault": "Google",
+ "visibleDefaultEngines": [
+ "amazon-br", "bing", "duckduckgo", "duckduckgo-pt-BR", "google", "startpage", "yahoo-br", "wikipedia-pt"
+ ]
+ }
+ },
+ "pt-PT": {
+ "default": {
+ "searchOrder": ["DuckDuckGo", "Startpage", "Google", "SAPO", "Priberam", "Wikipedia (pt)"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-pt-PT", "google", "priberam", "sapo", "startpage", "wikipedia-pt"
+ ]
+ }
+ },
+ "ru": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-ru-RU", "google", "startpage", "wikipedia-ru"
+ ]
+ }
+ },
+ "sk": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "Azet", "DuckDuckGo"],
+ "visibleDefaultEngines": [
+ "atlas-sk", "azet-sk", "duckduckgo", "duckduckgo-sk-SK", "google", "startpage", "wikipedia-sk", "zoznam-sk"
+ ]
+ }
+ },
+ "sv-SE": {
+ "default": {
+ "searchOrder": ["DuckDuckGo", "Startpage", "Google", "Bing"],
+ "visibleDefaultEngines": [
+ "bing", "duckduckgo", "duckduckgo-sv-SE", "google", "prisjakt-sv-SE", "startpage", "tyda-sv-SE", "wikipedia-sv-SE", "yahoo-sv-SE"
+ ]
+ }
+ },
+ "zh-CN": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo!"],
+ "visibleDefaultEngines": [
+ "amazon-zh-CN", "duckduckgo", "duckduckgo-zh-CN", "google", "startpage", "wikipedia-zh-CN", "yahoo-zh-CN"
+ ]
+ }
+ },
+ "zh-TW": {
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "DuckDuckGo", "Yahoo!"],
+ "visibleDefaultEngines": [
+ "duckduckgo", "duckduckgo-zh-TW", "google", "startpage", "wikipedia-zh-TW", "yahoo-bid-zh-TW", "yahoo-zh-TW"
+ ]
+ }
+ }
+ }
+}
diff --git a/comm/suite/components/search/searchplugins/mapy-cz.xml b/comm/suite/components/search/searchplugins/mapy-cz.xml
new file mode 100644
index 0000000000..7d2fb59615
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/mapy-cz.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Mapy.cz</ShortName>
+<Description>Vyhledávání na Mapy.cz</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAICAAAAEAIACvBAAAJgAAABAQAAABACAAPwIAANUEAACJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAR2SURBVFiFrZdbbFRVFIa/OZ1pO+20ldILLa3FtJpgSbmJLQRiFE3KRZMqidYHfTEYCMTwYo0PxCcDvkCMEmM0UR/EBy9EoEGUEIlGIKXBYqPSYrlVpJUO08tM22nn92HOnOlczplBWMlK9tn73/9aZ52191nLtftUK1mKD9gEPA4sAx4ASsy1ADAAnAdOAkeB8WxI3VlgGoAOoB0otMFUmNoMvApMAAeBvUC/E7khCRvNl7RHUq+kVyQVOmCTtdDc0ytpr8mVFmvYOFYP/GK+eW4WUbKTXOB1k6shHcAtKXluBdAJVN6F4WRZBvwEbAS65y4kR6ABOHaPjcek0uROiMTcCHiBL4HydLu3LO5gaeUT+EM3+OaPfQwEfv0/TpQDXwEtQAjAAGHqW6Clc54TdEn5Oly4KPVW075kNwXukrS4LLTJtAUIQwKJeold5jitTs4Erdco8BTx2KIXbbESNFe30Vq/DeRKt75LokGKR+ANkMfJ84hmE2K5qmqDbRQWz1/D5ge3sbb2WVbXtKXDeEybGJKKJLVnOtu5OfkJDuTm5LO6ti0FV+17iOcb38TlcgHwaPVmO84XJBUZkjZmc8l4jLyUjGpZ+DRuI8/ClOYv5OWlb+PJiWPLCmucLquNBmh9psTxeUrJMVJv7QJPMY9UbQDEfO9Ctq7chy/3vgSMC5cT93pDoskpmSQo89ZYhJLoGTplPa+7fwsV3kVsXbmf4rz5KU5GFHHibjIk1WcKf1Vx/O4YDl7l29/fJTw7DcA8bxU7Wz60jE9MBzh68QMLPzUTdOKuN0DFmT5Bbclii3BwrI+x6WF+vva1Nec2PAD4Q//w/tntDI7+aa1Nzow7cRcbUoRM2jBvuUV4xf8bUoQfBz5nenbSmr8a6OW9M9v5N3gVFzlzHJhw5DYkjTqFf0FhA0V5pRbhZf8FJDE+PcIPlz4D4Mz1Ixw4u4PRqaGUIxt1wJZ/1A26BJSlZI8pjZVrrXEoPMaN8Ytm+ODkwKecvnaI0EwgaVf8Dxv/BGnlkltSD9FKJq0sX/CkNb4wdIrZSOKNGAzfTtkzYyYoQMiMgI30GBIn7I5JTdHDVPjqLHTX9e8cj2tMQ+F4OTgZDjphT7hBnURruJR6b01dmzW+OX6Zv0a6cAinJbeCg3EH7D9BEOg0JI1J+iI5QbzuElZUP2Whv+//xLxUMteEgakh/h7tA+CKv9cOd1DSWKwg2QO8BHhiBptrn8FtRMvBvlvn6B48hrJ4+5gcOL2TCl8dA/7z6ZbDRCtm3GZ4+oF9RAvIqAM1m+i/1c25weN0DR5GRLI2DjARHmHAP2K3vB/oA3C9dnhVbNILnAaa7sjSnUsPsJpoDiSUZCHQc6DhTFfzXeiwaSMYm3NLCaHtB1q592U5wE2iZXlCp5SuMekG1hIN1b2SHpOzO3nBrjXrl9Qs6R1J4WyOno2GTY5mkzMFY9eaAUwSbc0agY8wkyZLCQEfm3s7TK604tpxaFm2pD5gM4nteaz+uk1ie36ELNvz/wAQuz6qTTbcEAAAAABJRU5ErkJggolQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAgZJREFUOI2Nk01I1FEUxX/z/I9OVJKKEZmCFX0YmGSUSFARYxa4Mlq06gukUISE3ERtA1sVJrUQqm3UQhd9uDIiCowIlQgiZiwKRXIxmc0097T5P/s7zKILB+5977z7zuHdF7s63k5B7ADOAEeBWiAGpIHnwD1gMkoOJPm8DLgOdANBQdMKYDdwCRgE+oBctEEpMAIkC+UUhAN6gO1AB5B1kpA0ICkZ5lzcM0jf/gesDirxawVok3RDEk5Sg6TuKGFVvJx1ifUkN59ecfBA7UlaN3X6+oKkBidZl2ROMjx8NG04wtp4FZLRWH2Y9i3nOb61y/MCyc4Gkh2KmowRY01pBaY8JbGA1rpOUj/ec2LX5WVO5JI2Z6Z6M+FRWVZD4OK8/TYGQEtNB6carzG/+JXZTAqACL/OSfmslMejvrIJgPTCFPffXaG0JEF6YZqhNz1kcguhgn/8QLIZoMpr2lndAsCn+QnmFlMMT/Tzcf41eeWKWUg7M417SQlXzraqfQB8z3wmb8bU7Aty+Sxm4lcuU2jhmZPsrmQmGc01xwhcnIeTA0RfxWPu58yyAsn+SDbsJJuSbFAymjcmeZl+zKv0o6INRj/cov/JQV/fkWw61juy14/y6H+Mso+xcJSX/ABlJeuQ7GYoraiC0OrtkLsk2Yrf+BvoBYaAc6GaunDvC/CUIt/5L8Hdoep4zHF5AAAAAElFTkSuQmCC</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.mapy.cz/"
+ resultdomain="mapy.cz">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="sourceid" value="Searchmodule_3"/>
+</Url>
+<SearchForm>https://www.mapy.cz/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/marktplaats-nl.xml b/comm/suite/components/search/searchplugins/marktplaats-nl.xml
new file mode 100644
index 0000000000..16ef62c52a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/marktplaats-nl.xml
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Marktplaats.nl</ShortName>
+<Description>Zoeken in alle categorieën op Marktplaats.nl</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAABC0lEQVR42rSSoU7DUBSGv42FNKHiBtMrEE2KwnRysoIwP4UZ6SOQ9AW2B5hFN+BRCDZVSQKi9WtSeVFUVCwEgehdb0p2xQTHnfz/d3POf+6gyhYcU0OOrBGQpBSV7lcxod9zFBVJChBIZhOGrWk61rKq/z75mAFIwfKW6Xg/0s0eeH6j2Rm3qnXrCaTo7yAFUlAqSmWAp6zX9gBP4AmAh1cjr3PuIntKrVYq1rl2S8GZYwcCSSABPmuATc484lLaAdfRcptMUZn0rIebTXAdgOvFAfcBoJsKmEd2YKtodjryVWyC7k7ZqSf3cZSkvHzw1fC+5eqCcxcg9Al9kpRNzvePVk9HDP79t/4OALMxUpBz00CfAAAAAElFTkSuQmCC</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.marktplaats.nl/z.html"
+ resultdomain="marktplaats.nl">
+ <Param name="query" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.marktplaats.nl</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/priberam.xml b/comm/suite/components/search/searchplugins/priberam.xml
new file mode 100644
index 0000000000..edc2922690
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/priberam.xml
@@ -0,0 +1,16 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Priberam</ShortName>
+<Description>Dicionário Priberam</Description>
+<InputEncoding>ISO-8859-15</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAAHnlligAAAABGdBTUEAANbY1E9YMgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKoSURBVHjaYvz7+dv3r98Y/n7+7u/jCxBAjH8/fmVgYGBat23T1+/fAQKI8e+X7wz//xeVlykqKjB8fP4qJj4GqHzR3LkAAQRV9+zlCylxCSYGJkYgunXnNohx78qNv5++fXn9Dkgy/Prw5YVwLQsLyy2hSoAAYgRaATTt4ZPHX799+fTh06Mnj4P9g5iZmZgYGBmASExEZPfevdHJCbdu3548c+rd+3eZGMCAk5Mz7b/Zg4cP33x8X5Cd+2vHbcaf7z4dOXbUwdaOARUABBDUXUDw79+/L9++MTIx8nJxA53FwsDI+Ofvn2vXr/Fy85y7fOnvnz9hQSEga4Ee+vPpK5D8+eGzqpLy8wePbpy7CPQ4C8g4Rsblq1ZIiEncvnd36vTpvLw8xaqFDECPyUhJA0kgkpeTB5K/P3wBeQ5oCAjdexuiYsvEzARkvz11++f91yzfvn27dv36rvDepcXtC7gvGttarbAq+asuzbh/yw4zU1MODg5UXzACBBg45P/9h4uAfPPly+9fv779+M7EzHzt5g0JMXFdLW2oNBMjOKjAwfjh44cLFy98+faVh49XWFRk3qIF2iaGl65cPnv+7NUb1yBqQFoYgKaDkQC/gIG+AR8P77nz58urq46fPPX/37/TZ86EBoW8ffcut6jg/qOHIB1AJ0E8ePX0ufrK6o3r1v/6+BkYdNaWlnp6ekDG7UtXwoNDLp89B1IGdD9Iw6dvpw4e2bBi9a8PnyEBdvPqVSkpKaBxU/snQkSg6Mt3aAKB+/hj+a6fM/dfZfslvDu+rreN+R9Dfnbu5YuXzk/a/PvZ28qwPMbbV65NmTFDVETk04cPMioKroY2CtxibLICDGzMwGA4cPSwvZWNoIAgw6+/fx99YFYUZPz39cd/pGDFD4CJBwBLx4gNwqkZawAAAABJRU5ErkJggg==</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.priberam.pt/dlpo/firefox.aspx">
+ <Param name="pal" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.priberam.pt/dlpo/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/prisjakt-sv-SE.xml b/comm/suite/components/search/searchplugins/prisjakt-sv-SE.xml
new file mode 100644
index 0000000000..ef9a339196
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/prisjakt-sv-SE.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Prisjakt</ShortName>
+<Description>Prisjakt - jämför priser och produkter</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAQAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKieMACommAAqJaMAKyauAC0prwBMSLsAZF%2FFAH16zQBwa9EAi4nTALu55QCxrvYA7Ov4AP%2F%2F%2FwBwADIAaQBjAG8AAADAHiQAfO8SAEwAAAAoJYAAjAAAAOS%2F9QBxGuYAjAAAAJgFAgAA8BIAGAAAAHDvEgDI7xIA4xq%2BAIwAAACYBQIAAPASABgAAAAAAAAA2T7GABg%2FxgDkCQUAVAAAAGDyEgChUcYA5AkFAFQAAABg8hIAAAAAAMzyEgBghgcAHjvnAPc65wDg8hIAWAcXAKzvEgCw7xIAgP4SAAlI6QBYMOgA%2F%2F%2F%2FAB475wAbrQEAYIYHAODyEgBYBxcABACkAAAApAD%2F%2F%2F8AsAgAAAAAQAAEAKQAZAAAAGIAbQBwADIAaQBjAG8ALgBlAHgAZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHQAAAAAAAAAAAAAAAAAAAACAAAAXPESALgLpADoC8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6AvCAAAAAAAAAAAAAAAAAP%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAX6bnAAAAAAAAAAAAAAAAAAAAAAAoLxQAAAAAAAkOAgACDgIADQAAAADw%2FQAA4P0AAg4CAAkOAgAAAAAA9gvCAODyEgBSAAAAAAAAACTVpAAAAAAA%2F%2F%2F%2FAFzxEgBq8RIAXPESAMzx5wAEwPUARPESAAAAFACoRPkARQAAAHgTFAAAABQAoCAUABzxEgAg8RIAZPMSAPCI%2BgBSAAAAAAAAAJDWpAAAAAAAOor1AAAAAAAA7P0AAAAAAAAAAABwADIAaQBjAG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDO8YApTbGAM0JAQAPAIUAAAAAAJDWpADNCQEAAQAAAAAAAAAAAAAAFTbGAM0JAQAAAAEAwU1BAM0JAQAAAwAABMD1AFik5wB0AAAAAAAAAOjyEgB0AAAAAAAAAAAAAAD%2F%2F%2F8AAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAKC8UAAAAAABkxfUAqfHnAIwAAAAAAAAAAAAAAAAAAAB88hIAAADdAAADAAAAAAAAyfHnAAADAAAAAN0AjAAAAAAAAAAAAwAAAQAYAAAAAABw8hIAAAAAAKqb9QCzm%2FUA4PUSACQAAgAA7P0AEW5AAAUAAAAkAAIAAPD9AJzyEgACAAAAQKP1AJACAgD5m%2FUA4En8ACOj9QAro%2FUAAAAAAAgCAACgIBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAAAAAAAACwgGFQYICwAAAAAAAAANCgUEBAEEBAUKDQAAAAAACgQEBwwCBAQEBAoAAAAACwUEBAQOAwQEBAQFCwAAAAgEBAwODg4OCQQEBAgAAAAGBAQEBAQEBA4EBAQGAAAVFQECAwwODg4MAwIBFRUAAAYEBAQOBAMEBAQEBAYAAAAIBAQECQ4ODg4MBAQIAAAACwUEBAQEAw4EBAQFCwAAAAAKBAQEBAIMBwQECgAAAAAADQoFBAQBBAQFCg0AAAAAAAAACwgGFQYICwAAAAAAAAAAAAAAABUAAAAAAAAAAP%2F%2FRgD%2B%2FwAA8B8AAMAHAADABwAAgAMAFYADAACAAwAAAAEAAIADBhWAAwsAgAMAAMAHDQrABwQB8B8FCv7%2FAAA%3D</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://www.prisjakt.nu/plugins/opensearch/suggestions.php">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://www.prisjakt.nu/supersearch.php"
+ resultdomain="prisjakt.nu">
+ <Param name="s" value="{searchTerms}"/>
+ <Param name="r" value="1"/>
+ <Param name="e" value="utf8"/>
+ <Param name="ref" value="155"/>
+</Url>
+<SearchForm>https://www.prisjakt.nu/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/pwn-pl.xml b/comm/suite/components/search/searchplugins/pwn-pl.xml
new file mode 100644
index 0000000000..b1b2ff6ff6
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/pwn-pl.xml
@@ -0,0 +1,14 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Encyklopedia PWN</ShortName>
+<Description>Wyszukiwanie w Encyklopedii PWN</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAI9UExURQAAAB9YryBWrgmU3SBWrh9YryBWriBWriBWri0zkwmU3Q2L1hCBzg2L1hCBzhR3xiNOqB9YsBxitydFoRhsvxR3xhxitxhsvyNOqB9YsCdEoCo7mi01lSs6mSBWriBWri00lCBWrgqT3CBWriBWrgmU3Ss6mQqR2g2L1iNOqCdEoC0zkyBWriBWridEoCBXryBWris6mSBWri0zlA2K1RCBzh9YsB9YryNOqC0zlCBWrhR3xhCBzhxitxhsvyBWriBWrh9YsBR3xhxitxhsvyBWri0zkyBWriBWriBWriwykx9YryBWriwykyBWriBWriBWri0zkyBXryBWrh9YryBWriBWriBWrisykyBWriBWriBWriBWriBWriBWriBWrh9YryBWriBWriBWriBWriBWriBWriBWriBWriBWriBWriBWrgmU3Q2L1idEoBCBziJNpwWS3Cs6mW6UzBR2xiNOqB9YrxVrvhphtxCAzgyL1hd4xyFZsEWJzD96whl6yBSP1w1/zQiT3Ehrtg6AzguK1SVCn8XQ6B1Xr3eq2hJ1xmiQynmi1D2Z2F2q3hB0xSSX2l605TNbru31+93l8nmKw5XC5io6mBltv4W/5mi553uy3yhFoRdfthp6yODn87LO6svh8iZputTc7is6mFyFxTh0v4Kp1+rv+Ojv+EqNzTuCyLHN6Xyk1YK34ZLD55il0Z3A44an1mKLyBlgthZrvs/U6S08mtrg8DJAnLe/3jNBnGd6vCw7mTZEnjWei8gAAABsdFJOUwBNVQgHMwpZVGO9vQgIvQgICAgICL29vb29u4AO+w0uT3y1kwz+VRX4/VUhCZf9LR/9hWNK/f1gVWQyVVVVVV4gVf39/QhifaW+Y1OvZJK0lWRiMUKef3NlTZSZerKsFimuImocX3IVYkMpUosemJAAAAEISURBVBjTY2BQV1LNaSoszAEBLmYGBgYTjdzWCbMmNlTl5uZy8wIFbExbemeun9zXWJmfz8cDFLB26pq2bt7C2f311SWi/EABO5etGxesWrN6RmdtubgIUMDWecvylWuXzV3UU1chJgQUcDTrWLqieNPm7uKaMklBoICFZsGkxfN3TGlvLiiVEAAKaOnlzdmwa8+2qW15eVLCQAE1w6IlO3fv3bd9epGsNJDPoG1s6eblyxkRyMmpIAMS0Ddn9GZkDWFICGJlkGcBCrAbWXn6RIbGs0f5O3DIgQR0mdzDwuOSM5liAuxVQALKBq7BsWmJKRzp0R46QAEORQYGtoykVJZsliw/NgYAqJdNo4WkE2YAAAAASUVORK5CYII=</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://encyklopedia.pwn.pl/szukaj/{searchTerms}"/>
+<SearchForm>https://encyklopedia.pwn.pl/szukaj/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/sapo.xml b/comm/suite/components/search/searchplugins/sapo.xml
new file mode 100644
index 0000000000..a45e47b84a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/sapo.xml
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>SAPO</ShortName>
+<Description>Pesquisa SAPO</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABXmHT/EnM8/wd6L/8EdRj/BHUY/wd6L/8Sczz/WJh0/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACt+UP8FaTL/B4I8/wmoTv8JO03/Ew62/xMOtv8JME7/CahO/weCPP8GajL/K35Q/wAAAAAAAAAAAAAAABJzPP8Hgjz/Cq5P/wm6Vv8BIwj/DAB8/xcG3v8ZB+b/DACA/wEUBf8JtVT/CahO/weCPP8Sczz/AAAAAECTZP8IkEP/Ca5Q/wquT/8CHA3/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AhwN/wmuUP8Krk//CJBD/0OUZv8IkEP/CahO/wquT/8FYCz/CJBD/wd3N/8DIA//AQwE/wEMBP8CHA3/BWAs/wiQQ/8FYCz/CahO/wquT/8HjkL/CahO/wSIPP8Hdzf/BqQ+/wakPv8GpD7/BqQ+/wakPv8GpD7/BqQ+/wakPv8GpD7/BqQ+/wN9N/8DfTf/CahO/1fAhv8Vm0//Ao82/wOmR/8Pok//BqQ+/wSeNP8Dpkf/A6ZH/wSeNP8GpD7/D6JP/wOmR/8Cjzb/FZtP/13Ejf8AAAAAa8eW/13Ejf8TqFP/AZgu/x2Zfv8xxdP/D6JP/w+iT/8xxdP/HZl+/wGXLP8Sp1L/aMiV/3fLof8AAAAAAAAAAAAAAACc1rv/u+rN/3G0nP8E8Pf/A/D3/xfNz/8Xzc//A/D3/wPw9/9ws5v/wezS/5zWu/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGQkH/BPD3/wAkJv8D8Pf/A/D3/wAsLv8D8Pf/RkJA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARkJB/0vw9v8E8Pf/NrjA/zi1vf8D8Pf/SPH3/0ZCQP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGQkH/etfY/0ZCQf9GQkH/edbY/0ZCQP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEZCQf8AAAAAAAAAAEZCQf9+fHz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////////wD///wAP//4AB//8AAP//AAD//wAA//8AAP//gAH//8AD///wD///8A////gf///9n////////w==</Image>
+<!-- Suggestions disabled as SSL is not available as at 23 Apr 2020
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://pesquisa.sapo.pt/livesapo">
+ <Param name="q" value="{searchTerms}"/>
+</Url> -->
+<Url type="text/html"
+ method="GET"
+ template="https://pesquisa.sapo.pt/FF2">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="enc" value="utf-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/seznam-cz.xml b/comm/suite/components/search/searchplugins/seznam-cz.xml
new file mode 100644
index 0000000000..e5c5bd27d7
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/seznam-cz.xml
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Seznam</ShortName>
+<Description>Vyhledávání na Seznam.cz</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAICAAAAEAIACABAAAJgAAABAQAAABACAAKQIAAKYEAACJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAARHSURBVFiFvZfLbxNXFMZ/M45DHk5I1SQFRB4QC6HGuAipSlrEojwk5HQH/AdlhVhkU7pg0QWLtpsmbLJou0KoFQqQhZtIoCgKEVG6CEJBNdA4dBPxcuLYJo7jseeeLjyObGfsDCHqJ92Ffc/9vm/OPXfmXE1EcAgP0Ad8BRwFDgC7rbk48C/wGJgA/gRWHbGKyFbDKyK/iMiqOMeqtca7FT9KqXKjRin1g1IqrZSSbY60UupHi8tWR1NK2SWmCxi2Ur0TeAxcAMKlE3YGjgGjwCc7JJ7HGyAAPCoyYJpm4W8vMA207LB4HhHgSwoyoRcURK2IDItIi4PCtB+RCDIygphmuZgWEbltaSEiVBW4+x74zPGzZLPIwgLMz8OzZ8iTJ2jPn4NSyOIi+qVL5Vb6La0rAFomk4Fc0T0F3JU05eVL1OgoMjMDT5+CYWzMaYVxuo7r5k00r7ccVQb4FAjnM/BdRXHDIDM4iHnrFihVJGZrVCnk4UPc5Q24Lc1vNMMwGoBXQL09m5Dq78ecnNxSGEAA6uqoHRrC5fdXCk0Ce6tEJFBWHDDu32d9YqKsuADoOnpnJy6fj6qjR6k+eRKtqYktXvP1QKAKOFUpKhuPY5imrYHqQIC6c+dw+3xodXWVaMrhlC4i/kpHq+bsWVRHB4ZpkikZWmcn7mPHoLZ2e8dWxK+lUqkI0FzJporHWb52jWQwWFztQFV7O57z5/H09VG1b9/7ZmBJS6VSaaDaSXRqdpbloSHWpqZAZMNMfqd3dXfjOX0az5kz7Dp0yAmloa2trTk2kEc6HGb5xg0SwSBmLLYpKwA1R47QfPEiuwMB0MqeH0NLJpNbbkE5SCZDYnKSlbExEuPjmPH4JjP1PT0cGBjAvWePHcWSlkwmZ4Ce7RgoMpPNkpieJjI8TDQYhGx2w0RNRwe+0VFcDQ2ly/7SRWRu2x+fws7G5aLxxAm6Bgfx3buH1ta2cXISL17w5s4du3VzuoiM74SBoqN78CD7+/tJmyaGNbLZrF3suE6u+Uh+6BaUIh4KbYgrj4eWQKA0ZA0Y1UXknYj8sVNPvx6J8PfVq/xz/ToZpWg4fpzPR0aobm0tjf1dRN5p8Xgccp1QiC0+x+WQSSR4PTbG4t27LE1O4m5qYm9fH/svXODjHtv6zgDdwHz+cxwGfga+dSKo0mmis7O8nZri7YMHxEMhGg8fprm3F+/lyzT39qK5XJUoBoB5AC0Wi+X/rAVmyHUsGxClWF1YIProEbFQCCMaxUylqGtro8nn4yO/H09XF5quO/EOMAd8Qa4GigxAQVOq0mkS4TCpV69wNzZS395OTWvr+wjZYVNTqq2srJQG/b9teTQatQv2Arcp2Y4PwBxwDpuLSbl8hsm9nn8iV7HbRcbi6LETr2QAYJ1c69wN/IpVNA6RAn6z1l6xuGyhLS8vOyX1AF9TfD1vsuZiFF/Pgzi8nv8HoFPlnCBLVLQAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAHwSURBVDiNjZI/S5tRFMZ/N2/y1pKi4Dewi3/SoSD6CYoamsExH8CCtJObKLo5CZ20tNBVUFxEKAixdRIcO9g6BTqoaDBGJEO8f87p4BtJ3rbSA89yznN+3Pvci6qS0oiqvlfVE1VtJfqZ9EbSfkSkrVhE1kQkiIj+Q0FEPorI0/YeIQRCCHEIoRJC0P/U12QHnHM459adc/qH6nX1lYqG+Xn1R0fp+ZpzDqy1BWutt9aqtVZto6FuaUn90JB6Y9SDOlA/Pq4Pnnt5a20hKyIzQERSWiphDg/RdiOKYHgYUy6DCB0VATNZEZnq7IZ6HQUUiObmyCwsQD5/D+wGAEyZZrN5B8QPJ6jV8IuLhO1tUCUzMUE0PU2mWMT096cB1tze3nYBHkBnZ7iNDfzODuH4GOKYXLnMk5UVTF9f29Y0Nzc3J8BQGtBZUq3SWl3Fbm6SnZzk2dZWe/QjIyJ7HZ/pr2JggHh5mTvAdc/2zdXV1QvgOx0v0RXq6Smt3V3swQG5sTHys7OY3l6AALw0tVoNYB14i/f4apVwcYFcXyONBqanh3h0lOzgYJr9AXhnLi8vSUL8oq3WK6IIk8s9FgnAN6AI2ExyFysirzWO1zWK5JE8REQ+iUgp2cGcn5+n6QXgDTAFPAcs8AvYBz4Dx53m36J46V9BLx/IAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://suggest.seznam.cz/fulltext_ff">
+ <Param name="phrase" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://search.seznam.cz/"
+ resultdomain="seznam.cz"
+ rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/startpage-pl.xml b/comm/suite/components/search/searchplugins/startpage-pl.xml
new file mode 100644
index 0000000000..c59e4cf512
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/startpage-pl.xml
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Startpage</ShortName>
+<Description>Prywatne wyszukiwanie za pomocÄ… Startpage.com</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/startpage.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.startpage.com/do/search"
+ resultDomain="startpage.com">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="segment" value="startpage.seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/startpage.xml b/comm/suite/components/search/searchplugins/startpage.xml
new file mode 100644
index 0000000000..52a33cf061
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/startpage.xml
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Startpage</ShortName>
+<Description>Private search with Startpage.com</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/startpage.ico</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.startpage.com/do/search"
+ resultDomain="startpage.com">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="segment" value="startpage.seamonkey"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/tyda-sv-SE.xml b/comm/suite/components/search/searchplugins/tyda-sv-SE.xml
new file mode 100644
index 0000000000..3c0ab2429c
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/tyda-sv-SE.xml
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Tyda.se</ShortName>
+<Description>Tyda.se, lexikon, ordlista och översättning.</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABQklEQVR42mO8fv3GxvVbdu/ay0AcYBYRkrhx4+aXL1+I1cDCxE68agYGBiY4y93Dtbe/Q1lFiVgN4uJi+gZ6PDw8xGrQN9BlYGBwc3eWkJQIDgmIi4+GiEtISsTFR+vp6WDYICHOwMCgoqLMzc2lpKwYlxANcV5gkF9cQjQvHy+6hl079jAwMEybOuvunXvr1m5kYGBwc3dhYGCwtrF88eLlo4eP0TUgg7t37t29c8/dw1VZRUlCQvzokeOfPn/Gp4GBgWHd2o08PNyNzbUMDAzr1236+OEjAQ1Hjxz78uWrhIT4xQuXXjx/gSWU0MCXL1+PHjnGwMCwaydKqmGBsxYtXLpo4VJkOQkJcbg2wjaIioroG+hBHIaiwcbWWkJSAlNDQlIsAwPDzu270WM6IipERUUZU4OEhPjv37+fI3kXAgDUBm6U4URcZQAAAABJRU5ErkJggg==</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://tyda.se/"
+ resultdomain="tyda.se">
+ <Param name="w" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://tyda.se/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/vatera.xml b/comm/suite/components/search/searchplugins/vatera.xml
new file mode 100644
index 0000000000..189e53ff33
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/vatera.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vatera</ShortName>
+<Description>Keresés a Vatera.hu piacterén</Description>
+<InputEncoding>ISO-8859-2</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAATCwAAEwsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wEAAAAAAGD/GABa/2MAWv9eAFv/XwBa/2AAWv8RAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZv8FAAAAAABZ/4wAZv//AF7//wBe//8AZv//AFn/dQAAAAAAgP8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWv/VAFv//wBa//sAWv/6AF3//wBa/8MAAAAAAID/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAFX/AwAAAAAAXP8yAFv//wBb//8AWf/qAFr/7QBc//8AWv/6AF3/IQAAAAAAgP8CAAAAAAAAAAAAAAAAAAAAAACA/wQAAAAAAFr/fwBf//8AYP//AFn/iQBa/5sAX///AF///wBa/2kAAAAAAID/BAAAAAAAAAAAAAAAAAAAAAAAAP8BAAAAAABa/8sAXf//AF///wBb/zgAWv9PAGD//wBd//8AWv+4AAAAAABV/wMAAAAAAAAAAAAAAAAAgP8CAAAAAABZ/ygAWv/9AFz//wBa/+oAVf8GAFX/GABa//YAXP//AFr/8wBg/xgAAAAAAID/AgAAAAAAAAAAAID/BAAAAAAAWv9xAF///wBe//8AWv+oAAAAAAAAAAAAWv+8AF3//wBf//8AWf9cAAAAAABA/wQAAAAAAAAAAACA/wIAAAAAAFv/vwBd//8AXv//AFn/WQAAAAAAAAAAAFr/bwBf//8AXv//AFr/rAAAAAAAQP8EAAAAAACA/wIAAAAAAF7/HgBa//cAW///AFr/8wBZ/xcAAAAAAAAAAABc/ycAWv/9AFv//wBa/+wAYP8QAAAAAAAA/wEAQP8EAAAAAABZ/2QAX///AF3//wBa/7gAAAAAAFX/AwAA/wEAAAAAAFr/ywBd//8AXv//AFr/TwAAAAAAQP8EAFX/AwAAAAAAWv+vAFz//wBe//8AWf9qAAAAAABA/wQAgP8EAAAAAABa/4AAXv//AF3//wBa/5sAAAAAAID/BAAAAAAAWP8aAFr//wBh//8AXP//AFr/IgAAAAAAgP8CAFX/AwAAAAAAWv8zAF///wBh//8AW//yAE7/DQAAAAAAAAAAAFX/EgBa/1IAWf9TAFn/QgAAAAAAAAAAAAAAAAAAAAAAAAAAAID/AgBa/0cAWv9SAFn/UABV/wwAAAAAAAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AffvAADoFwAA6BcAAPgXAADQCwAA0AsAANALAACgBQAAoYUAAKGFAABBggAAQkIAAEJCAACCQQAAh8EAAH/+AAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAATCwAAEwsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABV/wMAVf8DAFX/AwBV/wMAVf8DAFX/AwBV/wMAVf8DAFX/AwCA/wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFX/AwAAAAAAW/9iAFr/xgBa/7sAWv+9AFr/vQBa/70AWv+9AFr/vABa/8MAWf9FAAAAAABV/wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wEAAAAAAFX/BgBa/+oAYv//AF7//wBf//8AX///AF///wBf//8AXf//AGT//wBa/8QAAAAAAFX/AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFX/AwAAAAAAWf9CAF3//wBa//gAWv/8AFr//ABa//sAWv/7AFr/+wBa//sAWv/9AFn/+ABa/x8AAAAAAID/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8EAAAAAABa/5EAYP//AFr/+gBa//8AWv//AFr//gBa//8AWv//AFr//wBZ//sAYP//AFr/ZgAAAAAAQP8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAgP8CAFr/2gBd//8AWv/9AFr//wBa//8AWv//AFr//wBa//8AWv//AFr/+wBf//8AWv+1AAAAAABV/wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVf8DAAAAAABd/zcAXP//AFr//QBa//8AWv//AFr//wBa//sAWv/8AFr//wBa//8AWv/+AFv//wBa//MAWf8XAAAAAAD//wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA/wQAAAAAAFr/hQBg//8AWv/6AFr//wBa//sAYP//AFr/pwBa/8EAX///AFr//ABa//8AWf/7AGD//wBc/1kAAAAAAED/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAWf/QAF3//wBa//wAWv//AFn/+wBh//8AWf9FAFv/cwBi//8AWv/6AFr//wBa//sAYP//AFv/qQAAAAAAQP8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA/wIAAAAAAFv/LQBb//8AWv/+AFr//wBa//4AW///AFr/9gBd/wsAWv8zAF3//wBa//0AWv//AFr//QBc//8AWv/rAFX/DwAAAAAAAP8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAID/BAAAAAAAWf94AGD//wBa//oAWv//AFr/+wBf//8AWv+4AAAAAABV/wYAWv/cAF3//wBa//0AWv//AFr/+wBf//8AWf9NAAAAAABA/wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8CAAAAAABZ/8UAXv//AFr//ABa//8AWv/6AGD//wBa/2kAAAAAAAAAAABb/5UAYP//AFr/+gBa//8AWf/7AGD//wBZ/50AAAAAAED/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAID/AgAAAAAAV/8jAFr/+wBa//8AWv/+AFr//gBa//8AWv/7AFf/IwAAAAAAAAAAAFr/RwBe//8AWv/8AFr//wBa//0AXP//AFr/4gBt/wcAAAAAAAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8EAAAAAABZ/2oAYP//AFr/+gBa//8AWv/8AF7//wBa/8YAAAAAAFX/AwAAAAAAVf8MAFv/5wBc//8AWv/9AFr//wBa//wAXv//AFr/QQAAAAAAVf8DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABV/wMAAAAAAFr/uQBf//8AWv/7AFr//wBa//oAYP//AFr/egAAAAAAgP8EAED/BAAAAAAAWf+mAGD//wBZ//sAWv//AFr/+gBg//8AWv+QAAAAAACA/wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8CAAAAAABc/xkAWv/1AFv//wBa//4AWv//AFr//gBb//8AWv8wAAAAAABV/wMAQP8EAAAAAABb/1cAX///AFr/+wBa//8AWv/9AF3//wBa/9gAAP8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA/wQAAAAAAFr/XQBg//8AWf/7AFr//wBa//wAXf//AFr/1AAAAAAAAAAAAAAAAAAA/wEAAAAAAF3/FgBa//IAW///AFr//gBa//8AWv/9AFz//wBa/zYAAAAAAFX/AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAED/BAAAAAAAWv+tAF///wBa//sAWv//AFr/+gBg//8AWv+LAAAAAACA/wQAAAAAAAAAAABV/wMAAAAAAFr/tgBf//8AWv/7AFr//wBa//oAYP//AFr/ggAAAAAAgP8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wEAAAAAAFr/EQBa/+0AW///AFn//gBa//8AWv/9AF3//wBa/z4AAAAAAFX/AwAAAAAAAAAAAED/BAAAAAAAWf9nAGD//wBa//oAWv//AFr//ABe//8AWv/OAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAED/BAAAAAAAWf9QAF///wBa//sAWv//AFr//QBc//8AWv/gAFX/BgAAAAAAAP8BAAAAAAAAAAAAgP8CAAAAAABd/yEAWf/7AFr//wBa//4AWv/+AFr//gBa//8AWf8rAAAAAACA/wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQP8EAAAAAABa/58AYP//AFn/+wBa//8AWf/7AGD//wBa/5sAAAAAAED/BAAAAAAAAAAAAAAAAAAAAAAAgP8CAAAAAABa/8QAXv//AFr//ABa//8AWv/6AGD//wBZ/3UAAAAAAID/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAVf8JAFn/5ABc//8AWv/9AFr//wBa//sAX///AFn/TQAAAAAAQP8EAAAAAAAAAAAAAAAAAAAAAACA/wQAAAAAAFn/eABg//8AWv/6AFr//wBa//wAXv//AFv/wgAAAAAAgP8CAAAAAAAAAAAAAAAAAAAAAAAAAAAAVf8DAAAAAABa/0QAXv//AFr//ABa//8AWv/9AFv//wBa/+sAVf8PAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAID/AgAAAAAAWf8uAFv//wBa//4AWv//AFr//gBa//8AWv/6AFX/IQAAAAAAgP8CAAAAAAAAAAAAAAAAAAAAAACA/wQAAAAAAFn/kgBg//8AWv/6AFr//wBa//sAX///AFn/qwAAAAAAQP8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWv/SAF3//wBa//wAWv//AFr/+gBg//8AWf9nAAAAAABA/wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWf/WAFv//wBa//kAWv/7AFr/9gBf//8AWv9dAAAAAABA/wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8EAAAAAABZ/4kAX///AFr/9gBZ//sAWv/3AF7//wBa/7IAAAAAAFX/AwAAAAAAAAAAAAAAAABV/wMAAAAAAFn/QgBj//8AX///AGD//wBg//8AYf//AFv//wBc/xkAAAAAAID/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABV/wMAAAAAAFn/PABi//8AYP//AGD//wBg//8AYf//AFz//wBe/x4AAAAAAID/AgAAAAAAAAAAAFX/AwAAAAAAWf88AFv/qQBZ/50AWv+fAFn/nQBa/6gAWf9wAAAAAACA/wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP8CAFn/hgBa/6QAWv+eAFr/nwBa/54AWv+kAFz/JAAAAAAAgP8CAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAID/AgCA/wQAQP8EAED/BABA/wQAgP8EAFX/AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA/wQAQP8EAED/BABA/wQAQP8EAID/BAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////4Af//9/7//+gBf//QAX//0AC//9AAv/+gAL//oABf/6AAX/+gAF//QAAv/0BAL/9AYC/+gGAX/oCgF/6AkBf9AJAP/QHQC/0BaAv6AWgL+gFoBfoC9AX0AvQF9AL0AvQF/gL8BfoC6AX6AWgL/gF39//++A//Af/////</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.vatera.hu/listings/index.php">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="c" value="0"/>
+ <Param name="td" value="on"/>
+</Url>
+<SearchForm>https://www.vatera.hu/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-NO.xml b/comm/suite/components/search/searchplugins/wikipedia-NO.xml
new file mode 100644
index 0000000000..2449202447
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-NO.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (no)</ShortName>
+<Description>Wikipedia, den frie encyklopedi</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://no.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://no.wikipedia.org/wiki/Spesial:Søk"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-cz.xml b/comm/suite/components/search/searchplugins/wikipedia-cz.xml
new file mode 100644
index 0000000000..0c6672105a
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-cz.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedie (cs)</ShortName>
+<Description>Wikipedia, svobodná encyclopedie</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://cs.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://cs.wikipedia.org/wiki/Speciální:Hledání"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-de.xml b/comm/suite/components/search/searchplugins/wikipedia-de.xml
new file mode 100644
index 0000000000..d9e46f4a5c
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-de.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (de)</ShortName>
+<Description>Wikipedia, die freie Enzyklopädie</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://de.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://de.wikipedia.org/wiki/Spezial:Suche"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-el.xml b/comm/suite/components/search/searchplugins/wikipedia-el.xml
new file mode 100644
index 0000000000..1c73c65c53
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-el.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (el)</ShortName>
+<Description>Βικιπαίδεια, η ελεÏθεÏη εγκυκλοπαίδεια</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://el.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-es.xml b/comm/suite/components/search/searchplugins/wikipedia-es.xml
new file mode 100644
index 0000000000..dc1f798fd3
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-es.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (es)</ShortName>
+<Description>Wikipedia, la enciclopedia libre</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://es.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://es.wikipedia.org/wiki/Especial:Buscar"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-fi.xml b/comm/suite/components/search/searchplugins/wikipedia-fi.xml
new file mode 100644
index 0000000000..27ff04653d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-fi.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (fi)</ShortName>
+<Description>Wikipedia (fi), vapaa tietosanakirja</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://fi.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://fi.wikipedia.org/wiki/Toiminnot:Haku"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search" />
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-fr.xml b/comm/suite/components/search/searchplugins/wikipedia-fr.xml
new file mode 100644
index 0000000000..8d999262bb
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-fr.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (fr)</ShortName>
+<Description>Wikipédia, l'encyclopédie libre</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://fr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://fr.wikipedia.org/wiki/Spécial:Recherche"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-hu.xml b/comm/suite/components/search/searchplugins/wikipedia-hu.xml
new file mode 100644
index 0000000000..d2888bfe7e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-hu.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (hu)</ShortName>
+<Description>Wikipedia, the free encyclopedia</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://hu.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://hu.wikipedia.org/wiki/Speciális:Keresés"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-it.xml b/comm/suite/components/search/searchplugins/wikipedia-it.xml
new file mode 100644
index 0000000000..47040f6f6d
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-it.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (it)</ShortName>
+<Description>Wikipedia, l'enciclopedia libera</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://it.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://it.wikipedia.org/wiki/Speciale:Ricerca"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-ja.xml b/comm/suite/components/search/searchplugins/wikipedia-ja.xml
new file mode 100644
index 0000000000..3f516ff96e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-ja.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ja)</ShortName>
+<Description>Wikipedia - フリー百科事典</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ja.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://ja.wikipedia.org/wiki/特別:検索"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-ka.xml b/comm/suite/components/search/searchplugins/wikipedia-ka.xml
new file mode 100644
index 0000000000..7d90efd508
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-ka.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ვიკიპედირ(ka)</ShortName>
+<Description>ვიკიპედიáƒ, თáƒáƒ•áƒ˜áƒ¡áƒ£áƒ¤áƒáƒšáƒ˜ ენციკლáƒáƒžáƒ”დიáƒ</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ka.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://ka.wikipedia.org/wiki/სპეციáƒáƒšáƒ£áƒ áƒ˜:ძიებáƒ"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-nl.xml b/comm/suite/components/search/searchplugins/wikipedia-nl.xml
new file mode 100644
index 0000000000..0999f48b17
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-nl.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (nl)</ShortName>
+<Description>De vrije encyclopedie</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://nl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://nl.wikipedia.org/wiki/Speciaal:Zoeken"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-pl.xml b/comm/suite/components/search/searchplugins/wikipedia-pl.xml
new file mode 100644
index 0000000000..3d4f26e5f9
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-pl.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (pl)</ShortName>
+<Description>Wikipedia, wolna encyklopedia</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://pl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://pl.wikipedia.org/wiki/Specjalna:Szukaj"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-pt.xml b/comm/suite/components/search/searchplugins/wikipedia-pt.xml
new file mode 100644
index 0000000000..4745e920a8
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-pt.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (pt)</ShortName>
+<Description>Wikipédia, a enciclopédia livre</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://pt.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://pt.wikipedia.org/wiki/Especial:Pesquisar"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-ru.xml b/comm/suite/components/search/searchplugins/wikipedia-ru.xml
new file mode 100644
index 0000000000..4710887074
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-ru.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Ð’Ð¸ÐºÐ¸Ð¿ÐµÐ´Ð¸Ñ (ru)</ShortName>
+<Description>ВикипедиÑ, ÑÐ²Ð¾Ð±Ð¾Ð´Ð½Ð°Ñ ÑнциклопедиÑ</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ru.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://ru.wikipedia.org/wiki/СлужебнаÑ:ПоиÑк"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-sk.xml b/comm/suite/components/search/searchplugins/wikipedia-sk.xml
new file mode 100644
index 0000000000..3fc19e7f61
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-sk.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (sk)</ShortName>
+<Description>Wikipédia, slobodná a otvorená encyklopédia</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://sk.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-sv-SE.xml b/comm/suite/components/search/searchplugins/wikipedia-sv-SE.xml
new file mode 100644
index 0000000000..224e43242e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-sv-SE.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (sv)</ShortName>
+<Description>Wikipedia, den fria encyklopedin</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://sv.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://sv.wikipedia.org/wiki/Special:Sök"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-zh-CN.xml b/comm/suite/components/search/searchplugins/wikipedia-zh-CN.xml
new file mode 100644
index 0000000000..a852639c18
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-zh-CN.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>维基百科</ShortName>
+<Description>维基百科,自由的百科全书</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://zh.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://zh.wikipedia.org/wiki/Special:æœç´¢"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia-zh-TW.xml b/comm/suite/components/search/searchplugins/wikipedia-zh-TW.xml
new file mode 100644
index 0000000000..57357e10df
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia-zh-TW.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (zh)</ShortName>
+<Description>維基百科,自由的百科全書</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://zh.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://zh.wikipedia.org/wiki/Special:æœç´¢"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+ <Param name="variant" value="zh-tw"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wikipedia.xml b/comm/suite/components/search/searchplugins/wikipedia.xml
new file mode 100644
index 0000000000..3daf9a2724
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wikipedia.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (en)</ShortName>
+<Description>Wikipedia, the Free Encyclopedia</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/wikipedia.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://en.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://en.wikipedia.org/wiki/Special:Search"
+ resultdomain="wikipedia.org"
+ rel="searchform">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/wolnelektury-pl.xml b/comm/suite/components/search/searchplugins/wolnelektury-pl.xml
new file mode 100644
index 0000000000..b2d4e649a3
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/wolnelektury-pl.xml
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wolne Lektury</ShortName>
+<Description>Biblioteka internetowa WolneLektury.pl</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image height="16" width="16" type="image/png">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAZlBMVEWauyuauiubuiubuyqauyqbvCrHuBfHuBfsvADrvgDsvgDsvQDrvgDsvQCbuyvsvgC4ABy8ABy+ABm+ABq/ABq/ABm+ABk9YW4+YW49YW49YnA9Y3E4Y3E4Y3G/ABo+Ym+/ABm4ABw5ZyQ5AAAAInRSTlPbyaSAWzYSADZbgKTJ2///EjZbf6TJ28+kgFs2EgD//4AA+LFVUAAAAFdJREFUeAGdzsEKgDAMA9CklzlwQy/+/zeKoCAuspVdvAx8hwYCpSXoIKNLqC7Elo8RjaxnL/i7KAxyqc1ChkgHuiWjOm3qfwyv5G9xjFa4SfMurbhV4QWhBzKL2hdUYQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://wolnelektury.pl/katalog/jtags/">
+ <Param name="mozhint" value="1"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://wolnelektury.pl/szukaj/">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://wolnelektury.pl</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-NO.xml b/comm/suite/components/search/searchplugins/yahoo-NO.xml
new file mode 100644
index 0000000000..fc2a299577
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-NO.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Søk</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://no.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://no.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-ar.xml b/comm/suite/components/search/searchplugins/yahoo-ar.xml
new file mode 100644
index 0000000000..e7729d1998
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-ar.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo Argentina</ShortName>
+<Description>Buscar en Yahoo Argentina</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://ar.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://ar.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-bid-zh-TW.xml b/comm/suite/components/search/searchplugins/yahoo-bid-zh-TW.xml
new file mode 100644
index 0000000000..b06761db8b
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-bid-zh-TW.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo!奇摩æ‹è³£</ShortName>
+<Description>Yahoo!奇摩æ‹è³£</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/gif;base64,R0lGODlhEAAQAMQfAP+7Q4aWvf2QBaKtx//MS+6OLI1MMf/XYv+sKkpalEZNepyUa4KOo2ttjpx4Tdh1ALdrI9lSAHhaVWNSa+Z7Ge9zAPdrALVaIR4/ie9rAOZrELXB0Gtzc/9zCJg+Hf///yH5BAEAAB8ALAAAAAAQABAAAAWM4CeO3xAEA6mKw3IQDrOSAUEAiBPMpXsciIdk1XAUfkCBwKBiAG4IAED5UKg4iGxFWfFgNk0lF+KJWBqribgCwUgKlsRok0hMHvj2YNKJfzYMBxcXE4UTEQkbChlxDT9SBZEUGogBiwkSBFE4khoGcgEJAw0GEBSRBRQUF580CQawsbAJO64Kt7i0KiEAOw==</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://tw.search.bid.yahoo.com/search/ac"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-br.xml b/comm/suite/components/search/searchplugins/yahoo-br.xml
new file mode 100644
index 0000000000..7504947fb6
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-br.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Pesquisa Yahoo</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://br.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://br.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-de.xml b/comm/suite/components/search/searchplugins/yahoo-de.xml
new file mode 100644
index 0000000000..1ffafd0799
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-de.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Suche</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://de.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://de.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-en-GB.xml b/comm/suite/components/search/searchplugins/yahoo-en-GB.xml
new file mode 100644
index 0000000000..0e3f0a37e2
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-en-GB.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo.co.uk</ShortName>
+<Description>Yahoo UK &amp; Ireland Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://uk.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://uk.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-es.xml b/comm/suite/components/search/searchplugins/yahoo-es.xml
new file mode 100644
index 0000000000..721153e384
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-es.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Buscar</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://es.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://es.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-fi.xml b/comm/suite/components/search/searchplugins/yahoo-fi.xml
new file mode 100644
index 0000000000..739e6207fe
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-fi.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo-haku</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://fi.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://fi.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-fr.xml b/comm/suite/components/search/searchplugins/yahoo-fr.xml
new file mode 100644
index 0000000000..12da5c09f1
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-fr.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Recherche Yahoo</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://fr.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://fr.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-it.xml b/comm/suite/components/search/searchplugins/yahoo-it.xml
new file mode 100644
index 0000000000..950ca8a1f4
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-it.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://it.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://it.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-jp.xml b/comm/suite/components/search/searchplugins/yahoo-jp.xml
new file mode 100644
index 0000000000..0545a54949
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-jp.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo! JAPAN</ShortName>
+<Description>Yahoo Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMA/24zAP//MwD//zMA//8zAP//MwD//wAAAAAAAAAAMwD/7TMA/+MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADkI//8zAP//MwD/bwAAAAAAAAAAAAAAADEA/zQyAP8pNwD/FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5CP//MwD//zIA/3AAAAAAAAAAAAAAAAAAAAAAMgD/wDIA/6cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AP8sMwD/9zMA//8zAP//NwD/FwAAAAAAAAAAAAAAADQA/3EzAP//MQD/PgAAAAAAAAAAAAAAAAAAAAA3AP8OMwD/2DMA//80AP9PMwD/WjMA//8xAP8qAAAAAAAAAAAyAP8kMwD//zMA/9EAAAAIAAAAAAAAAAAAAAADMwD/tDMA//8yAP+JAAAAAAAAAAAxAP85MwD//zQA/1gAAAAAAAAAADMA/9UzAP//MwD/qQAAAAAAAAAAMgD/kzMA//8zAP/EAAAABAAAAAAAAAAAMwD//zMA//8zAP//MwD//zMA/30xAP8+MwD/+TEA/z4yAP+sMwD//zMA//8zAP//MwD//zMA/5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD//wAA//8AAP//AADgZwAA+OMAAPjzAADwcQAA4DAAAMMYAADDAAAAA/8AAP//AAD//wAA//8AAP//AAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwD//zMA//8zAP//MwD//zMA//8zAP//MwD//zMA//8zAP//MwD//zMA//8zAP//AAAAAAAAAAAAAAAAMQD/HzQA/2MyAP+iAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyAP+dMwD/9jMA//8zAP//MwD//zMA//8zAP//MwD//zMA//8zAP//MwD//zMA/3gAAAAAAAAAAAAAAAAzAP+/MwD//zMA//80AP9UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAD/JzQA/4ozAP//MwD//zMA//8zAP/nMwD/NzQA/ycxAP8VAAAABAAAAAAAAAAAAAAAADMA/1AzAP//MwD/zjMA/18AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMA//8zAP//MwD//zMA/98AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAA/yUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwD//zMA//8zAP//MwD/3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADUA/yIzAP9zMwD/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAP//MwD//zMA//8zAP/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAD/njMA//8zAP/pMwD/DwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAD/TzMA//8zAP//MwD//zMA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAP9QMwD//zMA//80AP+PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg0AP+UMwD//zMA//8zAP//MwD//jIA/1IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC4A/wszAP/2MwD//zMA//sxAP8qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwD/lzMA//8zAP//MwD//zMA/84zAP/5MwD/+TIA/1EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMA/7MzAP//MwD//zQA/7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIA/1szAP//MwD//zMA//8zAP/fOQD/EjMA/0EzAP/tMwD//DQA/2wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMgD/ZTMA//8zAP//MwD//zIA/1IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxAP8vMwD/9TMA//8zAP//MwD/+DEA/zQAAAAAAAAAADUA/yIzAP/UMwD//zMA/5YAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAzAP8ZMwD//TMA//8zAP//MwD/3wAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALgD/FjMA/98zAP//MwD//zMA//8yAP9mAAAAAAAAAAAAAAAAAAAAADMA/w8zAP+4MwD//zMA/8MzAP8ZAAAAAAAAAAAAAAAAAAAAAAAAAAAzAP/IMwD//zMA//8zAP//MgD/fwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYzAP/DMwD//zMA//8zAP//MgD/ogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYzAP+zMwD//zMA/+k0AP9JAAAAAAAAAAAAAAAAAAAAADMA/3kzAP//MwD//zMA//8zAP/2MAD/IAAAAAAAAAAAAAAAAAAAAAAAAAABMwD/pDMA//8zAP//MwD//zMA/9YuAP8LAAAAAAAAAAAAAAAAAAAAAAAAAAA1AP8/NAD/YjMA/5ozAP//MwD//zMA//8zAP+/MwD/gzIA/00wAP8QNQD/KzMA//8zAP//MwD//zMA//8zAP+zAAAAAAAAAAAAAAAAAAAAADMA/40zAP//MwD//zMA//8zAP/3MwD/LQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMA/98zAP/fMwD/3zMA/98zAP/fMwD/3zMA/98zAP/fMwD/3zMA/58AAAAAMwD/3TMA//8zAP/fMwD/kTEA/0MzAP88MgD/YDMA/4IzAP+6MwD//zMA//8zAP//MwD//zMA/7oyAP9HNAD/NjAA/yUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AP9FMgD/LgAAAAAAAAAAAAAAADMA//YzAP//MwD//zMA//8zAP//MwD//zMA//8zAP//MwD//zMA//8zAP//MwD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////////////////////////////////+ABw//gAcP/+AHD//4fz//+H/H//h/w//wf8P/4D/B/+Af4f/AD+D/gYPgfwPB8H4H4PA8B8AAPA/ACAAD//nAA//////////////////////////////////////////////</Image>
+<Url type="text/html" method="GET"
+ template="https://search.yahoo.co.jp/search"
+ resultdomain="yahoo.co.jp"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+ <Param name="fr" value="mozff" />
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-nl.xml b/comm/suite/components/search/searchplugins/yahoo-nl.xml
new file mode 100644
index 0000000000..8d9323b744
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-nl.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Zoeken</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://nl.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://nl.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-sv-SE.xml b/comm/suite/components/search/searchplugins/yahoo-sv-SE.xml
new file mode 100644
index 0000000000..4646c3404c
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-sv-SE.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Sök</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://se.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://se.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-zh-CN.xml b/comm/suite/components/search/searchplugins/yahoo-zh-CN.xml
new file mode 100644
index 0000000000..99a6b6f220
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-zh-CN.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo!</ShortName>
+<Description>Yahoo!奇摩æœå°‹</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://zh.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://zh.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo-zh-TW.xml b/comm/suite/components/search/searchplugins/yahoo-zh-TW.xml
new file mode 100644
index 0000000000..8045260d8e
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo-zh-TW.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo!</ShortName>
+<Description>Yahoo!奇摩æœå°‹</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://tw.search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://tw.search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/yahoo.xml b/comm/suite/components/search/searchplugins/yahoo.xml
new file mode 100644
index 0000000000..4359b9c481
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/yahoo.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<Description>Yahoo Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">resource://search-plugins/images/yahoo.ico</Image>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="https://search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson"/>
+ <Param name="appid" value="smd"/>
+ <Param name="command" value="{searchTerms}"/>
+</Url>
+<Url type="text/html"
+ method="GET"
+ template="https://search.yahoo.com/search"
+ resultdomain="yahoo.com"
+ rel="searchform">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+</Url>
+</SearchPlugin>
diff --git a/comm/suite/components/search/searchplugins/zoznam-sk.xml b/comm/suite/components/search/searchplugins/zoznam-sk.xml
new file mode 100644
index 0000000000..37ac6b8678
--- /dev/null
+++ b/comm/suite/components/search/searchplugins/zoznam-sk.xml
@@ -0,0 +1,13 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/" xmlns:os="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName>Zoznam</ShortName>
+<Description>Zoznam slovenskeho internetu</Description>
+<InputEncoding>WINDOWS-1250</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAApUlEQVQ4jWNggIJ/DAz//zEw/D+tKv3/dqT3fwYiASNMMy4FZ1WlGRg4OBgEdDQYVJdvZcRpEswFhPBmTu7/p1Wl4RYyohtCrNMZGBgYmBgYGHE7CYsL0TVjuIBUzUQZgE8zQQMIacZrADGacRpArGasBpCiGcMAUjWjGECOZqg67ABXUl5ta/x/d2o0aoo9bKqNO+3rGv+/7Gr8/3BmNknJnGgAAJxPXsbphIDfAAAAAElFTkSuQmCC</Image>
+<Url type="text/html"
+ method="GET"
+ template="https://www.zoznam.sk/hladaj.fcgi">
+ <Param name="co" value="odkazy"/>
+ <Param name="s" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.zoznam.sk/</SearchForm>
+</SearchPlugin>
diff --git a/comm/suite/components/security/content/prefs/pref-certs.js b/comm/suite/components/security/content/prefs/pref-certs.js
new file mode 100644
index 0000000000..a630f0aa9d
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-certs.js
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ var securityOCSPEnabled = document.getElementById("security.OCSP.enabled");
+ DoEnabling(securityOCSPEnabled.value);
+}
+
+function DoEnabling(aOCSPPrefValue)
+{
+ EnableElementById("requireWorkingOCSP", aOCSPPrefValue != 0, false);
+}
+
+function OpenCertManager()
+{
+ document.documentElement
+ .openWindow("mozilla:certmanager",
+ "chrome://pippki/content/certManager.xul",
+ "", null);
+}
+
+function OpenDeviceManager()
+{
+ document.documentElement
+ .openWindow("mozilla:devicemanager",
+ "chrome://pippki/content/device_manager.xul",
+ "", null);
+}
diff --git a/comm/suite/components/security/content/prefs/pref-certs.xul b/comm/suite/components/security/content/prefs/pref-certs.xul
new file mode 100644
index 0000000000..3caac6499c
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-certs.xul
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % prefCertsDTD SYSTEM "chrome://pippki/locale/pref-certs.dtd">
+ %prefCertsDTD;
+ <!ENTITY % prefSslDTD SYSTEM "chrome://pippki/locale/pref-ssl.dtd">
+ %prefSslDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="certs_pane"
+ label="&pref.certs.title;"
+ script="chrome://pippki/content/pref-certs.js">
+ <preferences id="cert_preferences">
+ <preference id="security.default_personal_cert"
+ name="security.default_personal_cert"
+ type="string"/>
+ <preference id="security.disable_button.openCertManager"
+ name="security.disable_button.openCertManager"
+ type="bool"/>
+ <preference id="security.disable_button.openDeviceManager"
+ name="security.disable_button.openDeviceManager"
+ type="bool"/>
+ <preference id="security.OCSP.enabled"
+ name="security.OCSP.enabled"
+ type="int"
+ onchange="DoEnabling(this.value);"/>
+ <preference id="security.OCSP.require"
+ name="security.OCSP.require"
+ type="bool"/>
+ </preferences>
+
+
+ <groupbox align="start">
+ <caption label="&SSLClientAuthMethod.caption;"/>
+ <description>&certselect.description;</description>
+ <radiogroup id="certSelection"
+ orient="horizontal"
+ preference="security.default_personal_cert"
+ aria-labelledby="CertGroupCaption CertSelectionDesc">
+ <radio value="Select Automatically"
+ label="&certselect.auto;"
+ accesskey="&certselect.auto.accesskey;"/>
+ <radio value="Ask Every Time"
+ label="&certselect.ask;"
+ accesskey="&certselect.ask.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <!-- Certificate manager -->
+ <groupbox>
+ <caption label="&managecerts.caption;"/>
+ <description>&managecerts.text;</description>
+ <hbox align="center">
+ <button label="&managecerts.button;"
+ oncommand="OpenCertManager();"
+ id="openCertManagerButton"
+ accesskey="&managecerts.accesskey;"
+ preference="security.disable_button.openCertManager"/>
+ </hbox>
+ </groupbox>
+
+ <!-- Device manager -->
+ <groupbox>
+ <caption label="&managedevices.caption;"/>
+ <description>&managedevices.text;</description>
+ <hbox align="center">
+ <button label="&managedevices.button;"
+ oncommand="OpenDeviceManager();"
+ id="openDeviceManagerButton"
+ accesskey="&managedevices.accesskey;"
+ preference="security.disable_button.openDeviceManager"/>
+ </hbox>
+ </groupbox>
+
+ <!-- Validation -->
+ <groupbox align="start">
+ <caption label="&validation.ocsp.caption;"/>
+ <checkbox id="enableOCSPBox"
+ label="&enableOCSP.label;"
+ accesskey="&enableOCSP.accesskey;"
+ onsynctopreference="return +this.checked;"
+ preference="security.OCSP.enabled"/>
+ <separator class="thin"/>
+ <checkbox id="requireWorkingOCSP"
+ label="&validation.requireOCSP.description;"
+ accesskey="&validation.requireOCSP.accesskey;"
+ preference="security.OCSP.require"/>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/security/content/prefs/pref-passwords.js b/comm/suite/components/security/content/prefs/pref-passwords.js
new file mode 100644
index 0000000000..f958a37055
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-passwords.js
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gInternalToken;
+
+function Startup() {
+ var tokendb = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB);
+ gInternalToken = tokendb.getInternalKeyToken();
+}
+
+function ChangePW()
+{
+ var p = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+ p.SetString(1, "");
+ window.openDialog("chrome://pippki/content/changepassword.xul", "",
+ "chrome,centerscreen,modal", p);
+}
+
+function ResetPW()
+{
+ var p = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+ p.SetString(1, gInternalToken.tokenName);
+ window.openDialog("chrome://pippki/content/resetpassword.xul", "",
+ "chrome,centerscreen,modal", p);
+}
diff --git a/comm/suite/components/security/content/prefs/pref-passwords.xul b/comm/suite/components/security/content/prefs/pref-passwords.xul
new file mode 100644
index 0000000000..af12060f0d
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-passwords.xul
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % prefMast SYSTEM "chrome://pippki/locale/pref-masterpass.dtd">
+ %prefMast;
+ <!ENTITY % prefPass SYSTEM "chrome://pippki/locale/pref-passwords.dtd">
+ %prefPass;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="passwords_pane"
+ label="&pref.passwords.title;"
+ script="chrome://pippki/content/pref-passwords.js">
+
+ <preferences id="passwords_preferences">
+ <preference id="signon.rememberSignons"
+ name="signon.rememberSignons"
+ type="bool"/>
+ <preference id="pref.advanced.password.disable_button.view_stored_password"
+ name="pref.advanced.password.disable_button.view_stored_password"
+ type="bool"/>
+ <preference id="security.disable_button.changePassword"
+ name="security.disable_button.changePassword"
+ type="bool"/>
+ <preference id="security.disable_button.resetPassword"
+ name="security.disable_button.resetPassword"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&signonHeader.caption;"/>
+ <description>&signonDescription.label;</description>
+ <hbox>
+ <checkbox id="signonRememberSignons"
+ label="&signonEnabled.label;"
+ accesskey="&signonEnabled.accesskey;"
+ preference="signon.rememberSignons"/>
+ </hbox>
+ <hbox pack="end">
+ <button id="viewStoredPassword"
+ label="&viewSignons.label;"
+ accesskey="&viewSignons.accesskey;"
+ oncommand="toDataManager('|passwords');"
+ preference="pref.advanced.password.disable_button.view_stored_password"/>
+ </hbox>
+ </groupbox>
+
+ <!-- Change Password -->
+ <groupbox>
+ <caption label="&changepassword.caption;"/>
+ <description>&changepassword.text;</description>
+ <hbox>
+ <button label="&changepassword.button;"
+ oncommand="ChangePW();"
+ id="changePasswordButton"
+ accesskey="&changepassword.accesskey;"
+ preference="security.disable_button.changePassword"/>
+ </hbox>
+ </groupbox>
+
+ <!-- Reset Password -->
+ <groupbox>
+ <caption label="&resetpassword.caption;"/>
+ <description>&resetpassword.text;</description>
+ <hbox>
+ <button label="&resetpassword2.button;"
+ oncommand="ResetPW();"
+ id="resetPasswordButton"
+ accesskey="&resetpassword2.accesskey;"
+ preference="security.disable_button.resetPassword"/>
+ </hbox>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/security/content/prefs/pref-ssl.js b/comm/suite/components/security/content/prefs/pref-ssl.js
new file mode 100644
index 0000000000..1e807f7402
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-ssl.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Startup()
+{
+ // map associating preference values with checkbox element IDs
+ gSslPrefElements = new Map([[1, "allowTLS10"],
+ [2, "allowTLS11"],
+ [3, "allowTLS12"],
+ [4, "allowTLS13"]]);
+
+ // initial setting of checkboxes based on preference values
+ UpdateSslBoxes();
+}
+
+function UpdateSslBoxes()
+{
+ // get minimum and maximum allowed protocol and locked status
+ let minVersion = document.getElementById("security.tls.version.min").value;
+ let maxVersion = document.getElementById("security.tls.version.max").value;
+ let minLocked = document.getElementById("security.tls.version.min").locked;
+ let maxLocked = document.getElementById("security.tls.version.max").locked;
+
+ // check if allowable limits are violated, use default values if they are
+ if (minVersion > maxVersion || !gSslPrefElements.has(minVersion)
+ || !gSslPrefElements.has(maxVersion))
+ {
+ minVersion = document.getElementById("security.tls.version.min").defaultValue;
+ maxVersion = document.getElementById("security.tls.version.max").defaultValue;
+ }
+
+ // set checked, disabled, and locked status for each protocol checkbox
+ for (let [version, id] of gSslPrefElements)
+ {
+ let currentBox = document.getElementById(id);
+ currentBox.checked = version >= minVersion && version <= maxVersion;
+
+ if ((minLocked && maxLocked) || (minLocked && version <= minVersion) ||
+ (maxLocked && version >= maxVersion))
+ {
+ // boxes subject to a preference's locked status are disabled and grayed
+ currentBox.removeAttribute("nogray");
+ currentBox.disabled = true;
+ }
+ else
+ {
+ // boxes which the user can't uncheck are disabled but not grayed
+ currentBox.setAttribute("nogray", "true");
+ currentBox.disabled = (version > minVersion && version < maxVersion) ||
+ (version == minVersion && version == maxVersion);
+ }
+ }
+}
+
+function UpdateSslPrefs()
+{
+ // this is called whenever a checkbox changes
+ let minVersion = -1;
+ let maxVersion = -1;
+
+ // find the first and last checkboxes which are now checked
+ for (let [version, id] of gSslPrefElements)
+ {
+ if (document.getElementById(id).checked)
+ {
+ if (minVersion < 0) // first box checked
+ minVersion = version;
+ maxVersion = version; // last box checked so far
+ }
+ }
+
+ // if minVersion is valid, then maxVersion is as well -> update prefs
+ if (minVersion >= 0)
+ {
+ document.getElementById("security.tls.version.min").value = minVersion;
+ document.getElementById("security.tls.version.max").value = maxVersion;
+ }
+
+ // update checkbox values and visibility based on prefs again
+ UpdateSslBoxes();
+}
diff --git a/comm/suite/components/security/content/prefs/pref-ssl.xul b/comm/suite/components/security/content/prefs/pref-ssl.xul
new file mode 100644
index 0000000000..8541c0f2a1
--- /dev/null
+++ b/comm/suite/components/security/content/prefs/pref-ssl.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % prefSslDTD SYSTEM "chrome://pippki/locale/pref-ssl.dtd">
+ %prefSslDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefpane id="ssl_pane"
+ label="&pref.ssltls.title;"
+ script="chrome://pippki/content/pref-ssl.js">
+ <preferences id="ssl_preferences">
+ <preference id="security.tls.version.min"
+ name="security.tls.version.min"
+ type="int"/>
+ <preference id="security.tls.version.max"
+ name="security.tls.version.max"
+ type="int"/>
+ <preference id="security.warn_entering_secure"
+ name="security.warn_entering_secure"
+ type="bool"/>
+ <preference id="security.warn_leaving_secure"
+ name="security.warn_leaving_secure"
+ type="bool"/>
+ <preference id="security.warn_submit_insecure"
+ name="security.warn_submit_insecure"
+ type="bool"/>
+ <preference id="security.warn_mixed_active_content"
+ name="security.warn_mixed_active_content"
+ type="bool"/>
+ <preference id="security.mixed_content.block_active_content"
+ name="security.mixed_content.block_active_content"
+ type="bool"/>
+ <preference id="security.warn_mixed_display_content"
+ name="security.warn_mixed_display_content"
+ type="bool"/>
+ <preference id="security.mixed_content.block_display_content"
+ name="security.mixed_content.block_display_content"
+ type="bool"/>
+ </preferences>
+
+ <groupbox align="start">
+ <caption label="&SSLTLSProtocolVersions.caption;"/>
+ <description>&limit.description;</description>
+
+ <hbox align="center">
+ <label id="allowEnable"
+ value="&limit.enable.label;"/>
+ <checkbox id="allowTLS10"
+ class="nogray-disabled"
+ label="&limit.tls10.label;"
+ accesskey="&limit.tls10.accesskey;"
+ oncommand="UpdateSslPrefs();"/>
+ <checkbox id="allowTLS11"
+ class="nogray-disabled"
+ label="&limit.tls11.label;"
+ accesskey="&limit.tls11.accesskey;"
+ oncommand="UpdateSslPrefs();"/>
+ <checkbox id="allowTLS12"
+ class="nogray-disabled"
+ label="&limit.tls12.label;"
+ accesskey="&limit.tls12.accesskey;"
+ oncommand="UpdateSslPrefs();"/>
+ <checkbox id="allowTLS13"
+ class="nogray-disabled"
+ label="&limit.tls13.label;"
+ accesskey="&limit.tls13.accesskey;"
+ oncommand="UpdateSslPrefs();"/>
+ </hbox>
+
+ </groupbox>
+
+ <groupbox align="start">
+ <caption label="&SSLTLSWarnings.caption;"/>
+ <description>&warn.description2;</description>
+ <checkbox id="warnEnteringSecure"
+ label="&warn.enteringsecure;"
+ accesskey="&warn.enteringsecure.accesskey;"
+ preference="security.warn_entering_secure"/>
+ <checkbox id="warnLeavingSecure"
+ label="&warn.leavingsecure;"
+ accesskey="&warn.leavingsecure.accesskey;"
+ preference="security.warn_leaving_secure"/>
+ <checkbox id="warnInsecurePost"
+ label="&warn.insecurepost;"
+ accesskey="&warn.insecurepost.accesskey;"
+ preference="security.warn_submit_insecure"/>
+ </groupbox>
+
+ <groupbox align="start">
+ <caption label="&SSLMixedContent.caption;"/>
+ <description>&mixed.description;</description>
+ <checkbox id="warnMixedActiveContent"
+ label="&warn.mixedactivecontent;"
+ accesskey="&warn.mixedactivecontent.accesskey;"
+ preference="security.warn_mixed_active_content"/>
+ <checkbox id="blockActiveContent"
+ label="&block.activecontent;"
+ accesskey="&block.activecontent.accesskey;"
+ preference="security.mixed_content.block_active_content"/>
+ <checkbox id="warnMixedDisplayContent"
+ label="&warn.mixeddisplaycontent;"
+ accesskey="&warn.mixeddisplaycontent.accesskey;"
+ preference="security.warn_mixed_display_content"/>
+ <checkbox id="blockDisplayContent"
+ label="&block.displaycontent;"
+ accesskey="&block.displaycontent.accesskey;"
+ preference="security.mixed_content.block_display_content"/>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/components/security/jar.mn b/comm/suite/components/security/jar.mn
new file mode 100644
index 0000000000..e98909e681
--- /dev/null
+++ b/comm/suite/components/security/jar.mn
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pippki.jar:
+ content/pippki/pref-certs.js (content/prefs/pref-certs.js)
+ content/pippki/pref-certs.xul (content/prefs/pref-certs.xul)
+ content/pippki/pref-passwords.js (content/prefs/pref-passwords.js)
+ content/pippki/pref-passwords.xul (content/prefs/pref-passwords.xul)
+ content/pippki/pref-ssl.js (content/prefs/pref-ssl.js)
+ content/pippki/pref-ssl.xul (content/prefs/pref-ssl.xul)
diff --git a/comm/suite/components/security/moz.build b/comm/suite/components/security/moz.build
new file mode 100644
index 0000000000..de5cd1bf81
--- /dev/null
+++ b/comm/suite/components/security/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/sessionstore/XPathGenerator.jsm b/comm/suite/components/sessionstore/XPathGenerator.jsm
new file mode 100644
index 0000000000..e202468a27
--- /dev/null
+++ b/comm/suite/components/sessionstore/XPathGenerator.jsm
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["XPathGenerator"];
+
+var XPathGenerator = {
+ // these two hashes should be kept in sync
+ namespaceURIs: { "xhtml": "http://www.w3.org/1999/xhtml" },
+ namespacePrefixes: { "http://www.w3.org/1999/xhtml": "xhtml" },
+
+ /**
+ * Generates an approximate XPath query to an (X)HTML node
+ */
+ generate: function sss_xph_generate(aNode) {
+ // have we reached the document node already?
+ if (!aNode.parentNode)
+ return "";
+
+ // Access localName, namespaceURI just once per node since it's expensive.
+ let nNamespaceURI = aNode.namespaceURI;
+ let nLocalName = aNode.localName;
+
+ let prefix = this.namespacePrefixes[nNamespaceURI] || null;
+ let tag = (prefix ? prefix + ":" : "") + this.escapeName(nLocalName);
+
+ // stop once we've found a tag with an ID
+ if (aNode.id)
+ return "//" + tag + "[@id=" + this.quoteArgument(aNode.id) + "]";
+
+ // count the number of previous sibling nodes of the same tag
+ // (and possible also the same name)
+ let count = 0;
+ let nName = aNode.name || null;
+ for (let n = aNode; (n = n.previousSibling); )
+ if (n.localName == nLocalName && n.namespaceURI == nNamespaceURI &&
+ (!nName || n.name == nName))
+ count++;
+
+ // recurse until hitting either the document node or an ID'd node
+ return this.generate(aNode.parentNode) + "/" + tag +
+ (nName ? "[@name=" + this.quoteArgument(nName) + "]" : "") +
+ (count ? "[" + (count + 1) + "]" : "");
+ },
+
+ /**
+ * Resolves an XPath query generated by XPathGenerator.generate
+ */
+ resolve: function sss_xph_resolve(aDocument, aQuery) {
+ let xptype = aDocument.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+ return aDocument.evaluate(aQuery, aDocument, this.resolveNS, xptype, null).singleNodeValue;
+ },
+
+ /**
+ * Namespace resolver for the above XPath resolver
+ */
+ resolveNS: function sss_xph_resolveNS(aPrefix) {
+ return XPathGenerator.namespaceURIs[aPrefix] || null;
+ },
+
+ /**
+ * @returns valid XPath for the given node (usually just the local name itself)
+ */
+ escapeName: function sss_xph_escapeName(aName) {
+ // we can't just use the node's local name, if it contains
+ // special characters (cf. bug 485482)
+ return /^\w+$/.test(aName) ? aName :
+ "*[local-name()=" + this.quoteArgument(aName) + "]";
+ },
+
+ /**
+ * @returns a properly quoted string to insert into an XPath query
+ */
+ quoteArgument: function sss_xph_quoteArgument(aArg) {
+ return !/'/.test(aArg) ? "'" + aArg + "'" :
+ !/"/.test(aArg) ? '"' + aArg + '"' :
+ "concat('" + aArg.replace(/'+/g, "',\"$&\",'") + "')";
+ },
+
+ /**
+ * @returns an XPath query to all savable form field nodes
+ */
+ get restorableFormNodes() {
+ // for a comprehensive list of all available <INPUT> types see
+ // http://mxr.mozilla.org/mozilla-central/search?string=kInputTypeTable
+ let ignoreTypes = ["password", "hidden", "button", "image", "submit", "reset"];
+ // XXXzeniko work-around until lower-case has been implemented (bug 398389)
+ let toLowerCase = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"';
+ let ignore = "not(translate(@type, " + toLowerCase + ")='" +
+ ignoreTypes.join("' or translate(@type, " + toLowerCase + ")='") + "')";
+ let formNodesXPath = "//textarea|//select|//xhtml:textarea|//xhtml:select|" +
+ "//input[" + ignore + "]|//xhtml:input[" + ignore + "]";
+
+ delete this.restorableFormNodes;
+ return (this.restorableFormNodes = formNodesXPath);
+ }
+};
diff --git a/comm/suite/components/sessionstore/content/aboutSessionRestore.js b/comm/suite/components/sessionstore/content/aboutSessionRestore.js
new file mode 100644
index 0000000000..677bf20adc
--- /dev/null
+++ b/comm/suite/components/sessionstore/content/aboutSessionRestore.js
@@ -0,0 +1,291 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var gStateObject;
+var gTreeData;
+
+// Page initialization
+
+window.onload = function() {
+ // establish the event handlers for <tree> and <button> elements
+ var tabList = document.getElementById("tabList");
+ tabList.addEventListener("click", onListClick);
+ tabList.addEventListener("keydown", onListKeyDown);
+
+ document.getElementById("errorTryAgain")
+ .addEventListener("command", restoreSession);
+
+ document.getElementById("errorCancel")
+ .addEventListener("command", startNewSession);
+
+ // the crashed session state is kept inside a textbox so that SessionStore picks it up
+ // (for when the tab is closed or the session crashes right again)
+ var sessionData = document.getElementById("sessionData");
+ if (!sessionData.value) {
+ var ss = Cc["@mozilla.org/suite/sessionstartup;1"].getService(Ci.nsISessionStartup);
+ sessionData.value = ss.state;
+ if (!sessionData.value) {
+ document.getElementById("errorTryAgain").disabled = true;
+ return;
+ }
+ }
+ // make sure the data is tracked to be restored in case of a subsequent crash
+ sessionData.dispatchEvent(new UIEvent("input",
+ { bubbles: true, cancelable: true, view: window, detail: 0 }));
+
+ gStateObject = JSON.parse(sessionData.value);
+
+ initTreeView();
+
+ document.getElementById("errorTryAgain").focus();
+};
+
+function initTreeView() {
+ var tabList = document.getElementById("tabList");
+ var winLabel = tabList.getAttribute("_window_label");
+
+ gTreeData = [];
+ gStateObject.windows.forEach(function(aWinData, aIx) {
+ var winState = {
+ label: winLabel.replace("%S", (aIx + 1)),
+ open: true,
+ checked: true,
+ ix: aIx
+ };
+ winState.tabs = aWinData.tabs.map(function(aTabData) {
+ var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" };
+ var iconURL = aTabData.attributes && aTabData.attributes.image || null;
+ // don't initiate a connection just to fetch a favicon (see bug 462863)
+ if (/^https?:/.test(iconURL))
+ iconURL = "moz-anno:favicon:" + iconURL;
+ return {
+ label: entry.title || entry.url,
+ checked: true,
+ src: iconURL,
+ parent: winState
+ };
+ });
+ gTreeData.push(winState);
+ for (var tab of winState.tabs)
+ gTreeData.push(tab);
+ }, this);
+
+ tabList.view = treeView;
+ tabList.view.selection.select(0);
+}
+
+// User actions
+
+function restoreSession() {
+ document.getElementById("errorTryAgain").disabled = true;
+
+ // remove all unselected tabs from the state before restoring it
+ var ix = gStateObject.windows.length - 1;
+ for (var t = gTreeData.length - 1; t >= 0; t--) {
+ if (treeView.isContainer(t)) {
+ if (gTreeData[t].checked === 0)
+ // this window will be restored partially
+ gStateObject.windows[ix].tabs =
+ gStateObject.windows[ix].tabs.filter((aTabData, aIx) =>
+ gTreeData[t].tabs[aIx].checked);
+ else if (!gTreeData[t].checked)
+ // this window won't be restored at all
+ gStateObject.windows.splice(ix, 1);
+ ix--;
+ }
+ }
+ var stateString = JSON.stringify(gStateObject);
+
+ var ss = Cc["@mozilla.org/suite/sessionstore;1"].getService(Ci.nsISessionStore);
+ var top = getBrowserWindow();
+
+ // if there's only this page open, reuse the window for restoring the session
+ if (top.gBrowser.tabContainer.childNodes.length == 1) {
+ ss.setWindowState(top, stateString, true);
+ return;
+ }
+
+ // restore the session into a new window and close the current tab
+ var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all", "about:blank");
+ var tab = top.gBrowser.selectedTab;
+ newWindow.addEventListener("load", function newWindowLoad() {
+ newWindow.removeEventListener("load", newWindowLoad, true);
+ ss.setWindowState(newWindow, stateString, true);
+
+ top.gBrowser.removeTab(tab);
+ }, true);
+}
+
+function startNewSession() {
+ if (Services.prefs.getIntPref("browser.startup.page") == 1)
+ getBrowserWindow().BrowserHome();
+ else
+ getBrowserWindow().getBrowser().loadURI("about:blank");
+}
+
+function onListClick(aEvent) {
+ // don't react to right-clicks
+ if (aEvent.button == 2)
+ return;
+
+ var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.col) {
+ // restore this specific tab in the same window for middle-clicking
+ // or Ctrl+clicking on a tab's title
+ if ((aEvent.button == 1 || aEvent.ctrlKey) && cell.col.id == "title" &&
+ !treeView.isContainer(cell.row))
+ restoreSingleTab(cell.row, aEvent.shiftKey);
+ else if (cell.col.id == "restore")
+ toggleRowChecked(cell.row);
+ }
+}
+
+function onListKeyDown(aEvent) {
+ switch (aEvent.keyCode)
+ {
+ case KeyEvent.DOM_VK_SPACE:
+ toggleRowChecked(document.getElementById("tabList").currentIndex);
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ var ix = document.getElementById("tabList").currentIndex;
+ if (aEvent.ctrlKey && !treeView.isContainer(ix))
+ restoreSingleTab(ix, aEvent.shiftKey);
+ break;
+ }
+}
+
+// Helper functions
+
+function getBrowserWindow() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
+
+function toggleRowChecked(aIx) {
+ var item = gTreeData[aIx];
+ item.checked = !item.checked;
+ treeView.treeBox.invalidateRow(aIx);
+
+ function isChecked(aItem) {
+ return aItem.checked;
+ }
+
+ if (treeView.isContainer(aIx)) {
+ // (un)check all tabs of this window as well
+ for (var tab of item.tabs) {
+ tab.checked = item.checked;
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(tab));
+ }
+ }
+ else {
+ // update the window's checkmark as well (0 means "partially checked")
+ item.parent.checked = item.parent.tabs.every(isChecked) ? true :
+ item.parent.tabs.some(isChecked) ? 0 : false;
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent));
+ }
+
+ document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked);
+}
+
+function restoreSingleTab(aIx, aShifted) {
+ var tabbrowser = getBrowserWindow().gBrowser;
+ var newTab = tabbrowser.addTab();
+ var item = gTreeData[aIx];
+
+ var ss = Cc["@mozilla.org/suite/sessionstore;1"].getService(Ci.nsISessionStore);
+ var tabState = gStateObject.windows[item.parent.ix]
+ .tabs[aIx - gTreeData.indexOf(item.parent) - 1];
+ ss.setTabState(newTab, JSON.stringify(tabState));
+
+ // respect the preference as to whether to select the tab (the Shift key inverses)
+ if (Services.prefs.getBoolPref("browser.tabs.loadInBackground") != !aShifted)
+ tabbrowser.selectedTab = newTab;
+}
+
+// Tree controller
+
+var treeView = {
+ treeBox: null,
+ selection: null,
+
+ get rowCount() { return gTreeData.length; },
+ setTree: function(treeBox) { this.treeBox = treeBox; },
+ getCellText: function(idx, column) { return gTreeData[idx].label; },
+ isContainer: function(idx) { return "open" in gTreeData[idx]; },
+ getCellValue: function(idx, column){ return gTreeData[idx].checked; },
+ isContainerOpen: function(idx) { return gTreeData[idx].open; },
+ isContainerEmpty: function(idx) { return false; },
+ isSeparator: function(idx) { return false; },
+ isSorted: function() { return false; },
+ isEditable: function(idx, column) { return false; },
+ canDrop: function(idx, orientation, dt) { return false; },
+ getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; },
+
+ getParentIndex: function(idx) {
+ if (!this.isContainer(idx))
+ for (var t = idx - 1; t >= 0 ; t--)
+ if (this.isContainer(t))
+ return t;
+ return -1;
+ },
+
+ hasNextSibling: function(idx, after) {
+ var thisLevel = this.getLevel(idx);
+ for (var t = after + 1; t < gTreeData.length; t++)
+ if (this.getLevel(t) <= thisLevel)
+ return this.getLevel(t) == thisLevel;
+ return false;
+ },
+
+ toggleOpenState: function(idx) {
+ if (!this.isContainer(idx))
+ return;
+ var item = gTreeData[idx];
+ if (item.open) {
+ // remove this window's tab rows from the view
+ var thisLevel = this.getLevel(idx);
+ for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++);
+ var deletecount = t - idx - 1;
+ gTreeData.splice(idx + 1, deletecount);
+ this.treeBox.rowCountChanged(idx + 1, -deletecount);
+ }
+ else {
+ // add this window's tab rows to the view
+ var toinsert = gTreeData[idx].tabs;
+ for (var i = 0; i < toinsert.length; i++)
+ gTreeData.splice(idx + i + 1, 0, toinsert[i]);
+ this.treeBox.rowCountChanged(idx + 1, toinsert.length);
+ }
+ item.open = !item.open;
+ this.treeBox.invalidateRow(idx);
+ },
+
+ getCellProperties: function(idx, column) {
+ if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0)
+ return "partial";
+ if (column.id == "title")
+ return this.getImageSrc(idx, column) ? "icon" : "noicon";
+ return "";
+ },
+
+ getRowProperties: function(idx) {
+ var winState = gTreeData[idx].parent || gTreeData[idx];
+ return winState.ix % 2 != 0 ? "alternate" : "";
+ },
+
+ getImageSrc: function(idx, column) {
+ if (column.id == "title")
+ return gTreeData[idx].src || null;
+ return null;
+ },
+
+ getProgressMode : function(idx, column) { },
+ cycleHeader: function(column) { },
+ cycleCell: function(idx, column) { },
+ selectionChanged: function() { },
+ getColumnProperties: function(column) { return ""; }
+};
diff --git a/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml b/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml
new file mode 100644
index 0000000000..34c9a893ea
--- /dev/null
+++ b/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % restorepageDTD SYSTEM "chrome://communicator/locale/aboutSessionRestore.dtd">
+ %restorepageDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&restorepage.tabtitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/>
+ <link rel="stylesheet" href="chrome://communicator/skin/aboutSessionRestore.css" type="text/css" media="all"/>
+ <link rel="icon" type="image/png" href="chrome://global/skin/icons/question-16.png"/>
+
+ <script src="chrome://communicator/content/aboutSessionRestore.js"/>
+ </head>
+
+ <body dir="&locale.dir;">
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 id="errorTitleText">&restorepage.pagetitle;</h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText">&restorepage.issueDesc;</p>
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc">
+ <p>&restorepage.remedies;</p>
+ <ul>
+ <li>&restorepage.dueToChrome;</li>
+ <li>&restorepage.dueToContent;</li>
+ </ul>
+ </div>
+
+ <!-- Short Description -->
+ <div id="errorTrailerDesc">
+ <tree xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="tabList" flex="1" seltype="single" hidecolumnpicker="true"
+ _window_label="&restorepage.windowLabel;">
+ <treecols>
+ <treecol id="restore" type="checkbox" label="&restorepage.restoreHeader;"/>
+ <splitter class="tree-splitter"/>
+ <treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+ </div>
+ </div>
+
+ <!-- Buttons -->
+ <hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="buttons">
+ <button id="errorTryAgain" label="&restorepage.restoreButton;"
+ accesskey="&restorepage.restore.access;"/>
+ <button id="errorCancel" label="&restorepage.cancelButton;"
+ accesskey="&restorepage.cancel.access;"/>
+ </hbox>
+ <!-- holds the session data for when the tab is closed -->
+ <input type="hidden" id="sessionData"/>
+ </div>
+
+ </body>
+</html>
diff --git a/comm/suite/components/sessionstore/jar.mn b/comm/suite/components/sessionstore/jar.mn
new file mode 100644
index 0000000000..5a398c353e
--- /dev/null
+++ b/comm/suite/components/sessionstore/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/aboutSessionRestore.js (content/aboutSessionRestore.js)
+ content/communicator/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
diff --git a/comm/suite/components/sessionstore/moz.build b/comm/suite/components/sessionstore/moz.build
new file mode 100644
index 0000000000..9d8fd29468
--- /dev/null
+++ b/comm/suite/components/sessionstore/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsISessionStartup.idl",
+ "nsISessionStore.idl",
+]
+
+XPIDL_MODULE = "suitecommon"
+
+EXTRA_COMPONENTS += [
+ "nsSessionStartup.js",
+ "nsSessionStartup.manifest",
+ "nsSessionStore.js",
+]
+
+EXTRA_JS_MODULES.sessionstore = [
+ "XPathGenerator.jsm",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/sessionstore/nsISessionStartup.idl b/comm/suite/components/sessionstore/nsISessionStartup.idl
new file mode 100644
index 0000000000..018b0c1d4c
--- /dev/null
+++ b/comm/suite/components/sessionstore/nsISessionStartup.idl
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsISessionStore keeps track of the current browsing state - i.e.
+ * tab history, cookies, scroll state, form data, POSTDATA and window features
+ * - and allows to restore everything into one window.
+ */
+
+[scriptable, uuid(dd709821-820a-4d0d-b7e8-a566b32377ef)]
+interface nsISessionStartup: nsISupports
+{
+ // Get session state
+ readonly attribute jsval state;
+
+ /**
+ * Determine if session should be restored
+ */
+ boolean doRestore();
+
+ /**
+ * What type of session we're restoring.
+ * NO_SESSION There is no data available from the previous session
+ * RECOVER_SESSION The last session crashed. It will either be restored or
+ * about:sessionrestore will be shown.
+ * RESUME_SESSION The previous session should be restored at startup
+ * DEFER_SESSION The previous session is fine, but it shouldn't be restored
+ * without explicit action (with the exception of pinned tabs)
+ */
+ const unsigned long NO_SESSION = 0;
+ const unsigned long RECOVER_SESSION = 1;
+ const unsigned long RESUME_SESSION = 2;
+ const unsigned long DEFER_SESSION = 3;
+
+ readonly attribute unsigned long sessionType;
+};
diff --git a/comm/suite/components/sessionstore/nsISessionStore.idl b/comm/suite/components/sessionstore/nsISessionStore.idl
new file mode 100644
index 0000000000..2fc2f22175
--- /dev/null
+++ b/comm/suite/components/sessionstore/nsISessionStore.idl
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsINode;
+
+/**
+ * nsISessionStore keeps track of the current browsing state - i.e.
+ * tab history, cookies, scroll state, form data, POSTDATA and window features
+ * - and allows to restore everything into one browser window.
+ *
+ * The nsISessionStore API operates mostly on browser windows and the tabbrowser
+ * tabs contained in them:
+ *
+ * * "Browser windows" are those DOM windows having loaded
+ * chrome://navigator/content/navigator.xul . From overlays you can just pass
+ * the global |window| object to the API, though (or |top| from a sidebar).
+ * From elsewhere you can get browser windows through the nsIWindowMediator
+ * by looking for "navigator:browser" windows.
+ *
+ * * "Tabbrowser tabs" are all the child nodes of a browser window's
+ * |getBrowser().tabContainer| such as e.g. |getBrowser().selectedTab|.
+ */
+
+[scriptable, uuid(27a8bd2b-dd76-4cee-82eb-a25f6a94478f)]
+interface nsISessionStore : nsISupports
+{
+ /**
+ * Initialize the service
+ */
+ void init(in nsIDOMWindow aWindow);
+
+ /**
+ * Is it possible to restore the previous session. Will always be false when
+ * in Private Browsing mode.
+ */
+ attribute boolean canRestoreLastSession;
+
+ /**
+ * Restore the previous session if possible. This will not overwrite the
+ * current session. Instead the previous session will be merged into the
+ * current session. Current windows will be reused if they were windows that
+ * pinned tabs were previously restored into. New windows will be opened as
+ * needed.
+ *
+ * Note: This will throw if there is no previous state to restore. Check with
+ * canRestoreLastSession first to avoid thrown errors.
+ */
+ void restoreLastSession();
+
+ /**
+ * Get the current browsing state.
+ * @returns a JSON string representing the session state.
+ */
+ AString getBrowserState();
+
+ /**
+ * Set the browsing state.
+ * This will immediately restore the state of the whole application to the state
+ * passed in, *replacing* the current session.
+ *
+ * @param aState is a JSON string representing the session state.
+ */
+ void setBrowserState(in AString aState);
+
+ /**
+ * @param aWindow is the browser window whose state is to be returned.
+ *
+ * @returns a JSON string representing a session state with only one window.
+ */
+ AString getWindowState(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window whose state is to be set.
+ * @param aState is a JSON string representing a session state.
+ * @param aOverwrite boolean overwrite existing tabs
+ */
+ void setWindowState(in nsIDOMWindow aWindow, in AString aState, in boolean aOverwrite);
+
+ /**
+ * @param aTab is the tabbrowser tab whose state is to be returned.
+ *
+ * @returns a JSON string representing the state of the tab
+ * (note: doesn't contain cookies - if you need them, use getWindowState instead).
+ */
+ AString getTabState(in nsINode aTab);
+
+ /**
+ * @param aTab is the tabbrowser tab whose state is to be set.
+ * @param aState is a JSON string representing a session state.
+ */
+ void setTabState(in nsINode aTab, in AString aState);
+
+ /**
+ * Duplicates a given tab as thoroughly as possible.
+ *
+ * @param aWindow is the browser window into which the tab will be duplicated.
+ * Pass null if you want to create a new window.
+ * @param aTab is the tabbrowser tab to duplicate (can be from a different window).
+ * @param aDelta is the offset to the history entry that you want to load.
+ * @param aRelated is a flag to be passed to addTab().
+ * @returns a reference to the newly created tab, or null if opening a window.
+ */
+ nsINode duplicateTab(in nsIDOMWindow aWindow, in nsINode aTab,
+ [optional] in long aDelta,
+ [optional] in boolean aRelated);
+
+ /**
+ * Get the number of restore-able tabs for a browser window
+ */
+ unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
+
+ /**
+ * Get closed tab data
+ *
+ * @param aWindow is the browser window for which to get closed tab data
+ * @returns a JSON string representing the list of closed tabs.
+ */
+ AString getClosedTabData(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window to reopen a closed tab in.
+ * @param aIndex is the index of the tab to be restored (FIFO ordered).
+ * @returns a reference to the reopened tab.
+ */
+ nsINode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aWindow is the browser window associated with the closed tab.
+ * @param aIndex is the index of the closed tab to be removed (FIFO ordered).
+ */
+ nsINode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * Get the number of restore-able windows
+ */
+ unsigned long getClosedWindowCount();
+
+ /**
+ * Get closed windows data
+ *
+ * @returns a JSON string representing the list of closed windows.
+ */
+ AString getClosedWindowData();
+
+ /**
+ * @param aIndex is the index of the windows to be restored (FIFO ordered).
+ * @returns the nsIDOMWindow object of the reopened window
+ */
+ nsIDOMWindow undoCloseWindow(in unsigned long aIndex);
+
+ /**
+ * @param aIndex is the index of the closed window to be removed (FIFO ordered).
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * when aIndex does not map to a closed window
+ */
+ nsINode forgetClosedWindow(in unsigned long aIndex);
+
+ /**
+ * @param aWindow is the window to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getWindowValue(in nsIDOMWindow aWindow, in AString aKey);
+
+ /**
+ * @param aWindow is the browser window to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use toSource/eval before setting JS objects).
+ */
+ void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in AString aStringValue);
+
+ /**
+ * @param aWindow is the browser window to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteWindowValue(in nsIDOMWindow aWindow, in AString aKey);
+
+ /**
+ * @param aTab is the tabbrowser tab to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getTabValue(in nsINode aTab, in AString aKey);
+
+ /**
+ * @param aTab is the tabbrowser tab to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use toSource/eval before setting JS objects).
+ */
+ void setTabValue(in nsINode aTab, in AString aKey, in AString aStringValue);
+
+ /**
+ * @param aTab is the tabbrowser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteTabValue(in nsINode aTab, in AString aKey);
+
+ /**
+ * @param aName is the name of the attribute to save/restore for all tabbrowser tabs.
+ */
+ void persistTabAttribute(in AString aName);
+
+ /**
+ * Returns true if the last window was closed and should be restored
+ *
+ * @returns true if the last window was closed and should be restored
+ */
+ boolean doRestoreLastWindow();
+};
diff --git a/comm/suite/components/sessionstore/nsSessionStartup.js b/comm/suite/components/sessionstore/nsSessionStartup.js
new file mode 100644
index 0000000000..32494d1f27
--- /dev/null
+++ b/comm/suite/components/sessionstore/nsSessionStartup.js
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Session Storage and Restoration
+ *
+ * Overview
+ * This service reads user's session file at startup, and makes a determination
+ * as to whether the session should be restored. It will restore the session
+ * under the circumstances described below.
+ *
+ * Crash Detection
+ * The session file stores a session.state property, that
+ * indicates whether the browser is currently running. When the browser shuts
+ * down, the field is changed to "stopped". At startup, this field is read, and
+ * if it's value is "running", then it's assumed that the browser had previously
+ * crashed, or at the very least that something bad happened, and that we should
+ * restore the session.
+ *
+ * Forced Restarts
+ * In the event that a restart is required due to application update or extension
+ * installation, set the browser.sessionstore.resume_session_once pref to true,
+ * and the session will be restored the next time the browser starts.
+ *
+ * Always Resume
+ * This service will always resume the session if the integer pref
+ * browser.startup.page is set to 3.
+*/
+
+/* :::::::: Constants and Helpers ::::::::::::::: */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const STATE_RUNNING_STR = "running";
+
+function debug(aMsg) {
+ Services.console.logStringMessage("SessionStartup: " + aMsg);
+}
+
+/* :::::::: The Service ::::::::::::::: */
+
+function SessionStartup() {
+}
+
+SessionStartup.prototype = {
+
+ // the state to restore at startup
+ _initialState: null,
+ _sessionType: Ci.nsISessionStartup.NO_SESSION,
+
+/* ........ Global Event Handlers .............. */
+
+ /**
+ * Initialize the component
+ */
+ init: function sss_init() {
+ // get file references
+ let sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ sessionFile.append("sessionstore.json");
+
+ let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+ let doResumeSession = doResumeSessionOnce ||
+ Services.prefs.getIntPref("browser.startup.page") == 3;
+
+ var resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
+
+ // only continue if the session file exists
+ if (!sessionFile.exists())
+ return;
+
+ // get string containing session state
+ let iniString = this._readStateFile(sessionFile);
+ if (!iniString)
+ return;
+
+ try {
+ // parse the session state into JS objects
+ this._initialState = JSON.parse(iniString);
+ }
+ catch (ex) {
+ doResumeSession = false;
+ debug("The session file is invalid: " + ex);
+ }
+
+ // If this is a normal restore then throw away any previous session
+ if (!doResumeSessionOnce && this._initialState)
+ delete this._initialState.lastSessionState;
+
+ let lastSessionCrashed =
+ this._initialState && this._initialState.session &&
+ this._initialState.session.state &&
+ this._initialState.session.state == STATE_RUNNING_STR;
+
+ // set the startup type
+ if (lastSessionCrashed && resumeFromCrash)
+ this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
+ else if (!lastSessionCrashed && doResumeSession)
+ this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
+ else if (this._initialState)
+ this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
+ else
+ this._initialState = null; // reset the state
+
+ if (this.doRestore()) {
+ // wait for the first browser window to open
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+ }
+ },
+
+ /**
+ * Handle notifications
+ */
+ observe: function sss_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "app-startup":
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "quit-application", true);
+ break;
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, "final-ui-startup");
+ Services.obs.removeObserver(this, "quit-application");
+ this.init();
+ break;
+ case "quit-application":
+ // no reason for initializing at this point (cf. bug 409115)
+ Services.obs.removeObserver(this, "final-ui-startup");
+ Services.obs.removeObserver(this, "quit-application");
+ break;
+ case "sessionstore-windows-restored":
+ // no need in repeating this, since session type won't change
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ // free _initialState after nsSessionStore is done with it
+ this._initialState = null;
+ // reset session type after restore
+ this._sessionType = Ci.nsISessionStartup.NO_SESSION;
+ break;
+ }
+ },
+
+/* ........ Public API ................*/
+
+ /**
+ * Get the session state as a string
+ */
+ get state() {
+ return this._initialState;
+ },
+
+ /**
+ * Determine whether there is a pending session restore.
+ * @returns bool
+ */
+ doRestore: function sss_doRestore() {
+ return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
+ this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
+ },
+
+ /**
+ * Get the type of pending session store, if any.
+ */
+ get sessionType() {
+ return this._sessionType;
+ },
+
+/* ........ Storage API .............. */
+
+ /**
+ * Reads a session state file into a string and lets
+ * observers modify the state before it's being used
+ *
+ * @param aFile is any nsIFile
+ * @returns a session state string
+ */
+ _readStateFile: function sss_readStateFile(aFile) {
+ var stateString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ stateString.data = this._readFile(aFile) || "";
+
+ Services.obs.notifyObservers(stateString, "sessionstore-state-read");
+
+ return stateString.data;
+ },
+
+ /**
+ * reads a file into a string
+ * @param aFile
+ * nsIFile
+ * @returns string
+ */
+ _readFile: function sss_readFile(aFile) {
+ try {
+ var stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(aFile, 0x01, 0, 0);
+ var cvStream = Cc["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Ci.nsIConverterInputStream);
+ cvStream.init(stream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ var content = "";
+ var data = {};
+ while (cvStream.readString(4096, data)) {
+ content += data.value;
+ }
+ cvStream.close();
+
+ return content.replace(/\r\n?/g, "\n");
+ }
+ catch (ex) { Cu.reportError(ex); }
+
+ return null;
+ },
+
+ /* ........ QueryInterface .............. */
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISessionStartup]),
+ classID: Components.ID("{4e6c1112-57b6-44ba-adf9-99fb573b0a30}")
+
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]);
diff --git a/comm/suite/components/sessionstore/nsSessionStartup.manifest b/comm/suite/components/sessionstore/nsSessionStartup.manifest
new file mode 100644
index 0000000000..c1ade9aa87
--- /dev/null
+++ b/comm/suite/components/sessionstore/nsSessionStartup.manifest
@@ -0,0 +1,11 @@
+# This components must restrict its registration for the app-startup category
+# to the specific list of apps that use it so it doesn't get loaded in xpcshell.
+# Thus we restrict it to these apps:
+#
+# suite {4e6c1112-57b6-44ba-adf9-99fb573b0a30}
+
+component {4e6c1112-57b6-44ba-adf9-99fb573b0a30} nsSessionStartup.js
+contract @mozilla.org/suite/sessionstartup;1 {4e6c1112-57b6-44ba-adf9-99fb573b0a30}
+category app-startup SessionStartup service,@mozilla.org/suite/sessionstartup;1
+component {d37ccdf1-496f-4135-9575-037180af010d} nsSessionStore.js
+contract @mozilla.org/suite/sessionstore;1 {d37ccdf1-496f-4135-9575-037180af010d}
diff --git a/comm/suite/components/sessionstore/nsSessionStore.js b/comm/suite/components/sessionstore/nsSessionStore.js
new file mode 100644
index 0000000000..e469af96bb
--- /dev/null
+++ b/comm/suite/components/sessionstore/nsSessionStore.js
@@ -0,0 +1,4174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Session Storage and Restoration
+ *
+ * Overview
+ * This service keeps track of a user's session, storing the various bits
+ * required to return the browser to its current state. The relevant data is
+ * stored in memory, and is periodically saved to disk in a file in the
+ * profile directory. The service is started at first window load, in
+ * delayedStartup, and will restore the session from the data received from
+ * the nsSessionStartup service.
+ */
+
+/* :::::::: Constants and Helpers ::::::::::::::: */
+
+const STATE_STOPPED = 0;
+const STATE_RUNNING = 1;
+const STATE_QUITTING = -1;
+
+const STATE_STOPPED_STR = "stopped";
+const STATE_RUNNING_STR = "running";
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+
+const PRIVACY_NONE = 0;
+const PRIVACY_ENCRYPTED = 1;
+const PRIVACY_FULL = 2;
+
+const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
+const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
+
+// global notifications observed
+const OBSERVING = [
+ "domwindowclosed",
+ "quit-application-requested", "quit-application-granted", "quit-application",
+ "browser-lastwindow-close-granted", "browser:purge-session-history"
+];
+
+/*
+XUL Window properties to (re)store
+Restored in restoreDimensions()
+*/
+const WINDOW_ATTRIBUTES = {
+ width: "outerWidth",
+ height: "outerHeight",
+ screenX: "screenX",
+ screenY: "screenY",
+ sizemode: "windowState"
+};
+
+/*
+Hideable window features to (re)store
+Restored in restoreWindowFeatures()
+*/
+const WINDOW_HIDEABLE_FEATURES = [
+ "menubar", "toolbar", "locationbar",
+ "personalbar", "statusbar", "scrollbars"
+];
+
+/*
+docShell capabilities to (re)store
+Restored in restoreHistory()
+eg: browser.docShell["allow" + aCapability] = false;
+
+XXX keep these in sync with all the attributes starting
+ with "allow" in /docshell/base/nsIDocShell.idl
+*/
+const CAPABILITIES = [
+ "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images",
+ "DNSPrefetch", "Auth", "WindowControl"
+];
+
+// These are tab events that we listen to.
+const TAB_EVENTS = ["TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide"];
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "SecMan",
+ "@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager");
+XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
+ "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator",
+ "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+ChromeUtils.defineModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+ChromeUtils.defineModuleGetter(this, "XPathGenerator",
+ "resource:///modules/sessionstore/XPathGenerator.jsm");
+
+function debug(aMsg) {
+ Services.console.logStringMessage("SessionStore: " + aMsg);
+}
+
+/* :::::::: The Service ::::::::::::::: */
+
+function SessionStoreService() {
+ XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () {
+ return Services.prefs.getBranch("browser.");
+ });
+
+ // minimal interval between two save operations (in milliseconds)
+ XPCOMUtils.defineLazyGetter(this, "_interval", function () {
+ // used often, so caching/observing instead of fetching on-demand
+ this._prefBranch.addObserver("sessionstore.interval", this, true);
+ return this._prefBranch.getIntPref("sessionstore.interval");
+ });
+
+ // when crash recovery is disabled, session data is not written to disk
+ XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function () {
+ // get crash recovery state from prefs and allow for proper reaction to state changes
+ this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true);
+ return this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
+ });
+}
+
+SessionStoreService.prototype = {
+ classID: Components.ID("{d37ccdf1-496f-4135-9575-037180af010d}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ // xul:tab attributes to (re)store (extensions might want to hook in here);
+ // the favicon is always saved for the about:sessionrestore page
+ xulAttributes: {"image": true},
+
+ // set default load state
+ _loadState: STATE_STOPPED,
+
+ // During the initial restore and setBrowserState calls tracks the number of
+ // windows yet to be restored
+ _restoreCount: -1,
+
+ // whether a setBrowserState call is in progress
+ _browserSetState: false,
+
+ // time in milliseconds (Date.now()) when the session was last written to file
+ _lastSaveTime: 0,
+
+ // time in milliseconds when the session was started (saved across sessions),
+ // defaults to now if no session was restored or timestamp doesn't exist
+ _sessionStartTime: Date.now(),
+
+ // states for all currently opened windows
+ _windows: {},
+
+ // states for all recently closed windows
+ _closedWindows: [],
+
+ // collection of session states yet to be restored
+ _statesToRestore: {},
+
+ // counts the number of crashes since the last clean start
+ _recentCrashes: 0,
+
+ // whether the last window was closed and should be restored
+ _restoreLastWindow: false,
+
+ // tabs to restore in order
+ _tabsToRestore: { visible: [], hidden: [] },
+ _tabsRestoringCount: 0,
+
+ // number of tabs to restore concurrently, pref controlled.
+ _maxConcurrentTabRestores: null,
+
+ // The state from the previous session (after restoring pinned tabs). This
+ // state is persisted and passed through to the next session during an app
+ // restart to make the third party add-on warning not trash the deferred
+ // session
+ _lastSessionState: null,
+
+ // Whether we've been initialized
+ _initialized: false,
+
+ // Mapping from legacy docshellIDs to docshellUUIDs.
+ _docshellUUIDMap: new Map(),
+
+/* ........ Public Getters .............. */
+
+ get canRestoreLastSession() {
+ // Always disallow restoring the previous session when in private browsing
+ return this._lastSessionState;
+ },
+
+ set canRestoreLastSession(val) {
+ // Cheat a bit; only allow false.
+ if (!val)
+ this._lastSessionState = null;
+ },
+
+/* ........ Global Event Handlers .............. */
+
+ /**
+ * Initialize the component
+ */
+ initService: function() {
+ OBSERVING.forEach(function(aTopic) {
+ Services.obs.addObserver(this, aTopic, true);
+ }, this);
+
+ this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
+ this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
+
+ this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
+ this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
+
+ // this pref is only read at startup, so no need to observe it
+ this._sessionhistory_max_entries =
+ this._prefBranch.getIntPref("sessionhistory.max_entries");
+
+ this._maxConcurrentTabRestores =
+ this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs");
+ this._prefBranch.addObserver("sessionstore.max_concurrent_tabs", this, true);
+
+ // Make sure gRestoreTabsProgressListener has a reference to sessionstore
+ // so that it can make calls back in
+ gRestoreTabsProgressListener.ss = this;
+
+ // get file references
+ this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ this._sessionFileBackup = this._sessionFile.clone();
+ this._sessionFile.append("sessionstore.json");
+ this._sessionFileBackup.append("sessionstore.bak");
+
+ // get string containing session state
+ var ss = Cc["@mozilla.org/suite/sessionstartup;1"]
+ .getService(Ci.nsISessionStartup);
+ try {
+ if (ss.sessionType != Ci.nsISessionStartup.NO_SESSION)
+ this._initialState = ss.state;
+ }
+ catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok
+
+ if (this._initialState) {
+ try {
+ // If we're doing a DEFERRED session, then we want to pull pinned tabs
+ // out so they can be restored.
+ if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
+ let [iniState, remainingState] = this._prepDataForDeferredRestore(this._initialState);
+ // If we have a iniState with windows, that means that we have windows
+ // with app tabs to restore.
+ if (iniState.windows.length)
+ this._initialState = iniState;
+ else
+ this._initialState = null;
+ if (remainingState.windows.length)
+ this._lastSessionState = remainingState;
+ }
+ else {
+ // Get the last deferred session in case the user still wants to
+ // restore it
+ this._lastSessionState = this._initialState.lastSessionState;
+
+ let lastSessionCrashed =
+ this._initialState.session && this._initialState.session.state &&
+ this._initialState.session.state == STATE_RUNNING_STR;
+ if (lastSessionCrashed) {
+ this._recentCrashes = (this._initialState.session &&
+ this._initialState.session.recentCrashes || 0) + 1;
+
+ if (this._needsRestorePage(this._initialState, this._recentCrashes)) {
+ // replace the crashed session with a restore-page-only session
+ let pageData = {
+ url: "about:sessionrestore",
+ triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL,
+ formdata: { "#sessionData": JSON.stringify(this._initialState) }
+ };
+ this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] };
+ }
+ }
+
+ // Load the session start time from the previous state
+ this._sessionStartTime = this._initialState.session &&
+ this._initialState.session.startTime ||
+ this._sessionStartTime;
+
+ // make sure that at least the first window doesn't have anything hidden
+ delete this._initialState.windows[0].hidden;
+ // Since nothing is hidden in the first window, it cannot be a popup
+ delete this._initialState.windows[0].isPopup;
+ // clear any lastSessionWindowID attributes since those don't matter
+ // during normal restore
+ this._initialState.windows.forEach(function(aWindow) {
+ delete aWindow.__lastSessionWindowID;
+ });
+ }
+ }
+ catch (ex) { debug("The session file is invalid: " + ex); }
+ }
+
+ if (this._resume_from_crash) {
+ // create a backup if the session data file exists
+ try {
+ if (this._sessionFileBackup.exists())
+ this._sessionFileBackup.remove(false);
+ if (this._sessionFile.exists())
+ this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+ }
+ catch (ex) { Cu.reportError(ex); } // file was write-locked?
+ }
+
+ // at this point, we've as good as resumed the session, so we can
+ // clear the resume_session_once flag, if it's set
+ if (this._loadState != STATE_QUITTING &&
+ this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
+ this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
+
+ this._initialized = true;
+ },
+
+ /**
+ * Start tracking a window.
+ * This function also initializes the component if it's not already
+ * initialized.
+ */
+ init: function sss_init(aWindow) {
+ // Initialize the service if needed.
+ if (!this._initialized)
+ this.initService();
+
+ if (aWindow) {
+ this.onLoad(aWindow);
+ } else if (this._loadState == STATE_STOPPED) {
+ // If init is being called with a null window, it's possible that we
+ // just want to tell sessionstore that a session is live (as is the case
+ // with starting Firefox with -private, for example; see bug 568816),
+ // so we should mark the load state as running to make sure that
+ // things like setBrowserState calls will succeed in restoring the session.
+ this._loadState = STATE_RUNNING;
+ }
+ },
+ /**
+ * Called on application shutdown, after notifications:
+ * quit-application-granted, quit-application
+ */
+ _uninit: function sss_uninit() {
+ // save all data for session resuming
+ this.saveState(true);
+
+ // clear out _tabsToRestore in case it's still holding refs
+ this._tabsToRestore.visible = null;
+ this._tabsToRestore.hidden = null;
+
+ // remove the ref to us from the progress listener
+ gRestoreTabsProgressListener.ss = null;
+
+ // Make sure to break our cycle with the save timer
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ }
+ },
+
+ /**
+ * Handle notifications
+ */
+ observe: function sss_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "domwindowclosed": // catch closed windows
+ this.onClose(aSubject);
+ break;
+ case "quit-application-requested":
+ // get a current snapshot of all windows
+ this._forEachBrowserWindow(function(aWindow) {
+ this._collectWindowData(aWindow);
+ });
+ DirtyWindows.clear();
+ break;
+ case "quit-application-granted":
+ // freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+ break;
+ case "browser-lastwindow-close-granted":
+ // last browser window is quitting.
+ // remember to restore the last window when another browser window is openend
+ // do not account for pref(resume_session_once) at this point, as it might be
+ // set by another observer getting this notice after us
+ this._restoreLastWindow = true;
+ break;
+ case "quit-application":
+ if (aData == "restart" && !this._isSwitchingProfile()) {
+ this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
+ // The browser:purge-session-history notification fires after the
+ // quit-application notification so unregister the
+ // browser:purge-session-history notification to prevent clearing
+ // session data on disk on a restart. It is also unnecessary to
+ // perform any other sanitization processing on a restart as the
+ // browser is about to exit anyway.
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ }
+
+ if (aData != "restart") {
+ // Throw away the previous session on shutdown
+ this._lastSessionState = null;
+ }
+
+ this._loadState = STATE_QUITTING; // just to be sure
+ this._uninit();
+ break;
+ case "browser:purge-session-history": // catch sanitization
+ this._clearDisk();
+ // If the browser is shutting down, simply return after clearing the
+ // session data on disk as this notification fires after the
+ // quit-application notification so the browser is about to exit.
+ if (this._loadState == STATE_QUITTING)
+ return;
+ this._lastSessionState = null;
+ let openWindows = {};
+ this._forEachBrowserWindow(function(aWindow) {
+ //Hide "Restore Last Session" menu item
+ let restoreItem = aWindow.document.getElementById("historyRestoreLastSession");
+ restoreItem.setAttribute("disabled", "true");
+
+ Array.from(aWindow.getBrowser().tabs).forEach(function(aTab) {
+ delete aTab.linkedBrowser.__SS_data;
+ delete aTab.linkedBrowser.__SS_formDataSaved;
+ if (aTab.linkedBrowser.__SS_restoreState)
+ this._resetTabRestoringState(aTab);
+ });
+ openWindows[aWindow.__SSi] = true;
+ });
+ // also clear all data about closed tabs and windows
+ for (let ix in this._windows) {
+ if (ix in openWindows) {
+ this._windows[ix]._closedTabs = [];
+ }
+ else {
+ delete this._windows[ix];
+ }
+ }
+ // also clear all data about closed windows
+ this._closedWindows = [];
+ // give the tabbrowsers a chance to clear their histories first
+ if (this._getMostRecentBrowserWindow())
+ Services.tm.mainThread.dispatch(this.saveState.bind(this, true),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ else if (this._loadState == STATE_RUNNING)
+ this.saveState(true);
+ break;
+ case "nsPref:changed": // catch pref changes
+ switch (aData) {
+ // if the user decreases the max number of closed tabs they want
+ // preserved update our internal states to match that max
+ case "sessionstore.max_tabs_undo":
+ this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
+ for (let ix in this._windows) {
+ this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
+ }
+ break;
+ case "sessionstore.max_windows_undo":
+ this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
+ this._capClosedWindows();
+ break;
+ case "sessionstore.interval":
+ this._interval = this._prefBranch.getIntPref("sessionstore.interval");
+ // reset timer and save
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ }
+ this.saveStateDelayed(null, -1);
+ break;
+ case "sessionstore.resume_from_crash":
+ this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
+ // either create the file with crash recovery information or remove it
+ // (when _loadState is not STATE_RUNNING, that file is used for session resuming instead)
+ if (this._resume_from_crash)
+ this.saveState(true);
+ else if (this._loadState == STATE_RUNNING)
+ this._clearDisk();
+ break;
+ case "sessionstore.max_concurrent_tabs":
+ this._maxConcurrentTabRestores =
+ this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs");
+ break;
+ }
+ break;
+ case "timer-callback": // timer call back for delayed saving
+ this._saveTimer = null;
+ this.saveState();
+ break;
+ }
+ },
+
+/* ........ Window Event Handlers .............. */
+
+ /**
+ * Implement nsIDOMEventListener for handling various window and tab events
+ */
+ handleEvent: function sss_handleEvent(aEvent) {
+ var win = aEvent.currentTarget.ownerDocument.defaultView;
+ switch (aEvent.type) {
+ case "load":
+ // If __SS_restore_data is set, then we need to restore the document
+ // (form data, scrolling, etc.). This will only happen when a tab is
+ // first restored.
+ if (aEvent.currentTarget.__SS_restore_data)
+ this.restoreDocument(win, aEvent.currentTarget, aEvent);
+ // We still need to call onTabLoad, so fall through to "pageshow" case.
+ case "pageshow":
+ this.onTabLoad(win, aEvent.currentTarget, aEvent);
+ break;
+ case "input":
+ case "DOMAutoComplete":
+ this.onTabInput(win, aEvent.currentTarget);
+ break;
+ case "TabOpen":
+ this.onTabAdd(win, aEvent.originalTarget);
+ break;
+ case "TabClose":
+ // aEvent.detail determines if the tab was closed by moving to a different window
+ if (!aEvent.detail)
+ this.onTabClose(win, aEvent.originalTarget);
+ this.onTabRemove(win, aEvent.originalTarget);
+ break;
+ case "TabSelect":
+ this.onTabSelect(win);
+ break;
+ case "TabShow":
+ this.onTabShow(aEvent.originalTarget);
+ break;
+ case "TabHide":
+ this.onTabHide(aEvent.originalTarget);
+ break;
+ }
+ },
+
+ /**
+ * If it's the first window load since app start...
+ * - determine if we're reloading after a crash or a forced-restart
+ * - restore window state
+ * - restart downloads
+ * Set up event listeners for this window's tabs
+ * @param aWindow
+ * Window reference
+ */
+ onLoad: function sss_onLoad(aWindow) {
+ // return if window has already been initialized
+ if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
+ return;
+
+ // ignore non-browser windows and windows opened while shutting down
+ if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" ||
+ this._loadState == STATE_QUITTING)
+ return;
+
+ // assign it a unique identifier (timestamp)
+ aWindow.__SSi = "window" + Date.now();
+
+ // and create its data object
+ this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [] };
+
+ if (!this._isWindowLoaded(aWindow))
+ this._windows[aWindow.__SSi]._restoring = true;
+ if (!aWindow.toolbar.visible)
+ this._windows[aWindow.__SSi].isPopup = true;
+
+ // perform additional initialization when the first window is loading
+ if (this._loadState == STATE_STOPPED) {
+ this._loadState = STATE_RUNNING;
+ this._lastSaveTime = Date.now();
+
+ // restore a crashed session resp. resume the last session if requested
+ if (this._initialState) {
+ // make sure that the restored tabs are first in the window
+ this._initialState._firstTabs = true;
+ this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0;
+ this.restoreWindow(aWindow, this._initialState,
+ this._isCmdLineEmpty(aWindow));
+ delete this._initialState;
+
+ // _loadState changed from "stopped" to "running"
+ // force a save operation so that crashes happening during startup are correctly counted
+ this.saveState(true);
+ }
+ else {
+ // Nothing to restore, notify observers things are complete.
+ this.windowToFocus = aWindow;
+ Services.tm.mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
+
+ // the next delayed save request should execute immediately
+ this._lastSaveTime -= this._interval;
+ }
+ }
+ // this window was opened by _openWindowWithState
+ else if (!this._isWindowLoaded(aWindow)) {
+ let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1;
+ this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp);
+ }
+ else if (this._restoreLastWindow && aWindow.toolbar.visible &&
+ this._closedWindows.length) {
+ // default to the most-recently closed window
+ // don't use popup windows
+ let closedWindowState = null;
+ let closedWindowIndex;
+ for (let i = 0; i < this._closedWindows.length; i++) {
+ // Take the first non-popup, point our object at it, and break out.
+ if (!this._closedWindows[i].isPopup) {
+ closedWindowState = this._closedWindows[i];
+ closedWindowIndex = i;
+ break;
+ }
+ }
+
+ if (closedWindowState) {
+ let newWindowState;
+ if (AppConstants.platform == "macosx" || !this._doResumeSession()) {
+ // We want to split the window up into pinned tabs and unpinned tabs.
+ // Pinned tabs should be restored. If there are any remaining tabs,
+ // they should be added back to _closedWindows.
+ // We'll cheat a little bit and reuse _prepDataForDeferredRestore
+ // even though it wasn't built exactly for this.
+ let [appTabsState, normalTabsState] =
+ this._prepDataForDeferredRestore({ windows: [closedWindowState] });
+
+ // These are our pinned tabs, which we should restore
+ if (appTabsState.windows.length) {
+ newWindowState = appTabsState.windows[0];
+ delete newWindowState.__lastSessionWindowID;
+ }
+
+ // In case there were no unpinned tabs, remove the window from _closedWindows
+ if (!normalTabsState.windows.length) {
+ this._closedWindows.splice(closedWindowIndex, 1);
+ }
+ // Or update _closedWindows with the modified state
+ else {
+ delete normalTabsState.windows[0].__lastSessionWindowID;
+ this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
+ }
+ }
+ else {
+ // If we're just restoring the window, make sure it gets removed from
+ // _closedWindows.
+ this._closedWindows.splice(closedWindowIndex, 1);
+ newWindowState = closedWindowState;
+ delete newWindowState.hidden;
+ }
+
+ if (newWindowState) {
+ // Ensure that the window state isn't hidden
+ this._restoreCount = 1;
+ let state = { windows: [newWindowState] };
+ this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow));
+ }
+ }
+ // we actually restored the session just now.
+ this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
+ }
+ if (this._restoreLastWindow && aWindow.toolbar.visible) {
+ // always reset (if not a popup window)
+ // we don't want to restore a window directly after, for example,
+ // undoCloseWindow was executed.
+ this._restoreLastWindow = false;
+ }
+
+ var tabbrowser = aWindow.getBrowser();
+
+ // add tab change listeners to all already existing tabs
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
+ }
+ // notification of tab add/remove/selection/show/hide
+ TAB_EVENTS.forEach(function(aEvent) {
+ tabbrowser.tabContainer.addEventListener(aEvent, this, true);
+ }, this);
+ },
+
+ /**
+ * On window close...
+ * - remove event listeners from tabs
+ * - save all window data
+ * @param aWindow
+ * Window reference
+ */
+ onClose: function sss_onClose(aWindow) {
+ // this window was about to be restored - conserve its original data, if any
+ let isFullyLoaded = this._isWindowLoaded(aWindow);
+ if (!isFullyLoaded) {
+ if (!aWindow.__SSi)
+ aWindow.__SSi = "window" + Date.now();
+ this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
+ delete this._statesToRestore[aWindow.__SS_restoreID];
+ delete aWindow.__SS_restoreID;
+ }
+
+ // ignore windows not tracked by SessionStore
+ if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
+ return;
+ }
+
+ if (this.windowToFocus && this.windowToFocus == aWindow) {
+ delete this.windowToFocus;
+ }
+
+ var tabbrowser = aWindow.getBrowser();
+
+ TAB_EVENTS.forEach(function(aEvent) {
+ tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
+ }, this);
+
+ // remove the progress listener for this window
+ try {
+ tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener);
+ } catch (ex) {};
+
+ let winData = this._windows[aWindow.__SSi];
+ if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down
+ // update all window data for a last time
+ this._collectWindowData(aWindow);
+
+ if (isFullyLoaded) {
+ winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
+ winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
+ tabbrowser.selectedTab);
+ this._updateCookies([winData]);
+ }
+
+ // save the window if it has multiple tabs or a single saveable tab
+ if (winData.tabs.length > 1 ||
+ (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0]))) {
+ this._closedWindows.unshift(winData);
+ this._capClosedWindows();
+ }
+
+ // clear this window from the list
+ delete this._windows[aWindow.__SSi];
+
+ // save the state without this window to disk
+ this.saveStateDelayed();
+ }
+
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
+ }
+
+ // Cache the window state until it is completely gone.
+ DyingWindowCache.set(aWindow, winData);
+
+ delete aWindow.__SSi;
+ },
+
+ /**
+ * set up listeners for a new tab
+ * @param aWindow
+ * Window reference
+ * @param aTab
+ * Tab reference
+ * @param aNoNotification
+ * bool Do not save state if we're updating an existing tab
+ */
+ onTabAdd: function sss_onTabAdd(aWindow, aTab, aNoNotification) {
+ let browser = aTab.linkedBrowser;
+ browser.addEventListener("load", this, true);
+ browser.addEventListener("pageshow", this, true);
+ browser.addEventListener("input", this, true);
+ browser.addEventListener("DOMAutoComplete", this, true);
+
+ if (!aNoNotification) {
+ this.saveStateDelayed(aWindow);
+ }
+
+ this._updateCrashReportURL(aWindow);
+ },
+
+ /**
+ * remove listeners for a tab
+ * @param aWindow
+ * Window reference
+ * @param aTab
+ * Tab reference
+ * @param aNoNotification
+ * bool Do not save state if we're updating an existing tab
+ */
+ onTabRemove: function sss_onTabRemove(aWindow, aTab, aNoNotification) {
+ let browser = aTab.linkedBrowser;
+ browser.removeEventListener("load", this, true);
+ browser.removeEventListener("pageshow", this, true);
+ browser.removeEventListener("change", this, true);
+ browser.removeEventListener("input", this, true);
+ browser.removeEventListener("DOMAutoComplete", this, true);
+
+ delete browser.__SS_data;
+
+ // If this tab was in the middle of restoring or still needs to be restored,
+ // we need to reset that state. If the tab was restoring, we will attempt to
+ // restore the next tab.
+ let previousState = browser.__SS_restoreState;
+ if (previousState) {
+ this._resetTabRestoringState(aTab);
+ if (previousState == TAB_STATE_RESTORING)
+ this.restoreNextTab();
+ }
+
+ if (!aNoNotification) {
+ this.saveStateDelayed(aWindow);
+ }
+ },
+
+ /**
+ * When a tab closes, collect its properties
+ * @param aWindow
+ * Window reference
+ * @param aTab
+ * Tab reference
+ */
+ onTabClose: function sss_onTabClose(aWindow, aTab) {
+ // notify the tabbrowser that the tab state will be retrieved for the last time
+ // (so that extension authors can easily set data on soon-to-be-closed tabs)
+ var event = aWindow.document.createEvent("Events");
+ event.initEvent("SSTabClosing", true, false);
+ aTab.dispatchEvent(event);
+
+ // don't update our internal state if we don't have to
+ if (this._max_tabs_undo == 0) {
+ return;
+ }
+
+ // make sure that the tab related data is up-to-date
+ var tabState = this._collectTabData(aTab);
+ this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
+
+ // store closed-tab data for undo
+ if (this._shouldSaveTabState(tabState)) {
+ aTab.tabData = { state: tabState };
+ var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
+ closedTabs.unshift(aTab.tabData);
+ if (closedTabs.length > this._max_tabs_undo)
+ closedTabs.length = this._max_tabs_undo;
+ };
+ },
+
+ /**
+ * When a tab loads, save state.
+ * @param aWindow
+ * Window reference
+ * @param aBrowser
+ * Browser reference
+ * @param aEvent
+ * Event obj
+ */
+ onTabLoad: function sss_onTabLoad(aWindow, aBrowser, aEvent) {
+ // react on "load" and solitary "pageshow" events (the first "pageshow"
+ // following "load" is too late for deleting the data caches)
+ // It's possible to get a load event after calling stop on a browser (when
+ // overwriting tabs). We want to return early if the tab hasn't been restored yet.
+ if ((aEvent.type != "load" && !aEvent.persisted) ||
+ (aBrowser.__SS_restoreState &&
+ aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)) {
+ return;
+ }
+
+ delete aBrowser.__SS_data;
+ this.saveStateDelayed(aWindow);
+
+ // attempt to update the current URL we send in a crash report
+ this._updateCrashReportURL(aWindow);
+ },
+
+ /**
+ * Called when a browser sends the "input" notification
+ * @param aWindow
+ * Window reference
+ * @param aBrowser
+ * Browser reference
+ */
+ onTabInput: function sss_onTabInput(aWindow, aBrowser) {
+ this.saveStateDelayed(aWindow, 3000);
+ },
+
+ /**
+ * When a tab is selected, save session data
+ * @param aWindow
+ * Window reference
+ */
+ onTabSelect: function sss_onTabSelect(aWindow) {
+ if (this._loadState == STATE_RUNNING) {
+ this._windows[aWindow.__SSi].selected = aWindow.getBrowser().tabContainer.selectedIndex;
+
+ let tab = aWindow.getBrowser().selectedTab;
+ // If __SS_restoreState is still on the browser and it is
+ // TAB_STATE_NEEDS_RESTORE, then then we haven't restored
+ // this tab yet. Explicitly call restoreTab to kick off the restore.
+ if (tab.linkedBrowser.__SS_restoreState &&
+ tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ this.restoreTab(tab);
+
+ // attempt to update the current URL we send in a crash report
+ this._updateCrashReportURL(aWindow);
+ }
+ },
+
+ onTabShow: function sss_onTabShow(aTab) {
+ // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket
+ if (aTab.linkedBrowser.__SS_restoreState &&
+ aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+ this._tabsToRestore.hidden.splice(this._tabsToRestore.hidden.indexOf(aTab), 1);
+ // Just put it at the end of the list of visible tabs;
+ this._tabsToRestore.visible.push(aTab);
+ }
+ },
+
+ onTabHide: function sss_onTabHide(aTab) {
+ // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket
+ if (aTab.linkedBrowser.__SS_restoreState &&
+ aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+ this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(aTab), 1);
+ // Just put it at the end of the list of hidden tabs;
+ this._tabsToRestore.hidden.push(aTab);
+ }
+ },
+
+/* ........ nsISessionStore API .............. */
+
+ getBrowserState: function sss_getBrowserState() {
+ return this._toJSONString(this._getCurrentState());
+ },
+
+ setBrowserState: function sss_setBrowserState(aState) {
+ this._handleClosedWindows();
+
+ try {
+ var state = JSON.parse(aState);
+ }
+ catch (ex) { /* invalid state object - don't restore anything */ }
+ if (!state || !state.windows)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ this._browserSetState = true;
+
+ // Make sure _tabsToRestore is emptied out
+ this._resetRestoringState();
+
+ var window = this._getMostRecentBrowserWindow();
+ if (!window) {
+ this._restoreCount = 1;
+ this._openWindowWithState(state);
+ return;
+ }
+
+ // close all other browser windows
+ this._forEachBrowserWindow(function(aWindow) {
+ if (aWindow != window) {
+ aWindow.close();
+ this.onClose(aWindow);
+ }
+ });
+
+ // make sure closed window data isn't kept
+ this._closedWindows = [];
+
+ // determine how many windows are meant to be restored
+ this._restoreCount = state.windows ? state.windows.length : 0;
+
+ // restore to the given state
+ this.restoreWindow(window, state, true);
+ },
+
+ getWindowState: function sss_getWindowState(aWindow) {
+ if ("__SSi" in aWindow) {
+ return this._toJSONString(this._getWindowState(aWindow));
+ }
+
+ if (DyingWindowCache.has(aWindow)) {
+ let data = DyingWindowCache.get(aWindow);
+ return this._toJSONString({ windows: [data] });
+ }
+
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ setWindowState: function sss_setWindowState(aWindow, aState, aOverwrite) {
+ if (!aWindow.__SSi)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ this.restoreWindow(aWindow, aState, aOverwrite);
+ },
+
+ getTabState: function sss_getTabState(aTab) {
+ if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ var tabState = this._collectTabData(aTab);
+
+ var window = aTab.ownerDocument.defaultView;
+ this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState);
+
+ return this._toJSONString(tabState);
+ },
+
+ setTabState: function sss_setTabState(aTab, aState) {
+ var tabState = JSON.parse(aState);
+ if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ var window = aTab.ownerDocument.defaultView;
+ this._sendWindowStateEvent(window, "Busy");
+ this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0);
+ },
+
+ duplicateTab: function sss_duplicateTab(aWindow, aTab, aDelta, aRelated) {
+ if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
+ aWindow && !aWindow.getBrowser)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ var tabState = this._collectTabData(aTab, true);
+ var sourceWindow = aTab.ownerDocument.defaultView;
+ this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true);
+ tabState.index += aDelta;
+ tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
+
+ if (aWindow) {
+ this._sendWindowStateEvent(aWindow, "Busy");
+ var newTab = aWindow.getBrowser()
+ .addTab(null, { relatedToCurrent: aRelated });
+ this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0);
+ return newTab;
+ }
+
+ var state = { windows: [{ tabs: [tabState] }] };
+ this.windowToFocus = this._openWindowWithState(state);
+ return null;
+ },
+
+ _getClosedTabs: function sss_getClosedTabs(aWindow) {
+ if (!aWindow.__SSi)
+ return this._toJSONString(aWindow.__SS_dyingCache._closedTabs);
+
+ var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
+ closedTabs = closedTabs.concat(aWindow.getBrowser().savedBrowsers);
+ closedTabs = closedTabs.filter(function(aTabData, aIndex, aArray) {
+ return aArray.indexOf(aTabData) == aIndex;
+ });
+ return closedTabs;
+ },
+
+ getClosedTabCount: function sss_getClosedTabCount(aWindow) {
+ if ("__SSi" in aWindow) {
+ return this._windows[aWindow.__SSi]._closedTabs.length;
+ }
+
+ if (DyingWindowCache.has(aWindow)) {
+ return DyingWindowCache.get(aWindow)._closedTabs.length;
+ }
+
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ getClosedTabData: function sss_getClosedTabData(aWindow) {
+ if ("__SSi" in aWindow) {
+ return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs);
+ }
+
+ if (DyingWindowCache.has(aWindow)) {
+ let data = DyingWindowCache.get(aWindow);
+ return this._toJSONString(data._closedTabs);
+ }
+
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ undoCloseTab: function sss_undoCloseTab(aWindow, aIndex) {
+ if (!aWindow.__SSi)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ var closedTabs = this._getClosedTabs(aWindow);
+ if (!(aIndex in closedTabs))
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ // fetch the data of closed tab, while removing it from the array
+ let closedTab = closedTabs[aIndex];
+ if (aIndex in this._windows[aWindow.__SSi]._closedTabs)
+ this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1);
+ var tabbrowser = aWindow.getBrowser();
+ var index = tabbrowser.savedBrowsers.indexOf(closedTab);
+ this._sendWindowStateEvent(aWindow, "Busy");
+ if (index != -1)
+ // SeaMonkey has its own undoclosetab functionality
+ return tabbrowser.restoreTab(index);
+
+ // create a new tab
+ var tab = tabbrowser.addTab();
+
+ // restore the tab's position
+ tabbrowser.moveTabTo(tab, closedTab.pos);
+
+ // restore tab content
+ this.restoreHistoryPrecursor(aWindow, [tab], [closedTab.state], 1, 0, 0);
+
+ // focus the tab's content area (bug 342432)
+ tab.linkedBrowser.focus();
+
+ return tab;
+ },
+
+ forgetClosedTab: function sss_forgetClosedTab(aWindow, aIndex) {
+ if (!aWindow.__SSi)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ var closedTabs = this._getClosedTabs(aWindow);
+ if (!(aIndex in closedTabs))
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ // remove closed tab from the array
+ var closedTab = closedTabs[aIndex];
+ if (aIndex in this._windows[aWindow.__SSi]._closedTabs)
+ this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1);
+ var tabbrowser = aWindow.getBrowser();
+ var index = tabbrowser.savedBrowsers.indexOf(closedTab);
+ if (index != -1)
+ tabbrowser.forgetSavedBrowser(aIndex);
+ },
+
+ getClosedWindowCount: function sss_getClosedWindowCount() {
+ return this._closedWindows.length;
+ },
+
+ getClosedWindowData: function sss_getClosedWindowData() {
+ return this._toJSONString(this._closedWindows);
+ },
+
+ undoCloseWindow: function sss_undoCloseWindow(aIndex) {
+ if (!(aIndex in this._closedWindows))
+ return null;
+
+ // reopen the window
+ let state = { windows: this._closedWindows.splice(aIndex, 1) };
+ let window = this._openWindowWithState(state);
+ this.windowToFocus = window;
+ return window;
+ },
+
+ forgetClosedWindow: function sss_forgetClosedWindow(aIndex) {
+ // default to the most-recently closed window
+ aIndex = aIndex || 0;
+ if (!(aIndex in this._closedWindows))
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ // remove closed window from the array
+ this._closedWindows.splice(aIndex, 1);
+ },
+
+ getWindowValue: function sss_getWindowValue(aWindow, aKey) {
+ if ("__SSi" in aWindow) {
+ var data = this._windows[aWindow.__SSi].extData || {};
+ return data[aKey] || "";
+ }
+ if (DyingWindowCache.has(aWindow)) {
+ let data = DyingWindowCache.get(aWindow).extData || {};
+ return data[aKey] || "";
+ }
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ setWindowValue: function sss_setWindowValue(aWindow, aKey, aStringValue) {
+ if (aWindow.__SSi) {
+ if (!this._windows[aWindow.__SSi].extData) {
+ this._windows[aWindow.__SSi].extData = {};
+ }
+ this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
+ this.saveStateDelayed(aWindow);
+ }
+ else {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+ },
+
+ deleteWindowValue: function sss_deleteWindowValue(aWindow, aKey) {
+ if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
+ this._windows[aWindow.__SSi].extData[aKey])
+ delete this._windows[aWindow.__SSi].extData[aKey];
+ },
+
+ getTabValue: function sss_getTabValue(aTab, aKey) {
+ let data = {};
+ if (aTab.__SS_extdata) {
+ data = aTab.__SS_extdata;
+ }
+ else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
+ // If the tab hasn't been fully restored, get the data from the to-be-restored data
+ data = aTab.linkedBrowser.__SS_data.extData;
+ }
+ return data[aKey] || "";
+ },
+
+ setTabValue: function sss_setTabValue(aTab, aKey, aStringValue) {
+ // If the tab hasn't been restored, then set the data there, otherwise we
+ // could lose newly added data.
+ let saveTo;
+ if (aTab.__SS_extdata) {
+ saveTo = aTab.__SS_extdata;
+ }
+ else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
+ saveTo = aTab.linkedBrowser.__SS_data.extData;
+ }
+ else {
+ aTab.__SS_extdata = {};
+ saveTo = aTab.__SS_extdata;
+ }
+ saveTo[aKey] = aStringValue;
+ this.saveStateDelayed(aTab.ownerDocument.defaultView);
+ },
+
+ deleteTabValue: function sss_deleteTabValue(aTab, aKey) {
+ // We want to make sure that if data is accessed early, we attempt to delete
+ // that data from __SS_data as well. Otherwise we'll throw in cases where
+ // data can be set or read.
+ let deleteFrom = null;
+ if (aTab.__SS_extdata) {
+ deleteFrom = aTab.__SS_extdata;
+ }
+ else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
+ deleteFrom = aTab.linkedBrowser.__SS_data.extData;
+ }
+
+ if (deleteFrom && deleteFrom[aKey])
+ delete deleteFrom[aKey];
+ },
+
+ persistTabAttribute: function sss_persistTabAttribute(aName) {
+ if (aName in this.xulAttributes)
+ return; // this attribute is already being tracked
+
+ this.xulAttributes[aName] = true;
+ this.saveStateDelayed();
+ },
+
+ doRestoreLastWindow: function sss_doRestoreLastWindow() {
+ let state = null;
+ this._closedWindows.forEach(function(aWinState) {
+ if (!state && !aWinState.isPopup) {
+ state = aWinState;
+ }
+ });
+ return (this._restoreLastWindow && state &&
+ this._doResumeSession());
+ },
+
+ /**
+ * Restores the session state stored in _lastSessionState. This will attempt
+ * to merge data into the current session. If a window was opened at startup
+ * with pinned tab(s), then the remaining data from the previous session for
+ * that window will be opened into that winddow. Otherwise new windows will
+ * be opened.
+ */
+ restoreLastSession: function sss_restoreLastSession() {
+ // Use the public getter since it also checks PB mode
+ if (!this.canRestoreLastSession)
+ throw (Components.returnCode = Cr.NS_ERROR_FAILURE);
+
+ // First collect each window with its id...
+ let windows = {};
+ this._forEachBrowserWindow(function(aWindow) {
+ if (aWindow.__SS_lastSessionWindowID)
+ windows[aWindow.__SS_lastSessionWindowID] = aWindow;
+ });
+
+ let lastSessionState = this._lastSessionState;
+
+ // This shouldn't ever be the case...
+ if (!lastSessionState.windows.length)
+ throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED);
+
+ // We're technically doing a restore, so set things up so we send the
+ // notification when we're done. We want to send "sessionstore-browser-state-restored".
+ this._restoreCount = lastSessionState.windows.length;
+ this._browserSetState = true;
+
+ // We want to re-use the last opened window instead of opening a new one in
+ // the case where it's "empty" and not associated with a window in the session.
+ // We will do more processing via _prepWindowToRestoreInto if we need to use
+ // the lastWindow.
+ let lastWindow = this._getMostRecentBrowserWindow();
+ let canUseLastWindow = lastWindow &&
+ !lastWindow.__SS_lastSessionWindowID;
+
+ // Restore into windows or open new ones as needed.
+ for (let i = 0; i < lastSessionState.windows.length; i++) {
+ let winState = lastSessionState.windows[i];
+ let lastSessionWindowID = winState.__lastSessionWindowID;
+ // delete lastSessionWindowID so we don't add that to the window again
+ delete winState.__lastSessionWindowID;
+
+ // See if we can use an open window. First try one that is associated with
+ // the state we're trying to restore and then fallback to the last selected
+ // window.
+ let windowToUse = windows[lastSessionWindowID];
+ if (!windowToUse && canUseLastWindow) {
+ windowToUse = lastWindow;
+ canUseLastWindow = false;
+ }
+
+ let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse);
+
+ // If there's a window already open that we can restore into, use that
+ if (canUseWindow) {
+ // Since we're not overwriting existing tabs, we want to merge _closedTabs,
+ // putting existing ones first. Then make sure we're respecting the max pref.
+ if (winState._closedTabs && winState._closedTabs.length) {
+ let curWinState = this._windows[windowToUse.__SSi];
+ curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
+ curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
+ }
+
+ // Restore into that window - pretend it's a followup since we'll already
+ // have a focused window.
+ //XXXzpao This is going to merge extData together (taking what was in
+ // winState over what is in the window already. The hack we have
+ // in _preWindowToRestoreInto will prevent most (all?) Panorama
+ // weirdness but we will still merge other extData.
+ // Bug 588217 should make this go away by merging the group data.
+ this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true);
+ }
+ else {
+ this._openWindowWithState({ windows: [winState] });
+ }
+ }
+
+ // Merge closed windows from this session with ones from last session
+ if (lastSessionState._closedWindows) {
+ this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
+ this._capClosedWindows();
+ }
+
+ // Set data that persists between sessions
+ this._recentCrashes = lastSessionState.session &&
+ lastSessionState.session.recentCrashes || 0;
+ this._sessionStartTime = lastSessionState.session &&
+ lastSessionState.session.startTime ||
+ this._sessionStartTime;
+
+ this._lastSessionState = null;
+ },
+
+ /**
+ * See if aWindow is usable for use when restoring a previous session via
+ * restoreLastSession. If usable, prepare it for use.
+ *
+ * @param aWindow
+ * the window to inspect & prepare
+ * @returns [canUseWindow, canOverwriteTabs]
+ * canUseWindow: can the window be used to restore into
+ * canOverwriteTabs: all of the current tabs are home pages and we
+ * can overwrite them
+ */
+ _prepWindowToRestoreInto: function sss__prepWindowToRestoreInto(aWindow) {
+ if (!aWindow)
+ return [false, false];
+
+ // We might be able to overwrite the existing tabs instead of just adding
+ // the previous session's tabs to the end. This will be set if possible.
+ let canOverwriteTabs = false;
+
+ // Step 1 of processing:
+ // Inspect extData for Panorama identifiers. If found, then we want to
+ // inspect further. If there is a single group, then we can use this
+ // window. If there are multiple groups then we won't use this window.
+ let data = this.getWindowValue(aWindow, "tabview-group");
+ if (data) {
+ data = JSON.parse(data);
+
+ // Multiple keys means multiple groups, which means we don't want to use this window.
+ if (Object.keys(data).length > 1) {
+ return [false, false];
+ }
+ else {
+ // If there is only one group, then we want to ensure that its group id
+ // is 0. This is how Panorama forces group merging when new tabs are opened.
+ //XXXzpao This is a hack and the proper fix really belongs in Panorama.
+ let groupKey = Object.keys(data)[0];
+ if (groupKey !== "0") {
+ data["0"] = data[groupKey];
+ delete data[groupKey];
+ this.setWindowValue(aWindow, "tabview-groups", JSON.stringify(data));
+ }
+ }
+ }
+
+ // Step 2 of processing:
+ // If we're still here, then the window is usable. Look at the open tabs in
+ // comparison to home pages. If all the tabs are home pages then we'll end
+ // up overwriting all of them. Otherwise we'll just close the tabs that
+ // match home pages.
+ let homePages = aWindow.getHomePage();
+ let removableTabs = [];
+ let tabbrowser = aWindow.getBrowser();
+ let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs;
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ let tab = tabbrowser.tabs[i];
+ if (homePages.includes(tab.linkedBrowser.currentURI.spec)) {
+ removableTabs.push(tab);
+ }
+ }
+
+ if (tabbrowser.tabs.length == removableTabs.length) {
+ canOverwriteTabs = true;
+ }
+ else {
+ // If we're not overwriting all of the tabs, then close the home tabs.
+ for (let i = removableTabs.length - 1; i >= 0; i--) {
+ tabbrowser.removeTab(removableTabs.pop(), { animate: false });
+ }
+ }
+
+ return [true, canOverwriteTabs];
+ },
+
+/* ........ Saving Functionality .............. */
+
+ /**
+ * Store all session data for a window
+ * @param aWindow
+ * Window reference
+ */
+ _saveWindowHistory: function sss_saveWindowHistory(aWindow) {
+ var tabbrowser = aWindow.getBrowser();
+ var tabs = tabbrowser.tabs;
+ var tabsData = this._windows[aWindow.__SSi].tabs = [];
+
+ for (var i = 0; i < tabs.length; i++)
+ tabsData.push(this._collectTabData(tabs[i]));
+
+ this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1;
+ },
+
+ /**
+ * Collect data related to a single tab
+ * @param aTab
+ * tabbrowser tab
+ * @param aFullData
+ * always return privacy sensitive data (use with care)
+ * @returns object
+ */
+ _collectTabData: function sss_collectTabData(aTab, aFullData) {
+ var tabData = { entries: [] };
+ var browser = aTab.linkedBrowser;
+
+ if (!browser || !browser.currentURI)
+ // can happen when calling this function right after .addTab()
+ return tabData;
+ else if (browser.__SS_data &&
+ browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+ // use the data to be restored when the tab hasn't been completely loaded
+ tabData = browser.__SS_data;
+ if (aTab.pinned)
+ tabData.pinned = true;
+ else
+ delete tabData.pinned;
+ tabData.hidden = aTab.hidden;
+
+ // If __SS_extdata is set then we'll use that since it might be newer.
+ if (aTab.__SS_extdata)
+ tabData.extData = aTab.__SS_extdata;
+ // If it exists but is empty then a key was likely deleted. In that case just
+ // delete extData.
+ if (tabData.extData && !Object.keys(tabData.extData).length)
+ delete tabData.extData;
+ return tabData;
+ }
+
+ var history = null;
+ try {
+ history = browser.sessionHistory;
+ }
+ catch (ex) { } // this could happen if we catch a tab during (de)initialization
+
+ // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
+ // data even when we shouldn't (e.g. Back, different anchor)
+ if (history && browser.__SS_data &&
+ browser.__SS_data.entries[history.index] &&
+ browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
+ history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
+ tabData = browser.__SS_data;
+ tabData.index = history.index + 1;
+ }
+ else if (history && history.count > 0) {
+ try {
+ for (var j = 0; j < history.count; j++) {
+ let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j),
+ aFullData, aTab.pinned);
+ tabData.entries.push(entry);
+ }
+ // If we make it through the for loop, then we're ok and we should clear
+ // any indicator of brokenness.
+ delete aTab.__SS_broken_history;
+ }
+ catch (ex) {
+ // In some cases, getEntryAtIndex will throw. This seems to be due to
+ // history.count being higher than it should be. By doing this in a
+ // try-catch, we'll update history to where it breaks, assert for
+ // non-release builds, and still save sessionstore.js. We'll track if
+ // we've shown the assert for this tab so we only show it once.
+ // cf. bug 669196.
+ if (!aTab.__SS_broken_history) {
+ // First Focus the window & tab we're having trouble with.
+ aTab.ownerDocument.defaultView.focus();
+ aTab.ownerDocument.defaultView.getBrowser().selectedTab = aTab;
+ debug("SessionStore failed gathering complete history " +
+ "for the focused window/tab. See bug 669196.");
+ aTab.__SS_broken_history = true;
+ }
+ }
+ tabData.index = history.index + 1;
+
+ // make sure not to cache privacy sensitive data which shouldn't get out
+ if (!aFullData)
+ browser.__SS_data = tabData;
+ }
+ else if (browser.currentURI.spec != "about:blank" ||
+ browser.contentDocument.body.hasChildNodes()) {
+ tabData.entries[0] = { url: browser.currentURI.spec,
+ triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL };
+ tabData.index = 1;
+ }
+
+ // If there is a userTypedValue set, then either the user has typed something
+ // in the URL bar, or a new tab was opened with a URI to load. userTypedClear
+ // is used to indicate whether the tab was in some sort of loading state with
+ // userTypedValue.
+ if (browser.userTypedValue) {
+ tabData.userTypedValue = browser.userTypedValue;
+ tabData.userTypedClear = browser.userTypedClear;
+ } else {
+ delete tabData.userTypedValue;
+ delete tabData.userTypedClear;
+ }
+
+ var disallow = [];
+ for (var i = 0; i < CAPABILITIES.length; i++)
+ if (!browser.docShell["allow" + CAPABILITIES[i]])
+ disallow.push(CAPABILITIES[i]);
+ if (disallow.length > 0)
+ tabData.disallow = disallow.join(",");
+ else if (tabData.disallow)
+ delete tabData.disallow;
+
+ tabData.attributes = {};
+ for (let name in this.xulAttributes) {
+ if (aTab.hasAttribute(name))
+ tabData.attributes[name] = aTab.getAttribute(name);
+ }
+
+ if (aTab.__SS_extdata)
+ tabData.extData = aTab.__SS_extdata;
+ else if (tabData.extData)
+ delete tabData.extData;
+
+ if (history && browser.docShell instanceof Ci.nsIDocShell)
+ this._serializeSessionStorage(tabData, history, browser.docShell, aFullData,
+ false);
+
+ return tabData;
+ },
+
+ /**
+ * Get an object that is a serialized representation of a History entry
+ * Used for data storage
+ * @param aEntry
+ * nsISHEntry instance
+ * @param aFullData
+ * always return privacy sensitive data (use with care)
+ * @param aIsPinned
+ * the tab is pinned and should be treated differently for privacy
+ * @returns object
+ */
+ _serializeHistoryEntry:
+ function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) {
+ var entry = { url: aEntry.URI.spec,
+ triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL };
+
+ if (aEntry.title && aEntry.title != entry.url) {
+ entry.title = aEntry.title;
+ }
+ if (aEntry.isSubFrame) {
+ entry.subframe = true;
+ }
+ if (!(aEntry instanceof Ci.nsISHEntry)) {
+ return entry;
+ }
+
+ var cacheKey = aEntry.cacheKey;
+ if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
+ cacheKey.data != 0) {
+ // XXXbz would be better to have cache keys implement
+ // nsISerializable or something.
+ entry.cacheKey = cacheKey.data;
+ }
+ entry.ID = aEntry.ID;
+ entry.docshellUUID = aEntry.docshellID.toString();
+
+ if (aEntry.referrerURI)
+ entry.referrer = aEntry.referrerURI.spec;
+
+ if (aEntry.contentType)
+ entry.contentType = aEntry.contentType;
+
+ var x = {}, y = {};
+ aEntry.getScrollPosition(x, y);
+ if (x.value != 0 || y.value != 0)
+ entry.scroll = x.value + "," + y.value;
+
+ try {
+ var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
+ if (aEntry.postData && (aFullData || prefPostdata &&
+ this._checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
+ aEntry.postData.QueryInterface(Ci.nsISeekableStream)
+ .seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ var stream = Cc["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Ci.nsIBinaryInputStream);
+ stream.setInputStream(aEntry.postData);
+ var postBytes = stream.readByteArray(stream.available());
+ var postdata = String.fromCharCode.apply(null, postBytes);
+ if (aFullData || prefPostdata == -1 ||
+ postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
+ prefPostdata) {
+ // We can stop doing base64 encoding once our serialization into JSON
+ // is guaranteed to handle all chars in strings, including embedded
+ // nulls.
+ entry.postdata_b64 = btoa(postdata);
+ }
+ }
+ }
+ catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right
+
+ // Collect triggeringPrincipal data for the current history entry.
+ // Please note that before Bug 1297338 there was no concept of a
+ // principalToInherit. To remain backward/forward compatible we
+ // serialize the principalToInherit as triggeringPrincipal_b64.
+ // Once principalToInherit is well established (within Gecko 55)
+ // we can update this code, remove triggeringPrincipal_b64 and
+ // just keep triggeringPrincipal_base64 as well as
+ // principalToInherit_base64.
+ if (aEntry.principalToInherit) {
+ try {
+ let principalToInherit = Utils.serializePrincipal(aEntry.principalToInherit);
+ if (principalToInherit) {
+ entry.triggeringPrincipal_b64 = principalToInherit;
+ entry.principalToInherit_base64 = principalToInherit;
+ }
+ } catch (e) {
+ debug(e);
+ }
+ }
+
+ if (aEntry.triggeringPrincipal) {
+ try {
+ let triggeringPrincipal = Utils.serializePrincipal(aEntry.triggeringPrincipal);
+ if (triggeringPrincipal) {
+ entry.triggeringPrincipal_base64 = triggeringPrincipal;
+ }
+ } catch (e) {
+ debug(e);
+ }
+ }
+
+ entry.docIdentifier = aEntry.BFCacheEntry.ID;
+
+ if (aEntry.stateData) {
+ entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
+ entry.structuredCloneVersion = aEntry.stateData.formatVersion;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHContainer)) {
+ return entry;
+ }
+
+ if (aEntry.childCount > 0) {
+ entry.children = [];
+ for (var i = 0; i < aEntry.childCount; i++) {
+ var child = aEntry.GetChildAt(i);
+ if (child) {
+ entry.children.push(this._serializeHistoryEntry(child, aFullData, aIsPinned));
+ }
+ else { // to maintain the correct frame order, insert a dummy entry
+ entry.children.push({ url: "about:blank",
+ triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL});
+ }
+ // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
+ if (/^wyciwyg:\/\//.test(entry.children[i].url)) {
+ delete entry.children;
+ break;
+ }
+ }
+ }
+
+ return entry;
+ },
+
+ /**
+ * Updates all sessionStorage "super cookies"
+ * @param aTabData
+ * The data object for a specific tab
+ * @param aHistory
+ * That tab's session history
+ * @param aDocShell
+ * That tab's docshell (containing the sessionStorage)
+ * @param aFullData
+ * always return privacy sensitive data (use with care)
+ * @param aIsPinned
+ * the tab is pinned and should be treated differently for privacy
+ */
+ _serializeSessionStorage:
+ function sss_serializeSessionStorage(aTabData, aHistory, aDocShell, aFullData, aIsPinned) {
+ let storageData = {};
+ let hasContent = false;
+
+ for (let i = 0; i < aHistory.count; i++) {
+ let principal;
+ try {
+ let uri = aHistory.getEntryAtIndex(i).URI;
+ principal = SecMan.getDocShellCodebasePrincipal(uri, aDocShell);
+ }
+ catch (ex) {
+ // Chances are that this is getEntryAtIndex throwing, as seen in bug 669196.
+ // We've already asserted in _collectTabData, so we won't show that again.
+ continue;
+ }
+
+ // sessionStorage is saved per principal (cf. nsGlobalWindow::GetSessionStorage)
+ let origin;
+ try {
+ origin = principal.origin;
+ }
+ catch (ex) {
+ origin = principal.URI.spec;
+ }
+
+ if (storageData[origin])
+ continue;
+
+ let isHTTPS = principal.URI && principal.URI.schemeIs("https");
+ if (!(aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned)))
+ continue;
+
+ let storage, storageItemCount = 0;
+
+ let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ try {
+ let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+ storage = storageManager.getStorage(window, principal);
+
+ // See Bug 1232955 - storage.length can throw, catch that failure here inside the try.
+ if (storage)
+ storageItemCount = storage.length;
+ }
+ catch (ex) { /* sessionStorage might throw if it's turned off, see bug 458954 */ }
+
+ if (storageItemCount == 0)
+ continue;
+
+ let data = storageData[origin] = {};
+
+ for (let j = 0; j < storageItemCount; j++) {
+ try {
+ let key = storage.key(j);
+ data[key] = storage.getItem(key);
+ }
+ catch (ex) { /* XXXzeniko this currently throws for secured items (cf. bug 442048) */ }
+ }
+ hasContent = true;
+ }
+
+ if (hasContent)
+ aTabData.storage = storageData;
+ },
+
+ /**
+ * go through all tabs and store the current scroll positions
+ * and innerHTML content of WYSIWYG editors
+ * @param aWindow
+ * Window reference
+ */
+ _updateTextAndScrollData: function sss_updateTextAndScrollData(aWindow) {
+ var browsers = aWindow.getBrowser().browsers;
+ for (var i = 0; i < browsers.length; i++) {
+ try {
+ var tabData = this._windows[aWindow.__SSi].tabs[i];
+ if (browsers[i].__SS_data &&
+ browsers[i].__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ continue; // ignore incompletely initialized tabs
+ this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
+ }
+ catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
+ }
+ },
+
+ /**
+ * go through all frames and store the current scroll positions
+ * and innerHTML content of WYSIWYG editors
+ * @param aWindow
+ * Window reference
+ * @param aBrowser
+ * single browser reference
+ * @param aTabData
+ * tabData object to add the information to
+ * @param aFullData
+ * always return privacy sensitive data (use with care)
+ */
+ _updateTextAndScrollDataForTab:
+ function sss_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) {
+ var tabIndex = (aTabData.index || aTabData.entries.length) - 1;
+ // entry data needn't exist for tabs just initialized with an incomplete session state
+ if (!aTabData.entries[tabIndex])
+ return;
+
+ let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
+ this._getSelectedPageStyle(aBrowser.contentWindow);
+ if (selectedPageStyle)
+ aTabData.pageStyle = selectedPageStyle;
+ else if (aTabData.pageStyle)
+ delete aTabData.pageStyle;
+
+ this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
+ aTabData.entries[tabIndex],
+ aFullData,
+ !!aTabData.pinned);
+ if (aBrowser.currentURI.spec == "about:config")
+ aTabData.entries[tabIndex].formdata = {
+ "#textbox": aBrowser.contentDocument.getElementById("textbox").value
+ };
+ },
+
+ /**
+ * go through all subframes and store all form data, the current
+ * scroll positions and innerHTML content of WYSIWYG editors
+ * @param aWindow
+ * Window reference
+ * @param aContent
+ * frame reference
+ * @param aData
+ * part of a tabData object to add the information to
+ * @param aFullData
+ * always return privacy sensitive data (use with care)
+ * @param aIsPinned
+ * the tab is pinned and should be treated differently for privacy
+ */
+ _updateTextAndScrollDataForFrame:
+ function sss_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
+ aFullData, aIsPinned) {
+ for (var i = 0; i < aContent.frames.length; i++) {
+ if (aData.children && aData.children[i])
+ this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
+ aData.children[i],
+ aFullData, aIsPinned);
+ }
+ var isHTTPS = this._getURIFromString((aContent.parent || aContent).
+ document.location.href).schemeIs("https");
+ if (aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned) ||
+ aContent.top.document.location.href == "about:sessionrestore") {
+ let formData = this._collectFormDataForFrame(aContent.document);
+ if (formData)
+ aData.formdata = formData;
+ else if (aData.formdata)
+ delete aData.formdata;
+
+ // designMode is undefined e.g. for XUL documents (as about:config)
+ if ((aContent.document.designMode || "") == "on") {
+ if (aData.innerHTML === undefined && !aFullData) {
+ // we get no "input" events from iframes - listen for keypress here
+ aContent.addEventListener("keypress", this.saveStateDelayed.bind(this, aWindow, 3000), true);
+ }
+ aData.innerHTML = aContent.document.body.innerHTML;
+ }
+ }
+
+ // get scroll position from nsIDOMWindowUtils, since it allows avoiding a
+ // flush of layout
+ let domWindowUtils = aContent.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ domWindowUtils.getScrollXY(false, scrollX, scrollY);
+ aData.scroll = scrollX.value + "," + scrollY.value;
+ },
+
+ /**
+ * determine the title of the currently enabled style sheet (if any)
+ * and recurse through the frameset if necessary
+ * @param aContent is a frame reference
+ * @returns the title style sheet determined to be enabled (empty string if none)
+ */
+ _getSelectedPageStyle: function sss_getSelectedPageStyle(aContent) {
+ const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i;
+ for (let i = 0; i < aContent.document.styleSheets.length; i++) {
+ let ss = aContent.document.styleSheets[i];
+ let media = ss.media.mediaText;
+ if (!ss.disabled && ss.title && (!media || forScreen.test(media)))
+ return ss.title
+ }
+ for (let i = 0; i < aContent.frames.length; i++) {
+ let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]);
+ if (selectedPageStyle)
+ return selectedPageStyle;
+ }
+ return "";
+ },
+
+ /**
+ * collect the state of all form elements
+ * @param aDocument
+ * document reference
+ */
+ _collectFormDataForFrame: function sss_collectFormDataForFrame(aDocument) {
+ let formNodes = aDocument.evaluate(XPathGenerator.restorableFormNodes, aDocument,
+ XPathGenerator.resolveNS,
+ aDocument.defaultView.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
+ let node = formNodes.iterateNext();
+ if (!node)
+ return null;
+
+ const MAX_GENERATED_XPATHS = 100;
+ let generatedCount = 0;
+
+ let data = {};
+ do {
+ let nId = node.id;
+ let hasDefaultValue = true;
+ let value;
+
+ // Only generate a limited number of XPath expressions for perf reasons (cf. bug 477564)
+ if (!nId && generatedCount > MAX_GENERATED_XPATHS)
+ continue;
+
+ if (ChromeUtils.getClassName(node) === "HTMLInputElement" ||
+ ChromeUtils.getClassName(node) === "HTMLTextAreaElement") {
+ switch (node.type) {
+ case "checkbox":
+ case "radio":
+ value = node.checked;
+ hasDefaultValue = value == node.defaultChecked;
+ break;
+ case "file":
+ value = { type: "file", fileList: node.mozGetFileNameArray() };
+ hasDefaultValue = !value.fileList.length;
+ break;
+ default: // text, textarea
+ value = node.value;
+ hasDefaultValue = value == node.defaultValue;
+ break;
+ }
+ }
+ else if (!node.multiple) {
+ // <select>s without the multiple attribute are hard to determine the
+ // default value, so assume we don't have the default.
+ hasDefaultValue = false;
+ value = node.selectedIndex;
+ }
+ else {
+ // <select>s with the multiple attribute are easier to determine the
+ // default value since each <option> has a defaultSelected
+ let options = Array.from(node.options, function(aOpt, aIx) {
+ let oSelected = aOpt.selected;
+ hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected);
+ return oSelected ? aIx : -1;
+ });
+ value = options.filter(aIx => aIx >= 0);
+ }
+ // In order to reduce XPath generation (which is slow), we only save data
+ // for form fields that have been changed. (cf. bug 537289)
+ if (!hasDefaultValue) {
+ if (nId) {
+ data["#" + nId] = value;
+ }
+ else {
+ generatedCount++;
+ data[XPathGenerator.generate(node)] = value;
+ }
+ }
+
+ } while ((node = formNodes.iterateNext()));
+
+ return data;
+ },
+
+ /**
+ * extract the base domain from a history entry and its children
+ * @param aEntry
+ * the history entry, serialized
+ * @param aHosts
+ * the hash that will be used to store hosts eg, { hostname: true }
+ * @param aCheckPrivacy
+ * should we check the privacy level for https
+ * @param aIsPinned
+ * is the entry we're evaluating for a pinned tab; used only if
+ * aCheckPrivacy
+ */
+ _extractHostsForCookiesFromEntry:
+ function sss__extractHostsForCookiesFromEntry(aEntry, aHosts, aCheckPrivacy, aIsPinned) {
+
+ if (aEntry.children) {
+ aEntry.children.forEach(function(entry) {
+ this._extractHostsForCookiesFromEntry(entry, aHosts, aCheckPrivacy, aIsPinned);
+ }, this);
+ }
+ },
+
+ /**
+ * extract the base domain from a host & scheme
+ * @param aHost
+ * the host of a uri (usually via nsIURI.host)
+ * @param aScheme
+ * the scheme of a uri (usually via nsIURI.scheme)
+ * @param aHosts
+ * the hash that will be used to store hosts eg, { hostname: true }
+ * @param aCheckPrivacy
+ * should we check the privacy level for https
+ * @param aIsPinned
+ * is the entry we're evaluating for a pinned tab; used only if
+ * aCheckPrivacy
+ */
+ _extractHostsForCookiesFromHostScheme:
+ function sss__extractHostsForCookiesFromHostScheme(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) {
+ // host and scheme may not be set (for about: urls for example), in which
+ // case testing scheme will be sufficient.
+ if (/https?/.test(aScheme) && !aHosts[aHost] &&
+ (!aCheckPrivacy ||
+ this._checkPrivacyLevel(aScheme == "https", aIsPinned))) {
+ // By setting this to true or false, we can determine when looking at
+ // the host in _updateCookies if we should check for privacy.
+ aHosts[aHost] = aIsPinned;
+ }
+ else if (aScheme == "file") {
+ aHosts[aHost] = true;
+ }
+ },
+
+ /**
+ * Serialize cookie data
+ * @param aWindows
+ * JS object containing window data references
+ * { id: winData, etc. }
+ */
+ _updateCookies: function sss_updateCookies(aWindows) {
+ var jscookies = {};
+ // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
+ var MAX_EXPIRY = Math.pow(2, 62);
+
+ for (let window of aWindows) {
+ window.cookies = [];
+
+ // Collect all hosts for the current window.
+ let hosts = {};
+ window.tabs.forEach(function(tab) {
+ tab.entries.forEach(function(entry) {
+ this._extractHostsForCookiesFromEntry(entry, hosts, true, tab.pinned);
+ }, this);
+ }, this);
+
+ for (var [host, isPinned] of Object.entries(hosts)) {
+ try {
+ var list = Services.cookies.getCookiesFromHost(host, {});
+ while (list.hasMoreElements()) {
+ var cookie = list.getNext().QueryInterface(Ci.nsICookie2);
+ // window._hosts will only have hosts with the right privacy rules,
+ // so there is no need to do anything special with this call to
+ // _checkPrivacyLevel.
+ if (cookie.isSession && this._checkPrivacyLevel(cookie.isSecure, isPinned)) {
+ // use the cookie's host, path, and name as keys into a hash,
+ // to make sure we serialize each cookie only once
+
+ // lazily build up a 3-dimensional hash, with
+ // host, path, and name as keys
+ if (!jscookies[cookie.host])
+ jscookies[cookie.host] = {};
+ if (!jscookies[cookie.host][cookie.path])
+ jscookies[cookie.host][cookie.path] = {};
+
+ if (!jscookies[cookie.host][cookie.path][cookie.name]) {
+ var jscookie = { "host": cookie.host, "value": cookie.value };
+ // only add attributes with non-default values (saving a few bits)
+ if (cookie.path)
+ jscookie.path = cookie.path;
+ if (cookie.name)
+ jscookie.name = cookie.name;
+ if (cookie.isSecure)
+ jscookie.secure = true;
+ if (cookie.isHttpOnly)
+ jscookie.httponly = true;
+ if (cookie.expiry < MAX_EXPIRY)
+ jscookie.expiry = cookie.expiry;
+ if (cookie.originAttributes)
+ jscookie.originAttributes = cookie.originAttributes;
+
+ jscookies[cookie.host][cookie.path][cookie.name] = jscookie;
+ }
+ window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]);
+ }
+ }
+ }
+ catch (ex) {
+ debug("getCookiesFromHost failed. Host: " + host);
+ }
+ }
+
+ // don't include empty cookie sections
+ if (!window.cookies.length)
+ delete window.cookies;
+ }
+ },
+
+ /**
+ * Store window dimensions, visibility, sidebar
+ * @param aWindow
+ * Window reference
+ */
+ _updateWindowFeatures: function sss_updateWindowFeatures(aWindow) {
+ var winData = this._windows[aWindow.__SSi];
+
+ for (var aAttr in WINDOW_ATTRIBUTES)
+ winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
+
+ var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) {
+ return aWindow[aItem] && !aWindow[aItem].visible;
+ });
+ if (hidden.length != 0)
+ winData.hidden = hidden.join(",");
+ else if (winData.hidden)
+ delete winData.hidden;
+
+ var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand");
+ if (sidebar)
+ winData.sidebar = sidebar;
+ else if (winData.sidebar)
+ delete winData.sidebar;
+ },
+
+ /**
+ * serialize session data as Ini-formatted string
+ * @param aUpdateAll
+ * Bool update all windows
+ * @returns string
+ */
+ _getCurrentState: function sss_getCurrentState(aUpdateAll) {
+ this._handleClosedWindows();
+
+ var activeWindow = this._getMostRecentBrowserWindow();
+
+ if (this._loadState == STATE_RUNNING) {
+ // update the data for all windows with activities since the last save operation
+ this._forEachBrowserWindow(function(aWindow) {
+ if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
+ return;
+ if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
+ this._collectWindowData(aWindow);
+ }
+ else { // always update the window features (whose change alone never triggers a save operation)
+ this._updateWindowFeatures(aWindow);
+ }
+ });
+ DirtyWindows.clear();
+ }
+
+ // collect the data for all windows
+ var total = [], ids = [];
+ var nonPopupCount = 0;
+ var ix;
+ for (ix in this._windows) {
+ if (this._windows[ix]._restoring) // window data is still in _statesToRestore
+ continue;
+ total.push(this._windows[ix]);
+ ids.push(ix);
+ if (!this._windows[ix].isPopup)
+ nonPopupCount++;
+ }
+ this._updateCookies(total);
+
+ // collect the data for all windows yet to be restored
+ for (ix in this._statesToRestore) {
+ for (let winData of this._statesToRestore[ix].windows) {
+ total.push(winData);
+ if (!winData.isPopup)
+ nonPopupCount++;
+ }
+ }
+
+ // shallow copy this._closedWindows to preserve current state
+ let lastClosedWindowsCopy = this._closedWindows.slice();
+
+ // If no non-popup browser window remains open, return the state of the last
+ // closed window(s). We only want to do this when we're actually "ending"
+ // the session.
+ //XXXzpao We should do this for _restoreLastWindow == true, but that has
+ // its own check for popups. c.f. bug 597619
+ if (AppConstants.platform != "macosx" &&
+ nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 &&
+ this._loadState == STATE_QUITTING) {
+ // prepend the last non-popup browser window, so that if the user loads more tabs
+ // at startup we don't accidentally add them to a popup window
+ do {
+ total.unshift(lastClosedWindowsCopy.shift())
+ } while (total[0].isPopup)
+ }
+
+ if (activeWindow) {
+ this.activeWindowSSiCache = activeWindow.__SSi || "";
+ }
+ ix = ids.indexOf(this.activeWindowSSiCache);
+ // We don't want to restore focus to a minimized window.
+ if (ix != -1 && total[ix].sizemode == "minimized")
+ ix = -1;
+
+ let session = {
+ state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
+ lastUpdate: Date.now(),
+ startTime: this._sessionStartTime,
+ recentCrashes: this._recentCrashes
+ };
+
+ return {
+ windows: total,
+ selectedWindow: ix + 1,
+ _closedWindows: lastClosedWindowsCopy,
+ session: session
+ };
+ },
+
+ /**
+ * serialize session data for a window
+ * @param aWindow
+ * Window reference
+ * @returns string
+ */
+ _getWindowState: function sss_getWindowState(aWindow) {
+ if (!this._isWindowLoaded(aWindow))
+ return this._statesToRestore[aWindow.__SS_restoreID];
+
+ if (this._loadState == STATE_RUNNING) {
+ this._collectWindowData(aWindow);
+ }
+
+ let windows = [this._windows[aWindow.__SSi]];
+ this._updateCookies(windows);
+
+ return { windows: windows };
+ },
+
+ _collectWindowData: function sss_collectWindowData(aWindow) {
+ if (!this._isWindowLoaded(aWindow))
+ return;
+
+ // update the internal state data for this window
+ this._saveWindowHistory(aWindow);
+ this._updateTextAndScrollData(aWindow);
+ this._updateWindowFeatures(aWindow);
+
+ // Make sure we keep __SS_lastSessionWindowID around for cases like entering
+ // or leaving PB mode.
+ if (aWindow.__SS_lastSessionWindowID)
+ this._windows[aWindow.__SSi].__lastSessionWindowID =
+ aWindow.__SS_lastSessionWindowID;
+
+ DirtyWindows.remove(aWindow);
+ },
+
+/* ........ Restoring Functionality .............. */
+
+ /**
+ * restore features to a single window
+ * @param aWindow
+ * Window reference
+ * @param aState
+ * JS object or its eval'able source
+ * @param aOverwriteTabs
+ * bool overwrite existing tabs w/ new ones
+ * @param aFollowUp
+ * bool this isn't the restoration of the first window
+ */
+ restoreWindow: function sss_restoreWindow(aWindow, aState, aOverwriteTabs, aFollowUp) {
+ if (!aFollowUp) {
+ this.windowToFocus = aWindow;
+ }
+ // initialize window if necessary
+ if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
+ this.onLoad(aWindow);
+
+ try {
+ var root = typeof aState == "string" ? JSON.parse(aState) : aState;
+ if (!root.windows[0]) {
+ this._sendRestoreCompletedNotifications();
+ return; // nothing to restore
+ }
+ }
+ catch (ex) { // invalid state object - don't restore anything
+ debug(ex);
+ this._sendRestoreCompletedNotifications();
+ return;
+ }
+
+ // We're not returning from this before we end up calling restoreHistoryPrecursor
+ // for this window, so make sure we send the SSWindowStateBusy event.
+ this._sendWindowStateEvent(aWindow, "Busy");
+
+ if (root._closedWindows)
+ this._closedWindows = root._closedWindows;
+
+ var winData;
+ if (!aState.selectedWindow || aState.selectedWindow > aState.windows.length) {
+ aState.selectedWindow = 0;
+ }
+ // open new windows for all further window entries of a multi-window session
+ // (unless they don't contain any tab data)
+ for (var w = 1; w < root.windows.length; w++) {
+ winData = root.windows[w];
+ if (winData && winData.tabs && winData.tabs[0]) {
+ var window = this._openWindowWithState({ windows: [winData] });
+ if (w == aState.selectedWindow - 1) {
+ this.windowToFocus = window;
+ }
+ }
+ }
+ winData = root.windows[0];
+ if (!winData.tabs) {
+ winData.tabs = [];
+ }
+ // don't restore a single blank tab when we've had an external
+ // URL passed in for loading at startup (cf. bug 357419)
+ else if (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 &&
+ (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
+ winData.tabs = [];
+ }
+
+ var tabbrowser = aWindow.getBrowser();
+ var openTabCount = aOverwriteTabs ? tabbrowser.browsers.length : -1;
+ var newTabCount = winData.tabs.length;
+ var tabs = [];
+
+ // disable smooth scrolling while adding, moving, removing and selecting tabs
+ var tabstrip = tabbrowser.tabContainer.arrowScrollbox;
+ var smoothScroll = tabstrip.smoothScroll;
+ tabstrip.smoothScroll = false;
+
+ // make sure that the selected tab won't be closed in order to
+ // prevent unnecessary flickering
+ if (aOverwriteTabs && tabbrowser.tabContainer.selectedIndex >= newTabCount)
+ tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
+
+ for (var t = 0; t < newTabCount; t++) {
+ tabs.push(t < openTabCount ?
+ tabbrowser.tabs[t] :
+ // Ftr, SeaMonkey doesn't support animation (yet).
+ tabbrowser.addTab("about:blank"));
+ // when resuming at startup: add additionally requested pages to the end
+ if (!aOverwriteTabs && root._firstTabs) {
+ tabbrowser.moveTabTo(tabs[t], t);
+ }
+ }
+
+ // If overwriting tabs, we want to reset each tab's "restoring" state. Since
+ // we're overwriting those tabs, they should no longer be restoring. The
+ // tabs will be rebuilt and marked if they need to be restored after loading
+ // state (in restoreHistoryPrecursor).
+ if (aOverwriteTabs) {
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ if (tabbrowser.browsers[i].__SS_restoreState)
+ this._resetTabRestoringState(tabbrowser.tabs[i]);
+ }
+ }
+
+ // We want to set up a counter on the window that indicates how many tabs
+ // in this window are unrestored. This will be used in restoreNextTab to
+ // determine if gRestoreTabsProgressListener should be removed from the window.
+ // If we aren't overwriting existing tabs, then we want to add to the existing
+ // count in case there are still tabs restoring.
+ if (!aWindow.__SS_tabsToRestore)
+ aWindow.__SS_tabsToRestore = 0;
+ if (aOverwriteTabs)
+ aWindow.__SS_tabsToRestore = newTabCount;
+ else
+ aWindow.__SS_tabsToRestore += newTabCount;
+
+ // We want to correlate the window with data from the last session, so
+ // assign another id if we have one. Otherwise clear so we don't do
+ // anything with it.
+ delete aWindow.__SS_lastSessionWindowID;
+ if (winData.__lastSessionWindowID)
+ aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
+
+ // when overwriting tabs, remove all superflous ones
+ for (t = openTabCount - 1; t >= newTabCount; t--) {
+ tabbrowser.removeTab(tabbrowser.tabs[t]);
+ }
+
+ if (aOverwriteTabs) {
+ this.restoreWindowFeatures(aWindow, winData);
+ delete this._windows[aWindow.__SSi].extData;
+ }
+ if (winData.cookies) {
+ this.restoreCookies(winData.cookies);
+ }
+ if (winData.extData) {
+ if (!this._windows[aWindow.__SSi].extData) {
+ this._windows[aWindow.__SSi].extData = {};
+ }
+ for (var key in winData.extData) {
+ this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
+ }
+ }
+ if (aOverwriteTabs || root._firstTabs) {
+ this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || [];
+ }
+
+ this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs,
+ (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0);
+
+ // set smoothScroll back to the original value
+ tabstrip.smoothScroll = smoothScroll;
+
+ this._sendRestoreCompletedNotifications();
+ },
+
+ /**
+ * Manage history restoration for a window
+ * @param aWindow
+ * Window to restore the tabs into
+ * @param aTabs
+ * Array of tab references
+ * @param aTabData
+ * Array of tab data
+ * @param aSelectTab
+ * Index of selected tab
+ * @param aIx
+ * Index of the next tab to check readyness for
+ * @param aCount
+ * Counter for number of times delaying b/c browser or history aren't ready
+ */
+ restoreHistoryPrecursor:
+ function sss_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount) {
+ var tabbrowser = aWindow.getBrowser();
+
+ // make sure that all browsers and their histories are available
+ // - if one's not, resume this check in 100ms (repeat at most 10 times)
+ for (var t = aIx; t < aTabs.length; t++) {
+ try {
+ if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) {
+ throw new Error();
+ }
+ }
+ catch (ex) { // in case browser or history aren't ready yet
+ if (aCount < 10) {
+ var restoreHistoryFunc = function(self) {
+ self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount + 1);
+ }
+ aWindow.setTimeout(restoreHistoryFunc, 100, this);
+ return;
+ }
+ }
+ }
+
+ if (!this._isWindowLoaded(aWindow)) {
+ // from now on, the data will come from the actual window
+ delete this._statesToRestore[aWindow.__SS_restoreID];
+ delete aWindow.__SS_restoreID;
+ delete this._windows[aWindow.__SSi]._restoring;
+
+ // It's important to set the window state to dirty so that
+ // we collect their data for the first time when saving state.
+ DirtyWindows.add(aWindow);
+ }
+
+ if (aTabs.length == 0) {
+ // this is normally done in restoreHistory() but as we're returning early
+ // here we need to take care of it.
+ this._sendWindowStateEvent(aWindow, "Ready");
+ return;
+ }
+
+ if (aTabs.length > 1) {
+ // Load hidden tabs last, by pushing them to the end of the list
+ let unhiddenTabs = aTabs.length;
+ for (let t = 0; t < unhiddenTabs; ) {
+ if (aTabData[t].hidden) {
+ aTabs = aTabs.concat(aTabs.splice(t, 1));
+ aTabData = aTabData.concat(aTabData.splice(t, 1));
+ if (aSelectTab > t)
+ --aSelectTab;
+ --unhiddenTabs;
+ continue;
+ }
+ ++t;
+ }
+
+ // Determine if we can optimize & load visible tabs first
+ let maxVisibleTabs = Math.ceil(tabbrowser.tabContainer.arrowScrollbox.scrollClientSize /
+ aTabs[unhiddenTabs - 1].getBoundingClientRect().width);
+
+ // make sure we restore visible tabs first, if there are enough
+ if (maxVisibleTabs < unhiddenTabs && aSelectTab > 1) {
+ let firstVisibleTab = 0;
+ if (unhiddenTabs - maxVisibleTabs > aSelectTab) {
+ // aSelectTab is leftmost since we scroll to it when possible
+ firstVisibleTab = aSelectTab - 1;
+ } else {
+ // aSelectTab is rightmost or no more room to scroll right
+ firstVisibleTab = unhiddenTabs - maxVisibleTabs;
+ }
+ aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs);
+ aTabData = aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData);
+ aSelectTab -= firstVisibleTab;
+ }
+ }
+
+ // make sure to restore the selected tab first (if any)
+ if (aSelectTab-- && aTabs[aSelectTab]) {
+ aTabs.unshift(aTabs.splice(aSelectTab, 1)[0]);
+ aTabData.unshift(aTabData.splice(aSelectTab, 1)[0]);
+ tabbrowser.selectedTab = aTabs[0];
+ }
+
+ // Prepare the tabs so that they can be properly restored. We'll pin/unpin
+ // and show/hide tabs as necessary. We'll also set the labels, user typed
+ // value, and attach a copy of the tab's data in case we close it before
+ // it's been restored.
+ for (t = 0; t < aTabs.length; t++) {
+ let tab = aTabs[t];
+ let browser = tabbrowser.getBrowserForTab(tab);
+ let tabData = aTabData[t];
+
+ if (tabData.hidden) {
+ tab.setAttribute("hidden", true);
+ } else {
+ if (tab.hidden) {
+ tab.removeAttribute("hidden");
+ }
+ }
+
+ for (let name in tabData.attributes)
+ this.xulAttributes[name] = true;
+
+ // keep the data around to prevent dataloss in case
+ // a tab gets closed before it's been properly restored
+ browser.__SS_data = tabData;
+ browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
+
+ // Make sure that set/getTabValue will set/read the correct data by
+ // wiping out any current value in tab.__SS_extdata.
+ delete tab.__SS_extdata;
+
+ if (!tabData.entries || tabData.entries.length == 0) {
+ // make sure to blank out this tab's content
+ // (just purging the tab's history won't be enough)
+ browser.contentDocument.location = "about:blank";
+ continue;
+ }
+
+ browser.stop(); // in case about:blank isn't done yet
+
+ // wall-paper fix for bug 439675: make sure that the URL to be loaded
+ // is always visible in the address bar
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ let activePageData = tabData.entries[activeIndex] || null;
+ let uri = activePageData ? activePageData.url || null : null;
+
+ // NB: we won't set initial URIs (about:blank, about:privatebrowsing, etc.)
+ // here because their load will not normally trigger a location bar clearing
+ // when they finish loading (to avoid race conditions where we then
+ // clear user input instead), so we shouldn't set them here either.
+ // They also don't fall under the issues in bug 439675 where user input
+ // needs to be preserved if the load doesn't succeed.
+ if (!browser.userTypedValue && uri && !aWindow.gInitialPages.has(uri)) {
+ browser.userTypedValue = uri;
+ }
+
+ // Also make sure currentURI is set so that switch-to-tab works before
+ // the tab is restored. We'll reset this to about:blank when we try to
+ // restore the tab to ensure that docshell doeesn't get confused.
+ if (uri)
+ browser.docShell.setCurrentURI(this._getURIFromString(uri));
+
+ // If the page has a title, set it.
+ if (activePageData) {
+ if (activePageData.title) {
+ tab.label = activePageData.title;
+ tab.crop = "end";
+ } else if (activePageData.url != "about:blank") {
+ tab.label = activePageData.url;
+ tab.crop = "center";
+ }
+ }
+ }
+
+ // helper hashes for ensuring unique frame IDs and unique document
+ // identifiers.
+ var idMap = { used: {} };
+ var docIdentMap = {};
+ this.restoreHistory(aWindow, aTabs, aTabData, idMap, docIdentMap);
+ },
+
+ /**
+ * Restore history for a window
+ * @param aWindow
+ * Window reference
+ * @param aTabs
+ * Array of tab references
+ * @param aTabData
+ * Array of tab data
+ * @param aIdMap
+ * Hash for ensuring unique frame IDs
+ */
+ restoreHistory:
+ function sss_restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap) {
+ // if the tab got removed before being completely restored, then skip it
+ while (aTabs.length > 0 && (!aTabs[0].parentNode || !aTabs[0].linkedBrowser)) {
+ aTabs.shift();
+ aTabData.shift();
+ }
+ if (aTabs.length == 0) {
+ // At this point we're essentially ready for consumers to read/write data
+ // via the sessionstore API so we'll send the SSWindowStateReady event.
+ this._sendWindowStateEvent(aWindow, "Ready");
+ return; // no more tabs to restore
+ }
+
+ var tab = aTabs.shift();
+ var tabData = aTabData.shift();
+
+ var browser = aWindow.getBrowser().getBrowserForTab(tab);
+ var history = browser.webNavigation.sessionHistory;
+
+ if (history.count > 0) {
+ history.PurgeHistory(history.count);
+ }
+
+ browser.__SS_shistoryListener = new SessionStoreSHistoryListener(this, tab);
+ history.addSHistoryListener(browser.__SS_shistoryListener);
+
+ if (!tabData.entries) {
+ tabData.entries = [];
+ }
+ if (tabData.extData) {
+ tab.__SS_extdata = {};
+ for (let key in tabData.extData)
+ tab.__SS_extdata[key] = tabData.extData[key];
+ }
+ else
+ delete tab.__SS_extdata;
+
+ for (var i = 0; i < tabData.entries.length; i++) {
+ let cloneEntry = false;
+ //XXXzpao Wallpaper patch for bug 509315
+ if (!tabData.entries[i].url)
+ continue;
+
+ let shEntry = this._deserializeHistoryEntry(tabData.entries[i],
+ aIdMap, aDocIdentMap);
+ try {
+ history.addEntry(shEntry, true);
+ }
+ catch (ex) {
+ cloneEntry = true;
+ }
+
+ // Workaround for bug 1466911.
+ // FIXME Remove this after the issue which caused the exception above
+ // to be thrown has been fixed.
+ if (cloneEntry) {
+ shEntry = shEntry.clone();
+ shEntry.abandonBFCacheEntry();
+
+ try {
+ history.addEntry(shEntry, true);
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ // make sure to reset the capabilities and attributes, in case this tab gets reused
+ var disallow = (tabData.disallow)?tabData.disallow.split(","):[];
+ CAPABILITIES.forEach(function(aCapability) {
+ browser.docShell["allow" + aCapability] = !disallow.includes(aCapability);
+ });
+ for (let name in this.xulAttributes)
+ tab.removeAttribute(name);
+ for (let name in tabData.attributes)
+ tab.setAttribute(name, tabData.attributes[name]);
+
+ if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell)
+ this._deserializeSessionStorage(tabData.storage, browser.docShell);
+
+ // notify the tabbrowser that the tab chrome has been restored
+ var event = aWindow.document.createEvent("Events");
+ event.initEvent("SSTabRestoring", true, false);
+ tab.dispatchEvent(event);
+
+ // Restore the history in the next tab
+ Services.tm.mainThread.dispatch(this.restoreHistory.bind(this, aWindow,
+ aTabs, aTabData, aIdMap, aDocIdentMap), Ci.nsIThread.DISPATCH_NORMAL);
+
+ // This could cause us to ignore the max_concurrent_tabs pref a bit, but
+ // it ensures each window will have its selected tab loaded.
+ if (aWindow.getBrowser().selectedBrowser == browser) {
+ this.restoreTab(tab);
+ }
+ else {
+ // Put the tab into the right bucket
+ if (tabData.hidden)
+ this._tabsToRestore.hidden.push(tab);
+ else
+ this._tabsToRestore.visible.push(tab);
+ this.restoreNextTab();
+ }
+ },
+
+ /**
+ * Restores the specified tab. If the tab can't be restored (eg, no history or
+ * calling gotoIndex fails), then state changes will be rolled back.
+ * This method will check if gTabsProgressListener is attached to the tab's
+ * window, ensuring that we don't get caught without one.
+ * This method removes the session history listener right before starting to
+ * attempt a load. This will prevent cases of "stuck" listeners.
+ * If this method returns false, then it is up to the caller to decide what to
+ * do. In the common case (restoreNextTab), we will want to then attempt to
+ * restore the next tab. In the other case (selecting the tab, reloading the
+ * tab), the caller doesn't actually want to do anything if no page is loaded.
+ *
+ * @param aTab
+ * the tab to restore
+ *
+ * @returns true/false indicating whether or not a load actually happened
+ */
+ restoreTab: function sss_restoreTab(aTab) {
+ let window = aTab.ownerDocument.defaultView;
+ let browser = aTab.linkedBrowser;
+ let tabData = browser.__SS_data;
+
+ // If the tabData which we're sending down has any sessionStorage associated
+ // with it, we need to send down permissions for the domains, as this
+ // information will be needed to correctly restore the session.
+ if (tabData.storage) {
+ for (let origin of Object.getOwnPropertyNames(tabData.storage)) {
+ try {
+ let {frameLoader} = browser;
+ if (frameLoader.tabParent) {
+ let attrs = browser.contentPrincipal.originAttributes;
+ let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs);
+ frameLoader.tabParent.transmitPermissionsForPrincipal(principal);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ // There are cases within where we haven't actually started a load. In that
+ // that case we'll reset state changes we made and return false to the caller
+ // can handle appropriately.
+ let didStartLoad = false;
+
+ // Make sure that the tabs progress listener is attached to this window
+ this._ensureTabsProgressListener(window);
+
+ // Make sure that this tab is removed from _tabsToRestore
+ this._removeTabFromTabsToRestore(aTab);
+
+ // Increase our internal count.
+ this._tabsRestoringCount++;
+
+ // Set this tab's state to restoring
+ browser.__SS_restoreState = TAB_STATE_RESTORING;
+
+ // Remove the history listener, since we no longer need it once we start restoring
+ this._removeSHistoryListener(aTab);
+
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ if (activeIndex >= tabData.entries.length)
+ activeIndex = tabData.entries.length - 1;
+
+ // Reset currentURI.
+ browser.webNavigation.setCurrentURI(this._getURIFromString("about:blank"));
+
+ // Attach data that will be restored on "load" event, after tab is restored.
+ if (activeIndex > -1) {
+ // restore those aspects of the currently active documents which are not
+ // preserved in the plain history entries (mainly scroll state and text data)
+ browser.__SS_restore_data = tabData.entries[activeIndex] || {};
+ browser.__SS_restore_pageStyle = tabData.pageStyle || "";
+ browser.__SS_restore_tab = aTab;
+
+ didStartLoad = true;
+ try {
+ // In order to work around certain issues in session history, we need to
+ // force session history to update its internal index and call reload
+ // instead of gotoIndex. See bug 597315.
+ var sessionHistory = browser.webNavigation.sessionHistory;
+ sessionHistory.index = activeIndex;
+ sessionHistory.reloadCurrentEntry();
+ }
+ catch (ex) {
+ // ignore page load errors
+ aTab.removeAttribute("busy");
+ didStartLoad = false;
+ }
+ }
+
+ // Handle userTypedValue. Setting userTypedValue seems to update gURLbar
+ // as needed. Calling loadURI will cancel form filling in restoreDocument
+ if (tabData.userTypedValue) {
+ browser.userTypedValue = tabData.userTypedValue;
+
+ if (tabData.userTypedClear) {
+ // Make it so that we'll enter restoreDocument on page load. We will
+ // fire SSTabRestored from there. We don't have any form data to
+ // restore so we can just set the URL to null.
+ browser.__SS_restore_data = { url: null };
+ browser.__SS_restore_tab = aTab;
+ didStartLoad = true;
+ browser.webNavigation
+ .loadURI(tabData.userTypedValue,
+ Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
+ null, null, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ }
+ }
+
+ // If we didn't start a load, then we won't reset this tab through the usual
+ // channel (via the progress listener), so reset the tab ourselves. We will
+ // also send SSTabRestored since this tab has technically been restored.
+ if (!didStartLoad) {
+ this._sendTabRestoredNotification(aTab);
+ this._resetTabRestoringState(aTab);
+ }
+
+ return didStartLoad;
+ },
+
+ /**
+ * This _attempts_ to restore the next available tab. If the restore fails,
+ * then we will attempt the next one.
+ * There are conditions where this won't do anything:
+ * if we're in the process of quitting
+ * if there are no tabs to restore
+ * if we have already reached the limit for number of tabs to restore
+ */
+ restoreNextTab: function sss_restoreNextTab() {
+ // If we call in here while quitting, we don't actually want to do anything
+ if (this._loadState == STATE_QUITTING)
+ return;
+
+ // If it's not possible to restore anything, then just bail out.
+ if (this._maxConcurrentTabRestores >= 0 &&
+ this._tabsRestoringCount >= this._maxConcurrentTabRestores)
+ return;
+
+ // Look in visible, then hidden
+ let nextTabArray;
+ if (this._tabsToRestore.visible.length) {
+ nextTabArray = this._tabsToRestore.visible;
+ }
+ else if (this._tabsToRestore.hidden.length) {
+ nextTabArray = this._tabsToRestore.hidden;
+ }
+
+ if (nextTabArray) {
+ let tab = nextTabArray.shift();
+ let didStartLoad = this.restoreTab(tab);
+ // If we don't start a load in the restored tab (eg, no entries) then we
+ // want to attempt to restore the next tab.
+ if (!didStartLoad)
+ this.restoreNextTab();
+ }
+ },
+
+ /**
+ * expands serialized history data into a session-history-entry instance
+ * @param aEntry
+ * Object containing serialized history data for a URL
+ * @param aIdMap
+ * Hash for ensuring unique frame IDs
+ * @returns nsISHEntry
+ */
+ _deserializeHistoryEntry:
+ function sss_deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
+
+ var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]
+ .createInstance(Ci.nsISHEntry);
+
+ shEntry.URI = this._getURIFromString(aEntry.url);
+ shEntry.title = aEntry.title || aEntry.url;
+ if (aEntry.subframe)
+ shEntry.isSubFrame = aEntry.subframe || false;
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ if (aEntry.contentType)
+ shEntry.contentType = aEntry.contentType;
+ if (aEntry.referrer)
+ shEntry.referrerURI = this._getURIFromString(aEntry.referrer);
+
+ if (aEntry.cacheKey) {
+ var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]
+ .createInstance(Ci.nsISupportsPRUint32);
+ cacheKey.data = aEntry.cacheKey;
+ shEntry.cacheKey = cacheKey;
+ }
+
+ if (aEntry.ID) {
+ // get a new unique ID for this frame (since the one from the last
+ // start might already be in use)
+ var id = aIdMap[aEntry.ID] || 0;
+ if (!id) {
+ for (id = Date.now(); id in aIdMap.used; id++);
+ aIdMap[aEntry.ID] = id;
+ aIdMap.used[id] = true;
+ }
+ shEntry.ID = id;
+ }
+
+ // If we have the legacy docshellID on our entry, upgrade it to a
+ // docshellUUID by going through the mapping.
+ if (aEntry.docshellID) {
+ if (!this._docshellUUIDMap.has(aEntry.docshellID)) {
+ // Convert the nsID to a string so that the docshellUUID property
+ // is correctly stored as a string.
+ this._docshellUUIDMap.set(aEntry.docshellID,
+ uuidGenerator.generateUUID().toString());
+ }
+ aEntry.docshellUUID = this._docshellUUIDMap.get(aEntry.docshellID);
+ delete aEntry.docshellID;
+ }
+
+ if (aEntry.docshellUUID)
+ shEntry.docshellID = Components.ID(aEntry.docshellUUID);
+
+ if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
+ shEntry.stateData =
+ Cc["@mozilla.org/docshell/structured-clone-container;1"]
+ .createInstance(Ci.nsIStructuredCloneContainer);
+
+ shEntry.stateData.initFromBase64(aEntry.structuredCloneState,
+ aEntry.structuredCloneVersion);
+ }
+
+ if (aEntry.scroll) {
+ var scrollPos = (aEntry.scroll || "0,0").split(",");
+ scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
+ shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
+ }
+
+ if (aEntry.postdata_b64) {
+ var postdata = atob(aEntry.postdata_b64);
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(postdata, postdata.length);
+ shEntry.postData = stream;
+ }
+
+ let childDocIdents = {};
+ if (aEntry.docIdentifier) {
+ // If we have a serialized document identifier, try to find an SHEntry
+ // which matches that doc identifier and adopt that SHEntry's
+ // BFCacheEntry. If we don't find a match, insert shEntry as the match
+ // for the document identifier.
+ let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
+ if (!matchingEntry) {
+ matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
+ aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
+ }
+ else {
+ shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
+ childDocIdents = matchingEntry.childDocIdents;
+ }
+ }
+
+ // The field entry.owner_b64 got renamed to entry.triggeringPricipal_b64 in
+ // Bug 1286472 and Bug 1334780 for SeaMonkey.
+ // To remain backward compatible we still have to support that field for a
+ // few cycles before we can remove it.
+ if (aEntry.owner_b64) {
+ aEntry.triggeringPricipal_b64 = aEntry.owner_b64;
+ delete aEntry.owner_b64;
+ }
+
+ // Before introducing the concept of principalToInherit we only had
+ // a triggeringPrincipal within every entry which basically is the
+ // equivalent of the new principalToInherit. To avoid compatibility
+ // issues, we first check if the entry has entries for
+ // triggeringPrincipal_base64 and principalToInherit_base64. If not
+ // we fall back to using the principalToInherit (which is stored
+ // as triggeringPrincipal_b64) as the triggeringPrincipal and
+ // the principalToInherit.
+ // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666.
+ if (aEntry.triggeringPrincipal_base64 || aEntry.principalToInherit_base64) {
+ if (aEntry.triggeringPrincipal_base64) {
+ shEntry.triggeringPrincipal =
+ Utils.deserializePrincipal(aEntry.triggeringPrincipal_base64);
+ }
+ if (aEntry.principalToInherit_base64) {
+ shEntry.principalToInherit =
+ Utils.deserializePrincipal(aEntry.principalToInherit_base64);
+ }
+ } else if (aEntry.triggeringPrincipal_b64) {
+ shEntry.triggeringPrincipal = Utils.deserializePrincipal(aEntry.triggeringPrincipal_b64);
+ shEntry.principalToInherit = shEntry.triggeringPrincipal;
+ }
+
+ if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
+ for (var i = 0; i < aEntry.children.length; i++) {
+ //XXXzpao Wallpaper patch for bug 509315
+ if (!aEntry.children[i].url)
+ continue;
+
+ // We're mysteriously getting sessionrestore.js files with a cycle in
+ // the doc-identifier graph. (That is, we have an entry where doc
+ // identifier A is an ancestor of doc identifier B, and another entry
+ // where doc identifier B is an ancestor of A.)
+ //
+ // If we were to respect these doc identifiers, we'd create a cycle in
+ // the SHEntries themselves, which causes the docshell to loop forever
+ // when it looks for the root SHEntry.
+ //
+ // So as a hack to fix this, we restrict the scope of a doc identifier
+ // to be a node's siblings and cousins, and pass childDocIdents, not
+ // aDocIdents, to _deserializeHistoryEntry. That is, we say that two
+ // SHEntries with the same doc identifier have the same document iff
+ // they have the same parent or their parents have the same document.
+
+ shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap,
+ childDocIdents), i);
+ }
+ }
+
+ return shEntry;
+ },
+
+ /**
+ * restores all sessionStorage "super cookies"
+ * @param aStorageData
+ * Storage data to be restored
+ * @param aDocShell
+ * A tab's docshell (containing the sessionStorage)
+ */
+ _deserializeSessionStorage: function sss_deserializeSessionStorage(aStorageData, aDocShell) {
+
+ for (let origin of Object.keys(aStorageData)) {
+ let data = aStorageData[origin];
+
+ let principal;
+
+ try {
+ // NOTE: We record the full origin for the URI which the
+ // sessionStorage is being captured for. As of bug 1319114 this code
+ // stopped parsing any origins which have originattributes correctly, as
+ // it decided to use the origin attributes from the docshell, and try to
+ // interpret the origin as a URI. Since bug 1473426 code now correctly
+ // parses the full origin, and then discards the origin attributes, to
+ // make the behavior line up with the original intentions in bug 1235657
+ // while preserving the ability to read all session storage from
+ // previous versions. In the future, if this behavior is desired, we may
+ // want to use the spec instead of the origin as the key, and avoid
+ // transmitting origin attribute information which we then discard when
+ // restoring.
+ //
+ // If changing this logic, make sure to also change the principal
+ // computation logic in restoretab.
+ let attrs = aDocShell.getOriginAttributes();
+ let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
+ principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs);
+ } catch (e) {
+ Cu.reportError(e);
+ continue;
+ }
+
+ let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+ let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ // There is no need to pass documentURI, it's only used to fill documentURI property of
+ // domstorage event, which in this case has no consumer. Prevention of events in case
+ // of missing documentURI will be solved in a followup bug to bug 600307.
+ let storage = storageManager.createStorage(window, principal, "");
+
+ for (let key of Object.keys(data)) {
+ try {
+ storage.setItem(key, data[key]);
+ } catch (e) {
+ // Throws e.g. for URIs that can't have sessionStorage.
+ Cu.reportError(e);
+ }
+ }
+ }
+ },
+
+ /**
+ * Restore properties to a loaded document
+ */
+ restoreDocument: function sss_restoreDocument(aWindow, aBrowser, aEvent) {
+ // wait for the top frame to be loaded completely
+ if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) {
+ return;
+ }
+
+ // always call this before injecting content into a document!
+ function hasExpectedURL(aDocument, aURL) {
+ return !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, "");
+ }
+
+ function restoreFormData(aDocument, aData, aURL) {
+ for (let key in aData) {
+ if (!hasExpectedURL(aDocument, aURL))
+ return;
+
+ let node = key.charAt(0) == "#" ? aDocument.getElementById(key.slice(1)) :
+ XPathGenerator.resolve(aDocument, key);
+ if (!node)
+ continue;
+
+ let eventType;
+ let value = aData[key];
+ if (typeof value == "string" && node.type != "file") {
+ if (node.value == value)
+ continue; // don't dispatch an input event for no change
+
+ node.value = value;
+ eventType = "input";
+ }
+ else if (typeof value == "boolean") {
+ if (node.checked == value)
+ continue; // don't dispatch a change event for no change
+
+ node.checked = value;
+ eventType = "change";
+ }
+ else if (typeof value == "number") {
+ // We saved the value blindly since selects take more work to determine
+ // default values. So now we should check to avoid unnecessary events.
+ if (node.selectedIndex == value)
+ continue;
+
+ try {
+ node.selectedIndex = value;
+ eventType = "change";
+ } catch (ex) { /* throws for invalid indices */ }
+ }
+ else if (value && value.fileList && value.type == "file" && node.type == "file") {
+ node.mozSetFileNameArray(value.fileList, value.fileList.length);
+ eventType = "input";
+ }
+ else if (value && typeof value.indexOf == "function" && node.options) {
+ Array.from(node.options).forEach(function(aOpt, aIx) {
+ aOpt.selected = value.includes(aIx);
+
+ // Only fire the event here if this wasn't selected by default
+ if (!aOpt.defaultSelected)
+ eventType = "change";
+ });
+ }
+
+ // Fire events for this node if applicable
+ if (eventType) {
+ let event = aDocument.createEvent("UIEvents");
+ event.initUIEvent(eventType, true, true, aDocument.defaultView, 0);
+ node.dispatchEvent(event);
+ }
+ }
+ }
+
+ let selectedPageStyle = aBrowser.__SS_restore_pageStyle;
+ function restoreTextDataAndScrolling(aContent, aData, aPrefix) {
+ if (aData.formdata)
+ restoreFormData(aContent.document, aData.formdata, aData.url);
+ if (aData.innerHTML) {
+ aWindow.setTimeout(function() {
+ if (aContent.document.designMode == "on" &&
+ hasExpectedURL(aContent.document, aData.url)) {
+ aContent.document.body.innerHTML = aData.innerHTML;
+ }
+ }, 0);
+ }
+ var match;
+ if (aData.scroll && (match = /(\d+),(\d+)/.exec(aData.scroll)) != null) {
+ aContent.scrollTo(match[1], match[2]);
+ }
+ Array.from(aContent.document.styleSheets).forEach(function(aSS) {
+ aSS.disabled = aSS.title && aSS.title != selectedPageStyle;
+ });
+ for (var i = 0; i < aContent.frames.length; i++) {
+ if (aData.children && aData.children[i] &&
+ hasExpectedURL(aContent.document, aData.url)) {
+ restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], aPrefix + i + "|");
+ }
+ }
+ }
+
+ // don't restore text data and scrolling state if the user has navigated
+ // away before the loading completed (except for in-page navigation)
+ if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) {
+ var content = aEvent.originalTarget.defaultView;
+ restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, "");
+ aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle";
+ }
+
+ // notify the tabbrowser that this document has been completely restored
+ this._sendTabRestoredNotification(aBrowser.__SS_restore_tab);
+
+ delete aBrowser.__SS_restore_data;
+ delete aBrowser.__SS_restore_pageStyle;
+ delete aBrowser.__SS_restore_tab;
+ },
+
+ /**
+ * Restore visibility and dimension features to a window
+ * @param aWindow
+ * Window reference
+ * @param aWinData
+ * Object containing session data for the window
+ */
+ restoreWindowFeatures: function sss_restoreWindowFeatures(aWindow, aWinData) {
+ var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[];
+ WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) {
+ aWindow[aItem].visible = !hidden.includes(aItem);
+ });
+
+ if (aWinData.isPopup)
+ this._windows[aWindow.__SSi].isPopup = true;
+ else
+ delete this._windows[aWindow.__SSi].isPopup;
+
+ Services.tm.mainThread.dispatch(this.restoreDimensions.bind(this, aWindow,
+ +aWinData.width || 0,
+ +aWinData.height || 0,
+ "screenX" in aWinData ? +aWinData.screenX : NaN,
+ "screenY" in aWinData ? +aWinData.screenY : NaN,
+ aWinData.sizemode || "", aWinData.sidebar || ""),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Restore a window's dimensions
+ * @param aWidth
+ * Window width
+ * @param aHeight
+ * Window height
+ * @param aLeft
+ * Window left
+ * @param aTop
+ * Window top
+ * @param aSizeMode
+ * Window size mode (eg: maximized)
+ * @param aSidebar
+ * Sidebar command
+ */
+ restoreDimensions: function sss_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) {
+ var win_ = this._getWindowDimension.bind(this, aWindow);
+
+ // find available space on the screen where this window is being placed
+ let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight);
+ if (screen) {
+ let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
+ screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
+ // constrain the dimensions to the actual space available
+ if (aWidth > screenWidth.value) {
+ aWidth = screenWidth.value;
+ }
+ if (aHeight > screenHeight.value) {
+ aHeight = screenHeight.value;
+ }
+ // and then pull the window within the screen's bounds
+ if (aLeft < screenLeft.value) {
+ aLeft = screenLeft.value;
+ } else if (aLeft + aWidth > screenLeft.value + screenWidth.value) {
+ aLeft = screenLeft.value + screenWidth.value - aWidth;
+ }
+ if (aTop < screenTop.value) {
+ aTop = screenTop.value;
+ } else if (aTop + aHeight > screenTop.value + screenHeight.value) {
+ aTop = screenTop.value + screenHeight.value - aHeight;
+ }
+ }
+
+ // only modify those aspects which aren't correct yet
+ if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) {
+ aWindow.resizeTo(aWidth, aHeight);
+ }
+ if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
+ aWindow.moveTo(aLeft, aTop);
+ }
+ if (aSizeMode && win_("sizemode") != aSizeMode)
+ {
+ switch (aSizeMode)
+ {
+ case "maximized":
+ aWindow.maximize();
+ break;
+ case "minimized":
+ aWindow.minimize();
+ break;
+ case "normal":
+ aWindow.restore();
+ break;
+ }
+ }
+ var sidebar = aWindow.document.getElementById("sidebar-box");
+ if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
+ aWindow.toggleSidebar(aSidebar);
+ }
+ // since resizing/moving a window brings it to the foreground,
+ // we might want to re-focus the last focused window
+ if (this.windowToFocus && this.windowToFocus.content) {
+ this.windowToFocus.content.focus();
+ }
+ },
+
+ /**
+ * Restores cookies
+ * @param aCookies
+ * Array of cookie objects
+ */
+ restoreCookies: function sss_restoreCookies(aCookies) {
+ // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
+ var MAX_EXPIRY = Math.pow(2, 62);
+ for (let i = 0; i < aCookies.length; i++) {
+ var cookie = aCookies[i];
+ try {
+ Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
+ cookie.value, !!cookie.secure, !!cookie.httponly,
+ true,
+ "expiry" in cookie ? cookie.expiry : MAX_EXPIRY,
+ "originAttributes" in cookie ? cookie.originAttributes : {});
+ }
+ catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering
+ }
+ },
+
+/* ........ Disk Access .............. */
+
+ /**
+ * save state delayed by N ms
+ * marks window as dirty (i.e. data update can't be skipped)
+ * @param aWindow
+ * Window reference
+ * @param aDelay
+ * Milliseconds to delay
+ */
+ saveStateDelayed: function sss_saveStateDelayed(aWindow, aDelay) {
+ if (aWindow) {
+ DirtyWindows.add(aWindow);
+ }
+
+ if (!this._saveTimer && this._resume_from_crash) {
+ // interval until the next disk operation is allowed
+ var minimalDelay = this._lastSaveTime + this._interval - Date.now();
+
+ // if we have to wait, set a timer, otherwise saveState directly
+ aDelay = Math.max(minimalDelay, aDelay || 2000);
+ if (aDelay > 0) {
+ this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ else {
+ this.saveState();
+ }
+ }
+ },
+
+ /**
+ * save state to disk
+ * @param aUpdateAll
+ * Bool update all windows
+ */
+ saveState: function sss_saveState(aUpdateAll) {
+ // if crash recovery is disabled, only save session resuming information
+ if (!this._resume_from_crash && this._loadState == STATE_RUNNING)
+ return;
+
+ // If crash recovery is disabled, we only want to resume with pinned tabs
+ // if we crash.
+ let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash;
+
+ var oState = this._getCurrentState(aUpdateAll);
+ if (!oState)
+ return;
+
+ // Persist the last session if we deferred restoring it
+ if (this._lastSessionState)
+ oState.lastSessionState = this._lastSessionState;
+
+ this._saveStateObject(oState);
+ },
+
+ /**
+ * write a state object to disk
+ */
+ _saveStateObject: function sss_saveStateObject(aStateObj) {
+ var stateString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ // parentheses are for backwards compatibility with older sessionstore files
+ stateString.data = this._toJSONString(aStateObj);
+
+ Services.obs.notifyObservers(stateString, "sessionstore-state-write");
+
+ // don't touch the file if an observer has deleted all state data
+ if (stateString.data)
+ this._writeFile(this._sessionFile, stateString.data);
+
+ this._lastSaveTime = Date.now();
+ },
+
+ /**
+ * delete session datafile and backup
+ */
+ _clearDisk: function sss_clearDisk() {
+ if (this._sessionFile.exists()) {
+ try {
+ this._sessionFile.remove(false);
+ }
+ catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
+ }
+ if (this._sessionFileBackup.exists()) {
+ try {
+ this._sessionFileBackup.remove(false);
+ }
+ catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
+ }
+ },
+
+/* ........ Auxiliary Functions .............. */
+
+ /**
+ * call a callback for all currently opened browser windows
+ * (might miss the most recent one)
+ * @param aFunc
+ * Callback each window is passed to
+ */
+ _forEachBrowserWindow: function sss_forEachBrowserWindow(aFunc) {
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+
+ while (windowsEnum.hasMoreElements()) {
+ var window = windowsEnum.getNext();
+ if (!window.closed && window.__SSi) {
+ aFunc.call(this, window);
+ }
+ }
+ },
+
+ /**
+ * Returns most recent window
+ * @returns Window reference
+ */
+ _getMostRecentBrowserWindow: function sss_getMostRecentBrowserWindow() {
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!win)
+ return null;
+ if (!win.closed)
+ return win;
+
+ let broken_wm_z_order =
+ AppConstants.platform != "macosx" && AppConstants.platform != "win";
+
+ if (broken_wm_z_order) {
+ win = null;
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ // this is oldest to newest, so this gets a bit ugly
+ while (windowsEnum.hasMoreElements()) {
+ let nextWin = windowsEnum.getNext();
+ if (!nextWin.closed)
+ win = nextWin;
+ }
+ return win;
+ }
+
+ var windowsEnum =
+ Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
+
+ while (windowsEnum.hasMoreElements()) {
+ win = windowsEnum.getNext();
+ if (!win.closed)
+ return win;
+ }
+
+ return null;
+ },
+
+ /**
+ * Calls onClose for windows that are determined to be closed but aren't
+ * destroyed yet, which would otherwise cause getBrowserState and
+ * setBrowserState to treat them as open windows.
+ */
+ _handleClosedWindows: function sss_handleClosedWindows() {
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+
+ while (windowsEnum.hasMoreElements()) {
+ var window = windowsEnum.getNext();
+ if (window.closed) {
+ this.onClose(window);
+ }
+ }
+ },
+
+ /**
+ * open a new browser window for a given session state
+ * called when restoring a multi-window session
+ * @param aState
+ * Object containing session data
+ */
+ _openWindowWithState: function sss_openWindowWithState(aState) {
+ var argString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ argString.data = "about:blank";
+
+ var features = "chrome,dialog=no,suppressanimation,all";
+ var winState = aState.windows[0];
+ for (var aAttr in WINDOW_ATTRIBUTES) {
+ // Use !isNaN as an easy way to ignore sizemode and check for numbers
+ if (aAttr in winState && !isNaN(winState[aAttr]))
+ features += "," + WINDOW_ATTRIBUTES[aAttr] + "=" + winState[aAttr];
+ }
+
+ var window =
+ Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
+ "_blank", features, argString);
+
+ do {
+ var ID = "window" + Math.random();
+ } while (ID in this._statesToRestore);
+ this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
+
+ return window;
+ },
+
+ /**
+ * Gets the tab for the given browser. This should be marginally better
+ * than using tabbrowser's getTabForContentWindow. This assumes the browser
+ * is the linkedBrowser of a tab, not a dangling browser.
+ *
+ * @param aBrowser
+ * The browser from which to get the tab.
+ */
+ _getTabForBrowser: function sss_getTabForBrowser(aBrowser) {
+ let windowTabs = aBrowser.ownerDocument.defaultView.getBrowser().tabs;
+ for (let i = 0; i < windowTabs.length; i++) {
+ let tab = windowTabs[i];
+ if (tab.linkedBrowser == aBrowser)
+ return tab;
+ }
+ },
+
+ /**
+ * Whether or not to resume session, if not recovering from a crash.
+ * @returns bool
+ */
+ _doResumeSession: function sss_doResumeSession() {
+ return this._prefBranch.getIntPref("startup.page") == 3 ||
+ this._prefBranch.getBoolPref("sessionstore.resume_session_once");
+ },
+
+ /**
+ * Are we restarting to switch profile.
+ * @returns bool
+ */
+ _isSwitchingProfile: function sss_isSwitchingProfile() {
+ var env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ return env.exists("XRE_PROFILE_NAME");
+ },
+
+ /**
+ * whether the user wants to load any other page at startup
+ * (except the homepage) - needed for determining whether to overwrite the current tabs
+ * C.f.: nsBrowserContentHandler's defaultArgs implementation.
+ * @returns bool
+ */
+ _isCmdLineEmpty: function sss_isCmdLineEmpty(aWindow) {
+ return "arguments" in aWindow && aWindow.arguments.length &&
+ aWindow.arguments[0] == "about:blank";
+ },
+
+ /**
+ * don't save sensitive data if the user doesn't want to
+ * (distinguishes between encrypted and non-encrypted sites)
+ * @param aIsHTTPS
+ * Bool is encrypted
+ * @param aUseDefaultPref
+ * don't do normal check for deferred
+ * @returns bool
+ */
+ _checkPrivacyLevel: function sss_checkPrivacyLevel(aIsHTTPS, aUseDefaultPref) {
+ let pref = "sessionstore.privacy_level";
+ // If we're in the process of quitting and we're not autoresuming the session
+ // then we should treat it as a deferred session. We have a different privacy
+ // pref for that case.
+ if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession())
+ pref = "sessionstore.privacy_level_deferred";
+ return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
+ },
+
+ /**
+ * on popup windows, the XULWindow's attributes seem not to be set correctly
+ * we use thus JSDOMWindow attributes for sizemode and normal window attributes
+ * (and hope for reasonable values when maximized/minimized - since then
+ * outerWidth/outerHeight aren't the dimensions of the restored window)
+ * @param aWindow
+ * Window reference
+ * @param aAttribute
+ * String sizemode | width | height | other window attribute
+ * @returns string
+ */
+ _getWindowDimension: function sss_getWindowDimension(aWindow, aAttribute) {
+ var dimension = aWindow[WINDOW_ATTRIBUTES[aAttribute]];
+ if (aAttribute == "sizemode") {
+ switch (dimension) {
+ case aWindow.STATE_MAXIMIZED:
+ return "maximized";
+ case aWindow.STATE_MINIMIZED:
+ return "minimized";
+ default:
+ return "normal";
+ }
+ }
+
+ if (aWindow.windowState == aWindow.STATE_NORMAL) {
+ return dimension;
+ }
+ return aWindow.document.documentElement.getAttribute(aAttribute) || dimension;
+ },
+
+ /**
+ * Get nsIURI from string
+ * @param string
+ * @returns nsIURI
+ */
+ _getURIFromString: function sss_getURIFromString(aString) {
+ return Services.io.newURI(aString);
+ },
+
+ /**
+ * Annotate a breakpad crash report with the currently selected tab's URL.
+ */
+ _updateCrashReportURL: function sss_updateCrashReportURL(aWindow) {
+
+ // If the crash reporter isn't built, we bail out.
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ try {
+ var currentURI = aWindow.getBrowser().currentURI.clone();
+ // if the current URI contains a username/password, remove it
+ try {
+ currentURI.userPass = "";
+ }
+ catch (ex) { } // ignore failures on about: URIs
+
+ Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsICrashReporter)
+ .annotateCrashReport("URL", currentURI.spec);
+ }
+ catch (ex) {
+ // don't make noise when crashreporter is built but not enabled
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED)
+ debug(ex);
+ }
+ },
+
+ /**
+ * @param aState is a session state
+ * @param aRecentCrashes is the number of consecutive crashes
+ * @returns whether a restore page will be needed for the session state
+ */
+ _needsRestorePage: function sss_needsRestorePage(aState, aRecentCrashes) {
+ const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
+
+ // don't display the page when there's nothing to restore
+ if (!aState.windows || !aState.windows.length)
+ return false;
+
+ // don't wrap a single about:sessionrestore page
+ let winData = aState.windows;
+ if (winData.length == 1 && winData[0].tabs &&
+ winData[0].tabs.length == 1 && winData[0].tabs[0].entries &&
+ winData[0].tabs[0].entries.length == 1 &&
+ winData[0].tabs[0].entries[0].url == "about:sessionrestore")
+ return false;
+
+ // don't automatically restore in Safe Mode
+ if (Services.appinfo.inSafeMode)
+ return true;
+
+ let max_resumed_crashes =
+ this._prefBranch.getIntPref("sessionstore.max_resumed_crashes");
+ let sessionAge = aState.session && aState.session.lastUpdate &&
+ (Date.now() - aState.session.lastUpdate);
+
+ return max_resumed_crashes != -1 &&
+ (aRecentCrashes > max_resumed_crashes ||
+ sessionAge && sessionAge >= SIX_HOURS_IN_MS);
+ },
+
+ /**
+ * Determine if the tab state we're passed is something we should save. This
+ * is used when closing a tab or closing a window with a single tab
+ *
+ * @param aTabState
+ * The current tab state
+ * @returns boolean
+ */
+ _shouldSaveTabState: function sss__shouldSaveTabState(aTabState) {
+ // If the tab has only the transient about:blank history entry, no other
+ // session history, and no userTypedValue, then we don't actually want to
+ // store this tab's data.
+ return aTabState.entries.length &&
+ !(aTabState.entries.length == 1 &&
+ aTabState.entries[0].url == "about:blank" &&
+ !aTabState.userTypedValue);
+ },
+
+ /**
+ * This is going to take a state as provided at startup (via
+ * nsISessionStartup.state) and split it into 2 parts. The first part
+ * (defaultState) will be a state that should still be restored at startup,
+ * while the second part (state) is a state that should be saved for later.
+ * defaultState will be comprised of windows with only pinned tabs, extracted
+ * from state. It will contain the cookies that go along with the history
+ * entries in those tabs. It will also contain window position information.
+ *
+ * defaultState will be restored at startup. state will be placed into
+ * this._lastSessionState and will be kept in case the user explicitly wants
+ * to restore the previous session (publicly exposed as restoreLastSession).
+ *
+ * @param state
+ * The state, presumably from nsISessionStartup.state
+ * @returns [defaultState, state]
+ */
+ _prepDataForDeferredRestore: function sss__prepDataForDeferredRestore(state) {
+ let defaultState = { windows: [], selectedWindow: 1 };
+
+ state.selectedWindow = state.selectedWindow || 1;
+
+ // Look at each window, remove pinned tabs, adjust selectedindex,
+ // remove window if necessary.
+ for (let wIndex = 0; wIndex < state.windows.length;) {
+ let window = state.windows[wIndex];
+ window.selected = window.selected || 1;
+ // We're going to put the state of the window into this object
+ let pinnedWindowState = { tabs: [], cookies: []};
+ for (let tIndex = 0; tIndex < window.tabs.length;) {
+ if (window.tabs[tIndex].pinned) {
+ // Adjust window.selected
+ if (tIndex + 1 < window.selected)
+ window.selected -= 1;
+ else if (tIndex + 1 == window.selected)
+ pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
+ // + 2 because the tab isn't actually in the array yet
+
+ // Now add the pinned tab to our window
+ pinnedWindowState.tabs =
+ pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
+ // We don't want to increment tIndex here.
+ continue;
+ }
+ tIndex++;
+ }
+
+ // At this point the window in the state object has been modified (or not)
+ // We want to build the rest of this new window object if we have pinnedTabs.
+ if (pinnedWindowState.tabs.length) {
+ // First get the other attributes off the window
+ WINDOW_ATTRIBUTES.forEach(function(attr) {
+ if (attr in window) {
+ pinnedWindowState[attr] = window[attr];
+ delete window[attr];
+ }
+ });
+ // We're just copying position data into the pinned window.
+ // Not copying over:
+ // - _closedTabs
+ // - extData
+ // - isPopup
+ // - hidden
+
+ // Assign a unique ID to correlate the window to be opened with the
+ // remaining data
+ window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
+ = "" + Date.now() + Math.random();
+
+ // Extract the cookies that belong with each pinned tab
+ this._splitCookiesFromWindow(window, pinnedWindowState);
+
+ // Actually add this window to our defaultState
+ defaultState.windows.push(pinnedWindowState);
+ // Remove the window from the state if it doesn't have any tabs
+ if (!window.tabs.length) {
+ if (wIndex + 1 <= state.selectedWindow)
+ state.selectedWindow -= 1;
+ else if (wIndex + 1 == state.selectedWindow)
+ defaultState.selectedIndex = defaultState.windows.length + 1;
+
+ state.windows.splice(wIndex, 1);
+ // We don't want to increment wIndex here.
+ continue;
+ }
+
+
+ }
+ wIndex++;
+ }
+
+ return [defaultState, state];
+ },
+
+ /**
+ * Splits out the cookies from aWinState into aTargetWinState based on the
+ * tabs that are in aTargetWinState.
+ * This alters the state of aWinState and aTargetWinState.
+ */
+ _splitCookiesFromWindow:
+ function sss_splitCookiesFromWindow(aWinState, aTargetWinState) {
+ if (!aWinState.cookies || !aWinState.cookies.length)
+ return;
+
+ // Get the hosts for history entries in aTargetWinState
+ let cookieHosts = {};
+ aTargetWinState.tabs.forEach(function(tab) {
+ tab.entries.forEach(function(entry) {
+ this._extractHostsForCookiesFromEntry(entry, cookieHosts, false);
+ }, this);
+ }, this);
+
+ // By creating a regex we reduce overhead and there is only one loop pass
+ // through either array (cookieHosts and aWinState.cookies).
+ let hosts = Object.keys(cookieHosts).join("|").replace(/\./g, "\\.");
+ let cookieRegex = new RegExp(".*(" + hosts + ")");
+ for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
+ if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
+ aTargetWinState.cookies =
+ aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
+ continue;
+ }
+ cIndex++;
+ }
+ },
+
+ /**
+ * Converts a JavaScript object into a JSON string
+ * (see http://www.json.org/ for more information).
+ *
+ * The inverse operation consists of JSON.parse(JSON_string).
+ *
+ * @param aJSObject is the object to be converted
+ * @returns the object's JSON representation
+ */
+ _toJSONString: function sss_toJSONString(aJSObject) {
+ return JSON.stringify(aJSObject);
+ },
+
+ _sendRestoreCompletedNotifications: function sss_sendRestoreCompletedNotifications() {
+ // not all windows restored, yet
+ if (this._restoreCount > 1) {
+ this._restoreCount--;
+ return;
+ }
+
+ // observers were already notified
+ if (this._restoreCount == -1)
+ return;
+
+ Services.tm.mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
+
+ this._restoreCount = -1;
+ },
+
+ run: function sss_run() {
+ // This was the last window restored at startup, notify observers.
+ Services.obs.notifyObservers(this.windowToFocus,
+ this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED);
+ this._browserSetState = false;
+ },
+
+ /**
+ * Dispatch an SSWindowState_____ event for the given window.
+ * @param aWindow the window
+ * @param aType the type of event, SSWindowState will be prepended to this string
+ */
+ _sendWindowStateEvent: function sss_sendWindowStateEvent(aWindow, aType) {
+ let event = aWindow.document.createEvent("Events");
+ event.initEvent("SSWindowState" + aType, true, false);
+ aWindow.dispatchEvent(event);
+ },
+
+ /**
+ * Dispatch the SSTabRestored event for the given tab.
+ * @param aTab the which has been restored
+ */
+ _sendTabRestoredNotification: function sss_sendTabRestoredNotification(aTab) {
+ let event = aTab.ownerDocument.createEvent("Events");
+ event.initEvent("SSTabRestored", true, false);
+ aTab.dispatchEvent(event);
+ },
+
+ /**
+ * @param aWindow
+ * Window reference
+ * @returns whether this window's data is still cached in _statesToRestore
+ * because it's not fully loaded yet
+ */
+ _isWindowLoaded: function sss_isWindowLoaded(aWindow) {
+ return !aWindow.__SS_restoreID;
+ },
+
+ /**
+ * Replace "Loading..." with the tab label (with minimal side-effects)
+ * @param aString is the string the title is stored in
+ * @param aTabbrowser is a tabbrowser object, containing aTab
+ * @param aTab is the tab whose title we're updating & using
+ *
+ * @returns aString that has been updated with the new title
+ */
+ _replaceLoadingTitle : function sss_replaceLoadingTitle(aString, aTabbrowser, aTab) {
+ if (aString == aTabbrowser.mStringBundle.getString("tabs.loading")) {
+ aTabbrowser.setTabTitle(aTab);
+ [aString, aTab.label] = [aTab.label, aString];
+ }
+ return aString;
+ },
+
+ /**
+ * Resize this._closedWindows to the value of the pref, except in the case
+ * where we don't have any non-popup windows on Windows and Linux. Then we must
+ * resize such that we have at least one non-popup window.
+ */
+ _capClosedWindows : function sss_capClosedWindows() {
+ if (this._closedWindows.length <= this._max_windows_undo)
+ return;
+ let spliceTo = this._max_windows_undo;
+ if (AppConstants.platform != "macosx") {
+ let normalWindowIndex = 0;
+ // try to find a non-popup window in this._closedWindows
+ while (normalWindowIndex < this._closedWindows.length &&
+ this._closedWindows[normalWindowIndex].isPopup)
+ normalWindowIndex++;
+ if (normalWindowIndex >= this._max_windows_undo)
+ spliceTo = normalWindowIndex + 1;
+ }
+
+ this._closedWindows.splice(spliceTo, this._closedWindows.length);
+ },
+
+ /**
+ * Reset state to prepare for a new session state to be restored.
+ */
+ _resetRestoringState: function sss_initRestoringState() {
+ this._tabsToRestore = { visible: [], hidden: [] };
+ this._tabsRestoringCount = 0;
+ },
+
+ /**
+ * Reset the restoring state for a particular tab. This will be called when
+ * removing a tab or when a tab needs to be reset (it's being overwritten).
+ *
+ * @param aTab
+ * The tab that will be "reset"
+ */
+ _resetTabRestoringState: function sss_resetTabRestoringState(aTab) {
+ let window = aTab.ownerDocument.defaultView;
+ let browser = aTab.linkedBrowser;
+
+ // Keep the tab's previous state for later in this method
+ let previousState = browser.__SS_restoreState;
+
+ // The browser is no longer in any sort of restoring state.
+ delete browser.__SS_restoreState;
+
+ // We want to decrement window.__SS_tabsToRestore here so that we always
+ // decrement it AFTER a tab is done restoring or when a tab gets "reset".
+ window.__SS_tabsToRestore--;
+
+ // Remove the progress listener if we should.
+ this._removeTabsProgressListener(window);
+
+ if (previousState == TAB_STATE_RESTORING) {
+ if (this._tabsRestoringCount)
+ this._tabsRestoringCount--;
+ }
+ else if (previousState == TAB_STATE_NEEDS_RESTORE) {
+ // Make sure the session history listener is removed. This is normally
+ // done in restoreTab, but this tab is being removed before that gets called.
+ this._removeSHistoryListener(aTab);
+
+ // Make sure that the tab is removed from the list of tabs to restore.
+ // Again, this is normally done in restoreTab, but that isn't being called
+ // for this tab.
+ this._removeTabFromTabsToRestore(aTab);
+ }
+ },
+
+ /**
+ * Remove the tab from this._tabsToRestore[visible/hidden]
+ *
+ * @param aTab
+ */
+ _removeTabFromTabsToRestore: function sss_removeTabFromTabsToRestore(aTab) {
+ let arr = this._tabsToRestore[aTab.hidden ? "hidden" : "visible"];
+ let index = arr.indexOf(aTab);
+ if (index > -1)
+ arr.splice(index, 1);
+ },
+
+ /**
+ * Add the tabs progress listener to the window if it isn't already
+ *
+ * @param aWindow
+ * The window to add our progress listener to
+ */
+ _ensureTabsProgressListener: function sss_ensureTabsProgressListener(aWindow) {
+ let tabbrowser = aWindow.getBrowser();
+ try {
+ tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener);
+ } catch (ex) { }
+ },
+
+ /**
+ * Attempt to remove the tabs progress listener from the window.
+ *
+ * @param aWindow
+ * The window from which to remove our progress listener from
+ */
+ _removeTabsProgressListener: function sss_removeTabsProgressListener(aWindow) {
+ // If there are no tabs left to restore (or restoring) in this window, then
+ // we can safely remove the progress listener from this window.
+ if (!aWindow.__SS_tabsToRestore)
+ try {
+ aWindow.getBrowser().removeTabsProgressListener(gRestoreTabsProgressListener);
+ } catch (ex) { }
+ },
+
+ /**
+ * Remove the session history listener from the tab's browser if there is one.
+ *
+ * @param aTab
+ * The tab who's browser to remove the listener
+ */
+ _removeSHistoryListener: function sss_removeSHistoryListener(aTab) {
+ let browser = aTab.linkedBrowser;
+ if (browser.__SS_shistoryListener) {
+ browser.webNavigation.sessionHistory.
+ removeSHistoryListener(browser.__SS_shistoryListener);
+ delete browser.__SS_shistoryListener;
+ }
+ },
+
+/* ........ Storage API .............. */
+
+ /**
+ * write file to disk
+ * @param aFile
+ * nsIFile
+ * @param aData
+ * String data
+ */
+ _writeFile: function sss_writeFile(aFile, aData) {
+ // Initialize the file output stream.
+ var ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ ostream.init(aFile, 0x02 | 0x08 | 0x20, parseInt("0600", 8), ostream.DEFER_OPEN);
+
+ // Obtain a converter to convert our data to a UTF-8 encoded input stream.
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ // Asynchronously copy the data to the file.
+ var istream = converter.convertToInputStream(aData);
+ var ObserverService = this._observerService;
+ NetUtil.asyncCopy(istream, ostream, function(rc) {
+ if (Components.isSuccessCode(rc)) {
+ Services.obs.notifyObservers(null,
+ "sessionstore-state-write-complete");
+ }
+ });
+ }
+};
+
+// A map storing a closed window's state data until it goes aways (is GC'ed).
+// This ensures that API clients can still read (but not write) states of
+// windows they still hold a reference to but we don't.
+var DyingWindowCache = {
+ _data: new WeakMap(),
+
+ has: function (window) {
+ return this._data.has(window);
+ },
+
+ get: function (window) {
+ return this._data.get(window);
+ },
+
+ set: function (window, data) {
+ this._data.set(window, data);
+ },
+
+ remove: function (window) {
+ this._data.delete(window);
+ }
+};
+
+// A weak set of dirty windows. We use it to determine which windows we need to
+// recollect data for when getCurrentState() is called.
+var DirtyWindows = {
+ _data: new WeakMap(),
+
+ has: function (window) {
+ return this._data.has(window);
+ },
+
+ add: function (window) {
+ return this._data.set(window, true);
+ },
+
+ remove: function (window) {
+ this._data.delete(window);
+ },
+
+ clear: function (window) {
+ this._data = new WeakMap();
+ }
+};
+
+// This is used to help meter the number of restoring tabs. This is the control
+// point for telling the next tab to restore. It gets attached to each gBrowser
+// via gBrowser.addTabsProgressListener
+var gRestoreTabsProgressListener = {
+ ss: null,
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Ignore state changes on browsers that we've already restored and state
+ // changes that aren't applicable.
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ // We need to reset the tab before starting the next restore.
+ let tab = this.ss._getTabForBrowser(aBrowser);
+ this.ss._resetTabRestoringState(tab);
+ this.ss.restoreNextTab();
+ }
+ }
+}
+
+// A SessionStoreSHistoryListener will be attached to each browser before it is
+// restored. We need to catch reloads that occur before the tab is restored
+// because otherwise, docShell will reload an old URI (usually about:blank).
+function SessionStoreSHistoryListener(ss, aTab) {
+ this.tab = aTab;
+ this.ss = ss;
+}
+
+SessionStoreSHistoryListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference]),
+ browser: null,
+ ss: null,
+ tab: null,
+ OnHistoryNewEntry: function(aNewURI) { },
+ OnHistoryGotoIndex: function(aIndex, aGotoURI) { },
+ OnHistoryPurge: function(aNumEntries) { },
+ OnHistoryReload: function(aReloadURI, aReloadFlags) {
+ // On reload, we want to make sure that session history loads the right
+ // URI. In order to do that, we will just call restoreTab. That will remove
+ // the history listener and load the right URI.
+ this.ss.restoreTab(this.tab);
+ // Returning false will stop the load that docshell is attempting.
+ return false;
+ },
+ OnHistoryReplaceEntry: function(aIndex) { },
+}
+
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]);
diff --git a/comm/suite/components/shell/ShellService.jsm b/comm/suite/components/shell/ShellService.jsm
new file mode 100644
index 0000000000..2af3e75c6b
--- /dev/null
+++ b/comm/suite/components/shell/ShellService.jsm
@@ -0,0 +1,110 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ShellService"];
+
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Internal functionality to save and restore the docShell.allow* properties.
+ */
+var ShellServiceInternal = {
+ /**
+ * Used to determine whether or not to offer "Set as desktop background"
+ * functionality. Even if shell service is available it is not
+ * guaranteed that it is able to set the background for every desktop
+ * which is especially true for Linux with its many different desktop
+ * environments.
+ */
+ get canSetDesktopBackground() {
+ if (AppConstants.platform == "win" ||
+ AppConstants.platform == "macosx") {
+ return true;
+ }
+
+ if (AppConstants.platform == "linux") {
+ if (this.shellService) {
+ let linuxShellService = this.shellService
+ .QueryInterface(Ci.nsIGNOMEShellService);
+ return linuxShellService.canSetDesktopBackground;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Used to determine whether or not to show a "Set Default Client"
+ * query dialog. This attribute is true if the application is starting
+ * up and "shell.checkDefaultClient" is true, otherwise it is false.
+ */
+ _checkedThisSession: false,
+ get shouldCheckDefaultClient() {
+ // If we've already checked, the suite has been started and this is a
+ // new window open, and we don't want to check again.
+ if (this._checkedThisSession) {
+ return false;
+ }
+
+ return Services.prefs.getBoolPref("shell.checkDefaultClient");
+ },
+
+ set shouldCheckDefaultClient(shouldCheck) {
+ Services.prefs.setBoolPref("shell.checkDefaultClient", !!shouldCheck);
+ },
+
+ get shouldBeDefaultClientFor() {
+ return Services.prefs.getIntPref("shell.checkDefaultApps");
+ },
+
+ set shouldBeDefaultClientFor(appTypes) {
+ Services.prefs.setIntPref("shell.checkDefaultApps", appTypes);
+ },
+
+ setDefaultClient(forAllUsers, claimAllTypes, appTypes) {
+ try {
+ this.shellService.setDefaultClient(forAllUsers, claimAllTypes, appTypes);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ isDefaultClient(startupCheck, appTypes) {
+ // If this is the first window, maintain internal state that we've
+ // checked this session (so that subsequent window opens don't show the
+ // default client dialog).
+ if (startupCheck) {
+ this._checkedThisSession = true;
+ }
+ if (this.shellService) {
+ return this.shellService.isDefaultClient(startupCheck, appTypes);
+ }
+ return false;
+ }
+};
+
+XPCOMUtils.defineLazyServiceGetter(ShellServiceInternal, "shellService",
+ "@mozilla.org/suite/shell-service;1", Ci.nsIShellService);
+
+/**
+ * The external API exported by this module.
+ */
+var ShellService = new Proxy(ShellServiceInternal, {
+ get(target, name) {
+ if (name in target) {
+ return target[name];
+ }
+ if (target.shellService) {
+ return target.shellService[name];
+ }
+ Services.console.logStringMessage(`${name} not found in ShellService: ${target.shellService}`);
+ return undefined;
+ }
+});
diff --git a/comm/suite/components/shell/content/setDesktopBackground.js b/comm/suite/components/shell/content/setDesktopBackground.js
new file mode 100644
index 0000000000..efcc5734e4
--- /dev/null
+++ b/comm/suite/components/shell/content/setDesktopBackground.js
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+var gShell = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+
+var gImage, gImageName, gPosition, gPicker, gDesktop;
+
+function onLoad()
+{
+ document.getElementById("itemsBox").hidden = AppConstants.platform == "macosx";
+ gImage = window.arguments[0];
+ gImageName = window.arguments[1];
+ gPosition = document.getElementById("position");
+ gPicker = document.getElementById("picker");
+ gDesktop = document.getElementById("desktop");
+
+ sizeToContent();
+ window.innerWidth += screen.width / 2 - gDesktop.boxObject.width;
+ window.innerHeight += screen.height / 2 - gDesktop.boxObject.height;
+
+ try {
+ var color = gShell.desktopBackgroundColor;
+ color = (0xF000000 | color).toString(16).toUpperCase().replace("F", "#");
+ gDesktop.style.backgroundColor = color;
+ gPicker.color = color;
+ } catch (e) {
+ gPicker.parentNode.hidden = true;
+ }
+
+ gDesktop.style.backgroundImage = 'url("' + gImage.src + '")';
+
+ updatePosition();
+}
+
+function onApply()
+{
+ if (!gPicker.parentNode.hidden)
+ gShell.desktopBackgroundColor = parseInt(gPicker.color.substr(1), 16);
+
+ gShell.setDesktopBackground(gImage, Ci.nsIShellService[gPosition.value],
+ gImageName);
+}
+
+function updatePosition()
+{
+ gDesktop.style.backgroundPosition = "center";
+ gDesktop.style.backgroundRepeat = "no-repeat";
+ switch (gPosition.value) {
+ case "BACKGROUND_FIT":
+ gDesktop.style.backgroundSize = "contain";
+ return;
+ case "BACKGROUND_FILL":
+ gDesktop.style.backgroundSize = "cover";
+ return;
+ case "BACKGROUND_STRETCH":
+ gDesktop.style.backgroundPosition = "";
+ gDesktop.style.backgroundSize = "100% 100%";
+ return;
+ case "BACKGROUND_TILE":
+ gDesktop.style.backgroundPosition = "";
+ gDesktop.style.backgroundRepeat = "repeat";
+ }
+ gDesktop.style.backgroundSize =
+ (gImage.naturalWidth / 2) + "px " + (gImage.naturalHeight / 2) + "px";
+}
+
+function updateColor()
+{
+ gDesktop.style.backgroundColor = gPicker.color;
+}
diff --git a/comm/suite/components/shell/content/setDesktopBackground.xul b/comm/suite/components/shell/content/setDesktopBackground.xul
new file mode 100644
index 0000000000..16fb384658
--- /dev/null
+++ b/comm/suite/components/shell/content/setDesktopBackground.xul
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://communicator/locale/setDesktopBackground.dtd">
+
+<dialog id="setDesktopBackground"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ buttons="accept,extra2"
+ buttoniconaccept="close"
+ buttonlabelaccept="&close.label;"
+ buttoniconextra2="apply"
+ buttonlabelextra2="&apply.label;"
+ buttonaccesskeyextra2="&apply.accesskey;"
+ ondialogextra2="onApply();"
+ title="&setDesktopBackground.title;">
+
+ <script src="chrome://communicator/content/setDesktopBackground.js"/>
+
+ <hbox id="itemsBox" align="center">
+ <label value="&position.label;" accesskey="&position.accesskey;"/>
+ <menulist id="position"
+ value="BACKGROUND_STRETCH"
+ persist="value"
+ oncommand="updatePosition();">
+ <menupopup>
+ <menuitem value="BACKGROUND_TILE" label="&position.tile.label;"/>
+ <menuitem value="BACKGROUND_STRETCH" label="&position.stretch.label;"/>
+ <menuitem value="BACKGROUND_CENTER" label="&position.center.label;"/>
+ <menuitem value="BACKGROUND_FILL" label="&position.fill.label;"/>
+ <menuitem value="BACKGROUND_FIT" label="&position.fit.label;"/>
+ </menupopup>
+ </menulist>
+ <hbox flex="1" pack="end">
+ <label value="&picker.label;" accesskey="&picker.accesskey;"/>
+ <colorpicker id="picker" type="button" onchange="updateColor();"/>
+ </hbox>
+ </hbox>
+
+ <groupbox flex="1">
+ <caption label="&preview.caption;"/>
+ <spacer id="desktop" flex="1"/>
+ </groupbox>
+</dialog>
diff --git a/comm/suite/components/shell/jar.mn b/comm/suite/components/shell/jar.mn
new file mode 100644
index 0000000000..c9eb925472
--- /dev/null
+++ b/comm/suite/components/shell/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/setDesktopBackground.js (content/setDesktopBackground.js)
+ content/communicator/setDesktopBackground.xul (content/setDesktopBackground.xul)
diff --git a/comm/suite/components/shell/moz.build b/comm/suite/components/shell/moz.build
new file mode 100644
index 0000000000..e508876c57
--- /dev/null
+++ b/comm/suite/components/shell/moz.build
@@ -0,0 +1,49 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsIShellService.idl",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ XPIDL_SOURCES += [
+ "nsIMacShellService.idl",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ XPIDL_SOURCES += [
+ "nsIGNOMEShellService.idl",
+ ]
+
+XPIDL_MODULE = "shellservice"
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += [
+ "nsWindowsShellService.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "/other-licenses/nsis/Contrib/CityHash/cityhash",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += ["nsMacShellService.cpp"]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ SOURCES += ["nsGNOMEShellService.cpp"]
+
+if SOURCES:
+ EXTRA_COMPONENTS += [
+ "nsSetDefault.js",
+ "nsSetDefault.manifest",
+ ]
+
+EXTRA_JS_MODULES += [
+ "ShellService.jsm",
+]
+
+FINAL_LIBRARY = "suite"
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/shell/nsGNOMEShellService.cpp b/comm/suite/components/shell/nsGNOMEShellService.cpp
new file mode 100644
index 0000000000..15ea0e1131
--- /dev/null
+++ b/comm/suite/components/shell/nsGNOMEShellService.cpp
@@ -0,0 +1,463 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "mozilla/ArrayUtils.h"
+
+#include "nsCOMPtr.h"
+#include "nsGNOMEShellService.h"
+#include "nsShellService.h"
+#include "nsIServiceManager.h"
+#include "nsIFile.h"
+#include "nsIProperties.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIPrefService.h"
+#include "prenv.h"
+#include "nsString.h"
+#include "nsIGIOService.h"
+#include "nsIGSettingsService.h"
+#include "nsIStringBundle.h"
+#include "nsIOutputStream.h"
+#include "nsIProcess.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIImageLoadingContent.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/dom/Element.h"
+#if defined(MOZ_WIDGET_GTK)
+#include "nsImageToPixbuf.h"
+#endif
+#include "nsXULAppAPI.h"
+#include "gfxPlatform.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <limits.h>
+#include <stdlib.h>
+
+using namespace mozilla;
+
+struct ProtocolAssociation {
+ uint16_t app;
+ const char* protocol;
+ bool essential;
+};
+
+struct MimeTypeAssociation {
+ uint16_t app;
+ const char* mimeType;
+ const char* extensions;
+};
+
+static const ProtocolAssociation gProtocols[] = {
+ { nsIShellService::BROWSER, "http", true },
+ { nsIShellService::BROWSER, "https", true },
+ { nsIShellService::BROWSER, "ftp", false },
+ { nsIShellService::BROWSER, "chrome", false },
+ { nsIShellService::MAIL, "mailto", true },
+ { nsIShellService::NEWS, "news", true },
+ { nsIShellService::NEWS, "snews", true },
+ { nsIShellService::RSS, "feed", true }
+};
+
+static const MimeTypeAssociation gMimeTypes[] = {
+ { nsIShellService::BROWSER, "text/html", "htm html shtml" },
+ { nsIShellService::BROWSER, "application/xhtml+xml", "xhtml xht" },
+ { nsIShellService::MAIL, "message/rfc822", "eml" },
+ { nsIShellService::RSS, "application/rss+xml", "rss" }
+};
+
+#define kDesktopBGSchema "org.gnome.desktop.background"
+#define kDesktopImageGSKey "picture-uri"
+#define kDesktopOptionGSKey "picture-options"
+#define kDesktopDrawBGGSKey "draw-background"
+#define kDesktopColorGSKey "primary-color"
+
+NS_IMPL_ISUPPORTS(nsGNOMEShellService, nsIGNOMEShellService, nsIShellService)
+
+nsresult
+GetBrandName(nsACString& aBrandName)
+{
+ // get the product brand name from localized strings
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService(do_GetService("@mozilla.org/intl/stringbundle;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle(BRAND_PROPERTIES, getter_AddRefs(brandBundle));
+ NS_ENSURE_TRUE(brandBundle, rv);
+
+ nsAutoString brandName;
+ rv = brandBundle->GetStringFromName("brandShortName", brandName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CopyUTF16toUTF8(brandName, aBrandName);
+ return rv;
+}
+
+nsresult
+nsGNOMEShellService::Init()
+{
+ nsresult rv;
+
+ if (gfxPlatform::IsHeadless()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Check G_BROKEN_FILENAMES. If it's set, then filenames in glib use
+ // the locale encoding. If it's not set, they use UTF-8.
+ mUseLocaleFilenames = PR_GetEnv("G_BROKEN_FILENAMES") != nullptr;
+
+ if (GetAppPathFromLauncher()) return NS_OK;
+
+ nsCOMPtr<nsIFile> appPath;
+ rv = NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE, getter_AddRefs(appPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return appPath->GetNativePath(mAppPath);
+}
+
+bool nsGNOMEShellService::GetAppPathFromLauncher() {
+ gchar *tmp;
+
+ const char* launcher = PR_GetEnv("MOZ_APP_LAUNCHER");
+ if (!launcher) return false;
+
+ if (g_path_is_absolute(launcher)) {
+ mAppPath = launcher;
+ tmp = g_path_get_basename(launcher);
+ gchar* fullpath = g_find_program_in_path(tmp);
+ if (fullpath && mAppPath.Equals(fullpath)) mAppIsInPath = true;
+ g_free(fullpath);
+ } else {
+ tmp = g_find_program_in_path(launcher);
+ if (!tmp) return false;
+ mAppPath = tmp;
+ mAppIsInPath = true;
+ }
+
+ g_free(tmp);
+ return true;
+}
+
+bool
+nsGNOMEShellService::CheckHandlerMatchesAppName(const nsACString &handler) const
+{
+ gint argc;
+ gchar** argv;
+ nsAutoCString command(handler);
+
+ // The string will be something of the form: [/path/to/]application "%s"
+ // We want to remove all of the parameters and get just the binary name.
+
+ if (g_shell_parse_argv(command.get(), &argc, &argv, nullptr) && argc > 0) {
+ command.Assign(argv[0]);
+ g_strfreev(argv);
+ }
+
+ gchar *commandPath;
+ if (mUseLocaleFilenames) {
+ gchar *nativePath =
+ g_filename_from_utf8(command.get(), -1, nullptr, nullptr, nullptr);
+ if (!nativePath) {
+ NS_ERROR("Error converting path to filesystem encoding");
+ return false;
+ }
+
+ commandPath = g_find_program_in_path(nativePath);
+ g_free(nativePath);
+ } else {
+ commandPath = g_find_program_in_path(command.get());
+ }
+
+ if (!commandPath) return false;
+
+ bool matches = mAppPath.Equals(commandPath);
+ g_free(commandPath);
+ return matches;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::IsDefaultClient(bool aStartupCheck, uint16_t aApps,
+ bool* aIsDefaultClient)
+{
+ *aIsDefaultClient = false;
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ nsAutoCString handler;
+ nsCOMPtr<nsIGIOMimeApp> gioApp;
+
+ for (unsigned int i = 0; i < ArrayLength(gProtocols); i++) {
+ if (aApps & gProtocols[i].app) {
+ if (!gProtocols[i].essential) continue;
+
+ if (giovfs) {
+ handler.Truncate();
+ nsCOMPtr<nsIHandlerApp> handlerApp;
+ nsDependentCString protocol(gProtocols[i].protocol);
+ giovfs->GetAppForURIScheme(protocol, getter_AddRefs(handlerApp));
+ gioApp = do_QueryInterface(handlerApp);
+ if (!gioApp)
+ return NS_OK;
+
+ if (NS_SUCCEEDED(gioApp->GetCommand(handler)) &&
+ !CheckHandlerMatchesAppName(handler))
+ return NS_OK;
+ }
+ }
+ }
+
+ *aIsDefaultClient = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDefaultClient(bool aForAllUsers,
+ bool aClaimAllTypes, uint16_t aApps)
+{
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (giovfs) {
+ nsresult rv;
+ nsCString brandName;
+ rv = GetBrandName(brandName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIGIOMimeApp> appInfo;
+ rv = giovfs->FindAppFromCommand(mAppPath, getter_AddRefs(appInfo));
+ if (NS_FAILED(rv)) {
+ // Application was not found in the list of installed applications
+ // provided by OS. Fallback to create appInfo from command and name.
+ rv = giovfs->CreateAppFromCommand(mAppPath, brandName,
+ getter_AddRefs(appInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // set handler for the protocols
+ for (unsigned int i = 0; i < ArrayLength(gProtocols); ++i) {
+ if (aApps & gProtocols[i].app) {
+ if (appInfo && (gProtocols[i].essential || aClaimAllTypes)) {
+ nsDependentCString protocol(gProtocols[i].protocol);
+ appInfo->SetAsDefaultForURIScheme(protocol);
+ }
+ }
+ }
+
+ if (aClaimAllTypes) {
+ for (unsigned int i = 0; i < ArrayLength(gMimeTypes); i++) {
+ if (aApps & gMimeTypes[i].app) {
+ nsDependentCString type(gMimeTypes[i].mimeType);
+ appInfo->SetAsDefaultForMimeType(type);
+ nsDependentCString extensions(gMimeTypes[i].extensions);
+ appInfo->SetAsDefaultForFileExtensions(extensions);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::GetCanSetDesktopBackground(bool* aResult)
+{
+ // for Gnome or desktops using the same GSettings keys
+ const char *currentDesktop = getenv("XDG_CURRENT_DESKTOP");
+ if (currentDesktop && strstr(currentDesktop, "GNOME") != nullptr) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ const char *gnomeSession = getenv("GNOME_DESKTOP_SESSION_ID");
+ if (gnomeSession) {
+ *aResult = true;
+ } else {
+ *aResult = false;
+ }
+
+ return NS_OK;
+}
+
+static nsresult WriteImage(const nsCString &aPath, imgIContainer *aImage) {
+#if !defined(MOZ_WIDGET_GTK)
+ return NS_ERROR_NOT_AVAILABLE;
+#else
+ RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(aImage);
+ if (!pixbuf) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ gboolean res = gdk_pixbuf_save(pixbuf, aPath.get(), "png", nullptr, nullptr);
+ return res ? NS_OK : NS_ERROR_FAILURE;
+#endif
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDesktopBackground(dom::Element* aElement,
+ int32_t aPosition,
+ const nsACString& aImageName)
+{
+ nsresult rv;
+ nsCOMPtr<nsIImageLoadingContent> imageContent =
+ do_QueryInterface(aElement, &rv);
+ if (!imageContent) return rv;
+
+ // Get the image container.
+ nsCOMPtr<imgIRequest> request;
+ rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(request));
+ if (!request) return rv;
+ nsCOMPtr<imgIContainer> container;
+ rv = request->GetImage(getter_AddRefs(container));
+ if (!container) return rv;
+
+ // Set desktop wallpaper filling style.
+ nsAutoCString options;
+ switch (aPosition) {
+ case BACKGROUND_TILE:
+ options.AssignLiteral("wallpaper");
+ break;
+ case BACKGROUND_STRETCH:
+ options.AssignLiteral("stretched");
+ break;
+ case BACKGROUND_FILL:
+ options.AssignLiteral("zoom");
+ break;
+ case BACKGROUND_FIT:
+ options.AssignLiteral("scaled");
+ break;
+ default:
+ options.AssignLiteral("centered");
+ break;
+ }
+
+ // Write the background file to the home directory.
+ nsCString filePath(PR_GetEnv("HOME"));
+
+ nsCString brandName;
+ rv = GetBrandName(brandName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build the file name.
+ filePath.Append('/');
+ filePath.Append(brandName);
+ filePath.AppendLiteral("_wallpaper.png");
+
+ // Write the image to a file in the home dir.
+ rv = WriteImage(filePath, container);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (gsettings) {
+ nsCOMPtr<nsIGSettingsCollection> background_settings;
+ gsettings->GetCollectionForSchema(nsLiteralCString(kDesktopBGSchema),
+ getter_AddRefs(background_settings));
+ if (background_settings) {
+ gchar *file_uri = g_filename_to_uri(filePath.get(), nullptr, nullptr);
+ if (!file_uri) return NS_ERROR_FAILURE;
+
+ background_settings->SetString(nsLiteralCString(kDesktopOptionGSKey),
+ options);
+ background_settings->SetString(nsLiteralCString(kDesktopImageGSKey),
+ nsDependentCString(file_uri));
+ g_free(file_uri);
+ background_settings->SetBoolean(nsLiteralCString(kDesktopDrawBGGSKey),
+ true);
+ return rv;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+#define COLOR_16_TO_8_BIT(_c) ((_c) >> 8)
+#define COLOR_8_TO_16_BIT(_c) ((_c) << 8 | (_c))
+
+NS_IMETHODIMP
+nsGNOMEShellService::GetDesktopBackgroundColor(uint32_t *aColor)
+{
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ nsCOMPtr<nsIGSettingsCollection> background_settings;
+ nsAutoCString background;
+
+ if (gsettings) {
+ gsettings->GetCollectionForSchema(nsLiteralCString(kDesktopBGSchema),
+ getter_AddRefs(background_settings));
+ if (background_settings) {
+ background_settings->GetString(nsLiteralCString(kDesktopColorGSKey),
+ background);
+ }
+ }
+
+ if (background.IsEmpty()) {
+ *aColor = 0;
+ return NS_OK;
+ }
+
+ GdkColor color;
+ NS_ENSURE_TRUE(gdk_color_parse(background.get(), &color), NS_ERROR_FAILURE);
+
+ *aColor = COLOR_16_TO_8_BIT(color.red) << 16 |
+ COLOR_16_TO_8_BIT(color.green) << 8 |
+ COLOR_16_TO_8_BIT(color.blue);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDesktopBackgroundColor(uint32_t aColor)
+{
+ NS_ENSURE_ARG_MAX(aColor, 0xFFFFFF);
+
+ uint16_t red = COLOR_8_TO_16_BIT((aColor >> 16) & 0xff);
+ uint16_t green = COLOR_8_TO_16_BIT((aColor >> 8) & 0xff);
+ uint16_t blue = COLOR_8_TO_16_BIT(aColor & 0xff);
+ char colorString[14];
+ sprintf(colorString, "#%04x%04x%04x", red, green, blue);
+
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (gsettings) {
+ nsCOMPtr<nsIGSettingsCollection> background_settings;
+ gsettings->GetCollectionForSchema(nsLiteralCString(kDesktopBGSchema),
+ getter_AddRefs(background_settings));
+ if (background_settings) {
+ background_settings->SetString(nsLiteralCString(kDesktopColorGSKey),
+ nsDependentCString(colorString));
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::OpenApplicationWithURI(nsIFile* aApplication, const nsACString& aURI)
+{
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process =
+ do_CreateInstance("@mozilla.org/process/util;1", &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ rv = process->Init(aApplication);
+ if (NS_FAILED(rv))
+ return rv;
+
+ const nsCString& spec = PromiseFlatCString(aURI);
+ const char* specStr = spec.get();
+ return process->Run(false, &specStr, 1);
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::GetDefaultFeedReader(nsIFile** _retval)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/suite/components/shell/nsGNOMEShellService.h b/comm/suite/components/shell/nsGNOMEShellService.h
new file mode 100644
index 0000000000..558663a2fe
--- /dev/null
+++ b/comm/suite/components/shell/nsGNOMEShellService.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsgnomeshellservice_h____
+#define nsgnomeshellservice_h____
+
+#include "nsIGNOMEShellService.h"
+#include "nsString.h"
+#include "mozilla/Attributes.h"
+#include "nsSuiteCID.h"
+
+class nsGNOMEShellService final : public nsIGNOMEShellService
+{
+public:
+ nsGNOMEShellService() : mAppIsInPath(false) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+ NS_DECL_NSIGNOMESHELLSERVICE
+
+ nsresult Init();
+
+private:
+ ~nsGNOMEShellService() {}
+
+ bool CheckHandlerMatchesAppName(const nsACString& handler) const;
+
+ bool GetAppPathFromLauncher();
+ bool mUseLocaleFilenames;
+ nsCString mAppPath;
+ bool mAppIsInPath;
+};
+
+#endif // nsgnomeshellservice_h____
+
diff --git a/comm/suite/components/shell/nsIGNOMEShellService.idl b/comm/suite/components/shell/nsIGNOMEShellService.idl
new file mode 100644
index 0000000000..64bf823c45
--- /dev/null
+++ b/comm/suite/components/shell/nsIGNOMEShellService.idl
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIShellService.idl"
+
+[scriptable, uuid(26ea117f-f1f1-4d53-8581-1322fcafa0d4)]
+interface nsIGNOMEShellService : nsIShellService
+{
+ /**
+ * Used to determine whether or not to offer "Set as desktop background"
+ * functionality. Even if shell service is available it is not
+ * guaranteed that it is able to set the background for every desktop
+ * which is especially true for Linux with its many different desktop
+ * environments.
+ */
+ readonly attribute boolean canSetDesktopBackground;
+};
diff --git a/comm/suite/components/shell/nsIMacShellService.idl b/comm/suite/components/shell/nsIMacShellService.idl
new file mode 100644
index 0000000000..a8e16c98a1
--- /dev/null
+++ b/comm/suite/components/shell/nsIMacShellService.idl
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIShellService.idl"
+
+[scriptable, uuid(66f4a5cf-8807-43dd-8e65-9954f3c34cf2)]
+interface nsIMacShellService : nsIShellService
+{
+ const long APPLICATION_KEYCHAIN_ACCESS = 2;
+ const long APPLICATION_NETWORK = 3;
+ const long APPLICATION_DESKTOP = 4;
+};
diff --git a/comm/suite/components/shell/nsIShellService.idl b/comm/suite/components/shell/nsIShellService.idl
new file mode 100644
index 0000000000..9320fc6afb
--- /dev/null
+++ b/comm/suite/components/shell/nsIShellService.idl
@@ -0,0 +1,98 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+webidl Element;
+
+[scriptable, uuid(d7a19d24-9c98-4f88-b11e-52fa8c39ceea)]
+interface nsIShellService : nsISupports
+{
+ /**
+ * app types we can be registered to handle
+ */
+ const unsigned short BROWSER = 0x0001;
+ const unsigned short MAIL = 0x0002;
+ const unsigned short NEWS = 0x0004;
+ const unsigned short RSS = 0x0008;
+
+ /**
+ * Determines whether or not SeaMonkey is the "Default Client" for the
+ * passed in app type.
+ *
+ * This is simply whether or not SeaMonkey is registered to handle
+ * the url schemes associated with the app.
+ *
+ * @param aStartupCheck true if this is the check being performed
+ * by the first window at startup,
+ * false otherwise.
+ * @param aApps the application types being tested (Browser, Mail, News, RSS)
+ */
+ boolean isDefaultClient(in boolean aStartupCheck, in unsigned short aApps);
+
+ /**
+ * Registers SeaMonkey as the "Default Client" for the
+ * passed in app types.
+ *
+ * @param aForAllUsers Whether or not SeaMonkey should attempt
+ * to become the default client for all
+ * users on a multi-user system.
+ * @param aClaimAllTypes Register SeaMonkey as the handler for
+ * additional protocols (ftp, chrome etc)
+ * and web documents (.html, .xhtml etc).
+ * @param aApps the application types being tested (Mail, News, Browser, RSS)
+ */
+ void setDefaultClient(in boolean aForAllUsers, in boolean aClaimAllTypes, in unsigned short aApps);
+
+ /**
+ * Sets the desktop background image using either the HTML <IMG>
+ * element supplied or the background image of the element supplied.
+ *
+ * @param aImageElement Either a HTML <IMG> element or an element with
+ * a background image from which to source the
+ * background image.
+ * @param aPosition How to place the image on the desktop
+ * @param aImageName The image name. Equivalent to the leaf name of the
+ * location.href.
+ */
+
+ void setDesktopBackground(in Element aElement,
+ in long aPosition,
+ in ACString aImageName);
+
+ /**
+ * Flags for positioning/sizing of the Desktop Background image.
+ */
+ const long BACKGROUND_TILE = 1;
+ const long BACKGROUND_STRETCH = 2;
+ const long BACKGROUND_CENTER = 3;
+ const long BACKGROUND_FILL = 4;
+ const long BACKGROUND_FIT = 5;
+
+ /**
+ * The desktop background color, visible when no background image is
+ * used, or if the background image is centered and does not fill the
+ * entire screen. An RGB value (r << 16 | g << 8 | b)
+ */
+ attribute unsigned long desktopBackgroundColor;
+
+ /**
+ * Opens an application with a specific URI to load.
+ * @param application
+ * The application file (or bundle directory, on OS X)
+ * @param uri
+ * The uri to be loaded by the application
+ */
+ void openApplicationWithURI(in nsIFile aApplication, in ACString aURI);
+
+ /**
+ * The default system handler for web feeds
+ */
+ readonly attribute nsIFile defaultFeedReader;
+};
+
diff --git a/comm/suite/components/shell/nsMacShellService.cpp b/comm/suite/components/shell/nsMacShellService.cpp
new file mode 100644
index 0000000000..6aa8aa3088
--- /dev/null
+++ b/comm/suite/components/shell/nsMacShellService.cpp
@@ -0,0 +1,398 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDirectoryServiceDefs.h"
+#include "nsIImageLoadingContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIContent.h"
+#include "nsICookieJarSettings.h"
+#include "nsILocalFileMac.h"
+#include "nsIObserverService.h"
+#include "nsIPrefService.h"
+#include "nsIServiceManager.h"
+#include "nsIStringBundle.h"
+#include "nsIURL.h"
+#include "nsIWebBrowserPersist.h"
+#include "nsMacShellService.h"
+#include "nsIProperties.h"
+#include "nsServiceManagerUtils.h"
+#include "nsShellService.h"
+#include "nsString.h"
+#include "nsIDocShell.h"
+#include "nsILoadContext.h"
+#include "nsIPrefService.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ReferrerInfo.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+
+#define SAFARI_BUNDLE_IDENTIFIER "com.apple.Safari"
+
+using mozilla::dom::Element;
+
+NS_IMPL_ISUPPORTS(nsMacShellService, nsIShellService, nsIWebProgressListener)
+
+NS_IMETHODIMP
+nsMacShellService::IsDefaultClient(bool aStartupCheck, uint16_t aApps, bool *aIsDefaultClient)
+{
+ *aIsDefaultClient = false;
+
+ if (aApps & nsIShellService::BROWSER)
+ if(!isDefaultHandlerForProtocol(CFSTR("http")))
+ return NS_OK;
+ if (aApps & nsIShellService::MAIL)
+ if(!isDefaultHandlerForProtocol(CFSTR("mailto")))
+ return NS_OK;
+ if (aApps & nsIShellService::NEWS)
+ if(!isDefaultHandlerForProtocol(CFSTR("news")))
+ return NS_OK;
+ if (aApps & nsIShellService::RSS)
+ if(!isDefaultHandlerForProtocol(CFSTR("feed")))
+ return NS_OK;
+
+ *aIsDefaultClient = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDefaultClient(bool aForAllUsers,
+ bool aClaimAllTypes, uint16_t aApps)
+{
+ // Note: We don't support aForAllUsers on macOS.
+
+ CFStringRef suiteID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
+ if (!suiteID)
+ return NS_ERROR_FAILURE;
+
+ if (aApps & nsIShellService::BROWSER)
+ {
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("http"), suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("https"), suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ if (::LSSetDefaultRoleHandlerForContentType(kUTTypeHTML, kLSRolesAll, suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ if (::LSSetDefaultRoleHandlerForContentType(CFSTR("public.xhtml"), kLSRolesAll, suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aApps & nsIShellService::MAIL)
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("mailto"), suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ if (aApps & nsIShellService::NEWS)
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("news"), suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+ if (aApps & nsIShellService::RSS)
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("feed"), suiteID) != noErr)
+ return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+bool
+nsMacShellService::isDefaultHandlerForProtocol(CFStringRef aScheme)
+{
+ bool isDefault = false;
+
+ CFStringRef suiteID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
+ if (!suiteID)
+ {
+ // CFBundleGetIdentifier is expected to return nullptr only if the specified
+ // bundle doesn't have a bundle identifier in its dictionary. In this case,
+ // that means a failure, since our bundle does have an identifier.
+ return isDefault;
+ }
+
+ // Get the default handler's bundle ID for the scheme.
+ CFStringRef defaultHandlerID = ::LSCopyDefaultHandlerForURLScheme(aScheme);
+ if (defaultHandlerID)
+ {
+ // The handler ID in LaunchServices is in all lower case, but the bundle
+ // identifier could have upper case characters. So we're using
+ // CFStringCompare with the kCFCompareCaseInsensitive option here.
+ isDefault = ::CFStringCompare(suiteID, defaultHandlerID,
+ kCFCompareCaseInsensitive) == kCFCompareEqualTo;
+ ::CFRelease(defaultHandlerID);
+ }
+
+ return isDefault;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDesktopBackground(Element* aElement,
+ int32_t aPosition,
+ const nsACString& aImageName)
+{
+ // Note: We don't support aPosition on OS X.
+
+ // Get the image URI:
+ nsresult rv;
+ nsCOMPtr<nsIImageLoadingContent> imageContent = do_QueryInterface(aElement, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> imageURI;
+ rv = imageContent->GetCurrentURI(getter_AddRefs(imageURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIURI *docURI = aElement->OwnerDoc()->GetDocumentURI();
+ if (!docURI)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIProperties> fileLocator
+ (do_GetService("@mozilla.org/file/directory_service;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current user's "Pictures" folder (That's ~/Pictures):
+ fileLocator->Get(NS_OSX_PICTURE_DOCUMENTS_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(mBackgroundFile));
+ if (!mBackgroundFile)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ nsAutoString fileNameUnicode;
+ CopyUTF8toUTF16(aImageName, fileNameUnicode);
+
+ // and add the image file name itself:
+ mBackgroundFile->Append(fileNameUnicode);
+
+ // Download the image; the desktop background will be set in OnStateChange():
+ nsCOMPtr<nsIWebBrowserPersist> wbp
+ (do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t flags = nsIWebBrowserPersist::PERSIST_FLAGS_NO_CONVERSION |
+ nsIWebBrowserPersist::PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWebBrowserPersist::PERSIST_FLAGS_FROM_CACHE;
+
+ wbp->SetPersistFlags(flags);
+ wbp->SetProgressListener(this);
+
+ nsCOMPtr<nsILoadContext> loadContext;
+ nsCOMPtr<nsISupports> container = aElement->OwnerDoc()->GetContainer();
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(container);
+ if (docShell)
+ {
+ loadContext = do_QueryInterface(docShell);
+ }
+
+ auto referrerInfo = mozilla::MakeRefPtr<mozilla::dom::ReferrerInfo>(*aElement);
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ aElement->OwnerDoc()->CookieJarSettings();
+ return wbp->SaveURI(imageURI, aElement->NodePrincipal(), 0, referrerInfo,
+ cookieJarSettings, nullptr, nullptr, mBackgroundFile,
+ nsIContentPolicy::TYPE_IMAGE, loadContext);
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI* aLocation,
+ uint32_t aFlags)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aState)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus)
+{
+ if (aStateFlags & STATE_STOP)
+ {
+ bool exists = false;
+ mBackgroundFile->Exists(&exists);
+ if (!exists)
+ return NS_OK;
+
+ nsAutoCString nativePath;
+ mBackgroundFile->GetNativePath(nativePath);
+
+ AEDesc tAEDesc = { typeNull, nil };
+ OSErr err = noErr;
+ AliasHandle aliasHandle = nil;
+ FSRef pictureRef;
+ OSStatus status;
+
+ // Convert the path into a FSRef:
+ status = ::FSPathMakeRef((const UInt8*)nativePath.get(), &pictureRef,
+ nullptr);
+ if (status == noErr)
+ {
+ err = ::FSNewAlias(nil, &pictureRef, &aliasHandle);
+ if (err == noErr && aliasHandle == nil)
+ err = paramErr;
+
+ if (err == noErr)
+ {
+ // We need the descriptor (based on the picture file reference)
+ // for the 'Set Desktop Picture' apple event.
+ char handleState = ::HGetState((Handle)aliasHandle);
+ ::HLock((Handle)aliasHandle);
+ err = ::AECreateDesc(typeAlias, *aliasHandle,
+ GetHandleSize((Handle)aliasHandle), &tAEDesc);
+ // Unlock the alias handler:
+ ::HSetState((Handle)aliasHandle, handleState);
+ ::DisposeHandle((Handle)aliasHandle);
+ }
+ if (err == noErr)
+ {
+ AppleEvent tAppleEvent;
+ OSType sig = 'MACS';
+ AEBuildError tAEBuildError;
+ // Create a 'Set Desktop Picture' Apple Event:
+ err = ::AEBuildAppleEvent(kAECoreSuite, kAESetData, typeApplSignature,
+ &sig, sizeof(OSType), kAutoGenerateReturnID,
+ kAnyTransactionID, &tAppleEvent, &tAEBuildError,
+ "'----':'obj '{want:type (prop),form:prop" \
+ ",seld:type('dpic'),from:'null'()},data:(@)",
+ &tAEDesc);
+ if (err == noErr)
+ {
+ AppleEvent reply = { typeNull, nil };
+ // Send the event we built, the reply event isn't necessary:
+ err = ::AESend(&tAppleEvent, &reply, kAENoReply, kAENormalPriority,
+ kNoTimeOut, nil, nil);
+ ::AEDisposeDesc(&tAppleEvent);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::GetDesktopBackgroundColor(uint32_t *aColor)
+{
+ // This method and |SetDesktopBackgroundColor| has no meaning on macOS.
+ // The mac desktop preferences UI uses pictures for the few solid colors it
+ // supports.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDesktopBackgroundColor(uint32_t aColor)
+{
+ // This method and |GetDesktopBackgroundColor| has no meaning on macOS.
+ // The mac desktop preferences UI uses pictures for the few solid colors it
+ // supports.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OpenApplicationWithURI(nsIFile* aApplication, const nsACString& aURI)
+{
+ nsCOMPtr<nsILocalFileMac> lfm(do_QueryInterface(aApplication));
+ CFURLRef appURL;
+ nsresult rv = lfm->GetCFURL(&appURL);
+ if (NS_FAILED(rv))
+ return rv;
+
+ const nsCString& spec = PromiseFlatCString(aURI);
+ const UInt8* uriString = (const UInt8*)spec.get();
+ CFURLRef uri = ::CFURLCreateWithBytes(nullptr, uriString, aURI.Length(),
+ kCFStringEncodingUTF8, nullptr);
+ if (!uri)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ CFArrayRef uris = ::CFArrayCreate(nullptr, (const void**)&uri, 1, nullptr);
+ if (!uris)
+ {
+ ::CFRelease(uri);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ LSLaunchURLSpec launchSpec;
+ launchSpec.appURL = appURL;
+ launchSpec.itemURLs = uris;
+ launchSpec.passThruParams = nullptr;
+ launchSpec.launchFlags = kLSLaunchDefaults;
+ launchSpec.asyncRefCon = nullptr;
+
+ OSErr err = ::LSOpenFromURLSpec(&launchSpec, nullptr);
+
+ ::CFRelease(uris);
+ ::CFRelease(uri);
+
+ return err != noErr ? NS_ERROR_FAILURE : NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::GetDefaultFeedReader(nsIFile** _retval)
+{
+ nsresult rv = NS_ERROR_FAILURE;
+ *_retval = nullptr;
+
+ CFStringRef defaultHandlerID = ::LSCopyDefaultHandlerForURLScheme(CFSTR("feed"));
+ if (!defaultHandlerID)
+ {
+ defaultHandlerID = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ SAFARI_BUNDLE_IDENTIFIER,
+ kCFStringEncodingASCII);
+ }
+
+ CFURLRef defaultHandlerURL = nullptr;
+ OSStatus status = ::LSFindApplicationForInfo(kLSUnknownCreator,
+ defaultHandlerID,
+ nullptr, // inName
+ nullptr, // outAppRef
+ &defaultHandlerURL);
+
+ if (status == noErr && defaultHandlerURL)
+ {
+ nsCOMPtr<nsILocalFileMac> defaultReader =
+ do_CreateInstance("@mozilla.org/file/local;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ {
+ rv = defaultReader->InitWithCFURL(defaultHandlerURL);
+ if (NS_SUCCEEDED(rv))
+ {
+ NS_ADDREF(*_retval = defaultReader);
+ }
+ }
+
+ ::CFRelease(defaultHandlerURL);
+ }
+
+ ::CFRelease(defaultHandlerID);
+
+ return rv;
+}
diff --git a/comm/suite/components/shell/nsMacShellService.h b/comm/suite/components/shell/nsMacShellService.h
new file mode 100644
index 0000000000..4fa92513c2
--- /dev/null
+++ b/comm/suite/components/shell/nsMacShellService.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsmacshellservice_h____
+#define nsmacshellservice_h____
+
+#include "nsIMacShellService.h"
+#include "nsIWebProgressListener.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "mozilla/Attributes.h"
+#include "nsSuiteCID.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+
+class nsMacShellService final : public nsIShellService,
+ public nsIWebProgressListener
+{
+public:
+ nsMacShellService() {};
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+protected:
+ ~nsMacShellService() {}
+ bool isDefaultHandlerForProtocol(CFStringRef aScheme);
+
+private:
+ nsCOMPtr<nsIFile> mBackgroundFile;
+};
+
+#endif
diff --git a/comm/suite/components/shell/nsSetDefault.js b/comm/suite/components/shell/nsSetDefault.js
new file mode 100644
index 0000000000..4d35dc4531
--- /dev/null
+++ b/comm/suite/components/shell/nsSetDefault.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This component handles the startup command line arguments of the form:
+ * -setDefaultBrowser
+ * -setDefaultMail
+ * -setDefaultNews
+ * -setDefaultFeed
+ */
+
+const nsICommandLineHandler = Ci.nsICommandLineHandler;
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function nsSetDefault() {
+}
+
+nsSetDefault.prototype = {
+ handle: function nsSetDefault_handle(aCmdline) {
+ if (aCmdline.handleFlag("setDefaultBrowser", false)) {
+ var shell = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ shell.setDefaultClient(true, true, Ci.nsIShellService.BROWSER);
+ }
+ else if (aCmdline.handleFlag("setDefaultMail", false)) {
+ var shell = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ shell.setDefaultClient(true, true, Ci.nsIShellService.MAIL);
+ }
+ else if (aCmdline.handleFlag("setDefaultNews", false)) {
+ var shell = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ shell.setDefaultClient(true, true, Ci.nsIShellService.NEWS);
+ }
+ else if (aCmdline.handleFlag("setDefaultFeed", false)) {
+ var shell = Cc["@mozilla.org/suite/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ shell.setDefaultClient(true, true, Ci.nsIShellService.RSS);
+ }
+ },
+
+ helpInfo: " -setDefaultBrowser Set this app as the default browser client.\n" +
+ " -setDefaultMail Set this app as the default mail client.\n" +
+ " -setDefaultNews Set this app as the default newsreader.\n" +
+ " -setDefaultFeed Set this app as the default feedreader.\n",
+
+ classID: Components.ID("{a3d5b950-690a-491f-a881-2c2cdcd241cb}"),
+ QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler])
+}
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSetDefault]);
+
diff --git a/comm/suite/components/shell/nsSetDefault.manifest b/comm/suite/components/shell/nsSetDefault.manifest
new file mode 100644
index 0000000000..7d4a585bc7
--- /dev/null
+++ b/comm/suite/components/shell/nsSetDefault.manifest
@@ -0,0 +1,3 @@
+component {a3d5b950-690a-491f-a881-2c2cdcd241cb} nsSetDefault.js
+contract @mozilla.org/suite/default-browser-clh;1 {a3d5b950-690a-491f-a881-2c2cdcd241cb}
+category command-line-handler m-setdefault @mozilla.org/suite/default-browser-clh;1
diff --git a/comm/suite/components/shell/nsShellService.h b/comm/suite/components/shell/nsShellService.h
new file mode 100644
index 0000000000..f5275a6556
--- /dev/null
+++ b/comm/suite/components/shell/nsShellService.h
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIShellService.h"
+
+#define PREF_CHECKDEFAULTCLIENT "shell.checkDefaultClient"
+
+#define SHELLSERVICE_PROPERTIES "chrome://communicator/locale/shellservice.properties"
+#define BRAND_PROPERTIES "chrome://branding/locale/brand.properties"
diff --git a/comm/suite/components/shell/nsWindowsShellService.cpp b/comm/suite/components/shell/nsWindowsShellService.cpp
new file mode 100644
index 0000000000..7cdf1cbd88
--- /dev/null
+++ b/comm/suite/components/shell/nsWindowsShellService.cpp
@@ -0,0 +1,793 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsWindowsShellService.h"
+
+#include "imgIContainer.h"
+#include "imgIRequest.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/RefPtr.h"
+#include "nsIContent.h"
+#include "nsIImageLoadingContent.h"
+#include "nsIOutputStream.h"
+#include "nsIPrefService.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIServiceManager.h"
+#include "nsIStringBundle.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsShellService.h"
+#include "nsIProcess.h"
+#include "nsICategoryManager.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIWindowsRegKey.h"
+#include "nsUnicharUtils.h"
+#include "nsIURLFormatter.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/dom/Element.h"
+
+#include "windows.h"
+#include "shellapi.h"
+
+#ifdef _WIN32_WINNT
+#undef _WIN32_WINNT
+#endif
+#define _WIN32_WINNT 0x0600
+#define INITGUID
+#include <shlobj.h>
+
+#ifndef MAX_BUF
+#define MAX_BUF 4096
+#endif
+
+#define REG_SUCCEEDED(val) \
+ (val == ERROR_SUCCESS)
+
+#define REG_FAILED(val) \
+ (val != ERROR_SUCCESS)
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+NS_IMPL_ISUPPORTS(nsWindowsShellService, nsIShellService)
+
+static nsresult
+OpenKeyForReading(HKEY aKeyRoot, const wchar_t* aKeyName, HKEY* aKey)
+{
+ DWORD res = ::RegOpenKeyExW(aKeyRoot, aKeyName, 0, KEY_READ, aKey);
+ switch (res) {
+ case ERROR_SUCCESS:
+ break;
+ case ERROR_ACCESS_DENIED:
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ case ERROR_FILE_NOT_FOUND:
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Default SeaMonkey OS integration Registry Settings
+// Note: Some settings only exist when using the installer!
+// The setting of SeaMonkey as default application is made by a helper
+// application since writing those values may require elevation.
+//
+// Default Browser settings:
+// - File Extension Mappings
+// -----------------------
+// The following file extensions:
+// .htm .html .shtml .xht .xhtml
+// are mapped like so:
+//
+// HKCU\SOFTWARE\Classes\.<ext>\ (default) REG_SZ SeaMonkeyHTML
+//
+// as aliases to the class:
+//
+// HKCU\SOFTWARE\Classes\SeaMonkeyHTML\
+// DefaultIcon (default) REG_SZ <appfolder>\chrome\icons\default\html-file.ico
+// shell\open\command (default) REG_SZ <apppath> -url "%1"
+//
+// - Windows Vista Protocol Handler
+//
+// HKCU\SOFTWARE\Classes\SeaMonkeyURL\(default) REG_SZ <appname> URL
+// EditFlags REG_DWORD 2
+// FriendlyTypeName REG_SZ <appname> URL
+// DefaultIcon (default) REG_SZ <apppath>,1
+// shell\open\command (default) REG_SZ <apppath> -requestPending -osint -url "%1"
+// shell\open\ddeexec (default) REG_SZ "%1",,0,0,,,,
+// shell\open\ddeexec NoActivateHandler REG_SZ
+// \Application (default) REG_SZ SeaMonkey
+// \Topic (default) REG_SZ WWW_OpenURL
+//
+// - Protocol Mappings
+// -----------------
+// The following protocols:
+// HTTP, HTTPS, FTP
+// are mapped like so:
+//
+// HKCU\SOFTWARE\Classes\<protocol>\
+// DefaultIcon (default) REG_SZ <apppath>,0
+// shell\open\command (default) REG_SZ <apppath> -requestPending -osint -url "%1"
+// shell\open\ddeexec (default) REG_SZ "%1",,0,0,,,,
+// shell\open\ddeexec NoActivateHandler REG_SZ
+// \Application (default) REG_SZ SeaMonkey
+// \Topic (default) REG_SZ WWW_OpenURL
+//
+// - Windows Start Menu (Win2K SP2, XP SP1, and newer)
+// -------------------------------------------------
+// The following keys are set to make SeaMonkey appear in the Start Menu as the
+// browser:
+//
+// HKCU\SOFTWARE\Clients\StartMenuInternet\SEAMONKEY.EXE\
+// (default) REG_SZ <appname>
+// DefaultIcon (default) REG_SZ <apppath>,0
+// InstallInfo HideIconsCommand REG_SZ <uninstpath> /HideShortcuts
+// InstallInfo IconsVisible REG_DWORD 1
+// InstallInfo ReinstallCommand REG_SZ <uninstpath> /SetAsDefaultAppGlobal
+// InstallInfo ShowIconsCommand REG_SZ <uninstpath> /ShowShortcuts
+// shell\open\command (default) REG_SZ <apppath>
+// shell\properties (default) REG_SZ <appname> &Preferences
+// shell\properties\command (default) REG_SZ <apppath> -preferences
+// shell\safemode (default) REG_SZ <appname> &Safe Mode
+// shell\safemode\command (default) REG_SZ <apppath> -safe-mode
+//
+//
+//
+// Default Mail&News settings
+//
+// - File Extension Mappings
+// -----------------------
+// The following file extension:
+// .eml
+// is mapped like this:
+//
+// HKCU\SOFTWARE\Classes\.eml (default) REG_SZ SeaMonkeyEML
+//
+// That aliases to this class:
+// HKCU\SOFTWARE\Classes\SeaMonkeyEML\ (default) REG_SZ SeaMonkey (Mail) Document
+// FriendlyTypeName REG_SZ SeaMonkey (Mail) Document
+// DefaultIcon (default) REG_SZ <appfolder>\chrome\icons\default\html-file.ico
+// shell\open\command (default) REG_SZ <apppath> "%1"
+//
+// - Windows Vista Protocol Handler
+//
+// HKCU\SOFTWARE\Classes\SeaMonkeyCOMPOSE (default) REG_SZ SeaMonkey (Mail) URL
+// DefaultIcon REG_SZ <apppath>,0
+// EditFlags REG_DWORD 2
+// shell\open\command (default) REG_SZ <apppath> -osint -compose "%1"
+//
+// HKCU\SOFTWARE\Classes\SeaMonkeyNEWS (default) REG_SZ SeaMonkey (News) URL
+// DefaultIcon REG_SZ <apppath>,0
+// EditFlags REG_DWORD 2
+// shell\open\command (default) REG_SZ <apppath> -osint -news "%1"
+//
+//
+// - Protocol Mappings
+// -----------------
+// The following protocol:
+// mailto
+// is mapped like this:
+//
+// HKCU\SOFTWARE\Classes\mailto\ (default) REG_SZ SeaMonkey (Mail) URL
+// EditFlags REG_DWORD 2
+// URL Protocol REG_SZ
+// DefaultIcon (default) REG_SZ <apppath>,0
+// shell\open\command (default) REG_SZ <apppath> -osint -compose "%1"
+//
+// The following protocols:
+// news,nntp,snews
+// are mapped like this:
+//
+// HKCU\SOFTWARE\Classes\<protocol>\ (default) REG_SZ SeaMonkey (News) URL
+// EditFlags REG_DWORD 2
+// URL Protocol REG_SZ
+// DefaultIcon (default) REG_SZ <appath>,0
+// shell\open\command (default) REG_SZ <appath> -osint -news "%1"
+//
+// - Windows Start Menu (Win2K SP2, XP SP1, and newer)
+// -------------------------------------------------
+// The following keys are set to make SeaMonkey appear in the Start Menu as
+// the default mail program:
+//
+// HKCU\SOFTWARE\Clients\Mail\SeaMonkey
+// (default) REG_SZ <appname>
+// DLLPath REG_SZ <appfolder>\mozMapi32.dll
+// DefaultIcon (default) REG_SZ <apppath>,0
+// InstallInfo HideIconsCommand REG_SZ <uninstpath> /HideShortcuts
+// InstallInfo ReinstallCommand REG_SZ <uninstpath> /SetAsDefaultAppGlobal
+// InstallInfo ShowIconsCommand REG_SZ <uninstpath> /ShowShortcuts
+// shell\open\command (default) REG_SZ <apppath> -mail
+// shell\properties (default) REG_SZ <appname> &Preferences
+// shell\properties\command (default) REG_SZ <apppath> -preferences
+//
+// Also set SeaMonkey as News reader (Usenet), though Windows does currently
+// not expose a default news reader to UI. Applications like Outlook
+// also add themselves to this registry key
+//
+// HKCU\SOFTWARE\Clients\News\SeaMonkey
+// (default) REG_SZ <appname>
+// DLLPath REG_SZ <appfolder>\mozMapi32.dll
+// DefaultIcon (default) REG_SZ <apppath>,0
+// shell\open\command (default) REG_SZ <apppath> -news
+//
+///////////////////////////////////////////////////////////////////////////////
+
+
+typedef enum {
+ NO_SUBSTITUTION = 0x00,
+ APP_PATH_SUBSTITUTION = 0x01
+} SettingFlags;
+
+#define APP_REG_NAME L"SeaMonkey"
+// APP_REG_NAME_MAIL and APP_REG_NAME_NEWS should be kept in synch with
+// AppRegNameMail and AppRegNameNews in the installer file: defines.nsi.in
+#define APP_REG_NAME_MAIL L"SeaMonkey (Mail)"
+#define APP_REG_NAME_NEWS L"SeaMonkey (News)"
+#define CLS_HTML "SeaMonkeyHTML"
+#define CLS_URL "SeaMonkeyURL"
+#define CLS_EML "SeaMonkeyEML"
+#define CLS_MAILTOURL "SeaMonkeyCOMPOSE"
+#define CLS_NEWSURL "SeaMonkeyNEWS"
+#define CLS_FEEDURL "SeaMonkeyFEED"
+#define SMI "SOFTWARE\\Clients\\StartMenuInternet\\"
+#define DI "\\DefaultIcon"
+#define II "\\InstallInfo"
+#define SOP "\\shell\\open\\command"
+
+#define VAL_ICON "%APPPATH%,0"
+#define VAL_HTML_OPEN "\"%APPPATH%\" -url \"%1\""
+#define VAL_URL_OPEN "\"%APPPATH%\" -requestPending -osint -url \"%1\""
+#define VAL_MAIL_OPEN "\"%APPPATH%\" \"%1\""
+
+#define MAKE_KEY_NAME1(PREFIX, MID) \
+ PREFIX MID
+
+// The DefaultIcon registry key value should never be used (e.g. NON_ESSENTIAL)
+// when checking if SeaMonkey is the default browser since other applications
+// (e.g. MS Office) may modify the DefaultIcon registry key value to add Icon
+// Handlers.
+// see http://msdn2.microsoft.com/en-us/library/aa969357.aspx for more info.
+static SETTING gBrowserSettings[] = {
+ // File Extension Class - as of 1.8.1.2 the value for VAL_URL_OPEN is also
+ // checked for CLS_HTML since SeaMonkey should also own opening local files
+ // when set as the default browser.
+ { MAKE_KEY_NAME1(CLS_HTML, SOP), "", VAL_HTML_OPEN, APP_PATH_SUBSTITUTION },
+
+ // Protocol Handler Class - for Vista and above
+ { MAKE_KEY_NAME1(CLS_URL, SOP), "", VAL_URL_OPEN, APP_PATH_SUBSTITUTION },
+
+ // Protocol Handlers
+ { MAKE_KEY_NAME1("HTTP", DI), "", VAL_ICON, APP_PATH_SUBSTITUTION },
+ { MAKE_KEY_NAME1("HTTP", SOP), "", VAL_URL_OPEN, APP_PATH_SUBSTITUTION },
+ { MAKE_KEY_NAME1("HTTPS", DI), "", VAL_ICON, APP_PATH_SUBSTITUTION },
+ { MAKE_KEY_NAME1("HTTPS", SOP), "", VAL_URL_OPEN, APP_PATH_SUBSTITUTION }
+
+ // These values must be set by hand, since they contain localized strings.
+ // seamonkey.exe\shell\properties (default) REG_SZ SeaMonkey &Preferences
+ // seamonkey.exe\shell\safemode (default) REG_SZ SeaMonkey &Safe Mode
+};
+
+ static SETTING gMailSettings[] = {
+ // File Extension Aliases
+ { ".eml", "", CLS_EML, NO_SUBSTITUTION },
+ // File Extension Class
+ { MAKE_KEY_NAME1(CLS_EML, SOP), "", VAL_MAIL_OPEN, APP_PATH_SUBSTITUTION},
+
+ // Protocol Handler Class - for Vista and above
+ { MAKE_KEY_NAME1(CLS_MAILTOURL, SOP), "", "\"%APPPATH%\" -osint -compose \"%1\"", APP_PATH_SUBSTITUTION },
+
+ // Protocol Handlers
+ { MAKE_KEY_NAME1("mailto", SOP), "", "\"%APPPATH%\" -osint -compose \"%1\"", APP_PATH_SUBSTITUTION }
+ };
+
+ static SETTING gNewsSettings[] = {
+ // Protocol Handler Class - for Vista and above
+ { MAKE_KEY_NAME1(CLS_NEWSURL, SOP), "", "\"%APPPATH%\" -osint -mail \"%1\"", APP_PATH_SUBSTITUTION },
+
+ // Protocol Handlers
+ { MAKE_KEY_NAME1("news", SOP), "", "\"%APPPATH%\" -osint -mail \"%1\"", APP_PATH_SUBSTITUTION },
+ { MAKE_KEY_NAME1("nntp", SOP), "", "\"%APPPATH%\" -osint -mail \"%1\"", APP_PATH_SUBSTITUTION },
+};
+
+ static SETTING gFeedSettings[] = {
+ // Protocol Handler Class - for Vista and above
+ { MAKE_KEY_NAME1(CLS_FEEDURL, SOP), "", "\"%APPPATH%\" -osint -mail \"%1\"", APP_PATH_SUBSTITUTION },
+
+ // Protocol Handlers
+ { MAKE_KEY_NAME1("feed", SOP), "", "\"%APPPATH%\" -osint -mail \"%1\"", APP_PATH_SUBSTITUTION },
+};
+
+nsresult
+GetHelperPath(nsString& aPath)
+{
+ nsresult rv;
+ nsCOMPtr<nsIProperties> directoryService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> appHelper;
+ rv = directoryService->Get(XRE_EXECUTABLE_FILE,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(appHelper));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->SetNativeLeafName("uninstall"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->AppendNative("helper.exe"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->GetPath(aPath);
+
+ aPath.Insert('"', 0);
+ aPath.Append('"');
+
+ return rv;
+}
+
+nsresult
+LaunchHelper(const nsString& aPath)
+{
+ STARTUPINFOW si = {sizeof(si), 0};
+ PROCESS_INFORMATION pi = {0};
+
+ BOOL ok = CreateProcessW(nullptr, (LPWSTR)aPath.get(), nullptr, nullptr,
+ FALSE, 0, nullptr, nullptr, &si, &pi);
+
+ if (!ok)
+ return NS_ERROR_FAILURE;
+
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ return NS_OK;
+}
+
+/* helper routine. Iterate over the passed in settings object,
+ testing each key to see if we are handling it.
+*/
+bool
+nsWindowsShellService::TestForDefault(SETTING aSettings[], int32_t aSize)
+{
+ wchar_t currValue[MAX_BUF];
+ SETTING* end = aSettings + aSize;
+ for (SETTING * settings = aSettings; settings < end; ++settings) {
+ NS_ConvertUTF8toUTF16 dataLongPath(settings->valueData);
+ NS_ConvertUTF8toUTF16 dataShortPath(settings->valueData);
+ NS_ConvertUTF8toUTF16 key(settings->keyName);
+ NS_ConvertUTF8toUTF16 value(settings->valueName);
+ if (settings->flags & APP_PATH_SUBSTITUTION) {
+ int32_t offset = dataLongPath.Find(u"%APPPATH%");
+ dataLongPath.Replace(offset, 9, mAppLongPath);
+ // Remove the quotes around %APPPATH% in VAL_OPEN for short paths
+ int32_t offsetQuoted = dataShortPath.Find(u"\"%APPPATH%\"");
+ if (offsetQuoted != -1)
+ dataShortPath.Replace(offsetQuoted, 11, mAppShortPath);
+ else
+ dataShortPath.Replace(offset, 9, mAppShortPath);
+ }
+
+ ::ZeroMemory(currValue, sizeof(currValue));
+ HKEY theKey;
+ nsresult rv = OpenKeyForReading(HKEY_CLASSES_ROOT, key.get(), &theKey);
+ if (NS_FAILED(rv))
+ // Key does not exist
+ return false;
+
+ DWORD len = sizeof currValue;
+ DWORD res = ::RegQueryValueExW(theKey, value.get(),
+ nullptr, nullptr, (LPBYTE)currValue, &len);
+ // Close the key we opened.
+ ::RegCloseKey(theKey);
+ if (REG_FAILED(res) ||
+ _wcsicmp(dataLongPath.get(), currValue) &&
+ _wcsicmp(dataShortPath.get(), currValue)) {
+ // Key wasn't set, or was set to something else (something else became the default client)
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult nsWindowsShellService::Init()
+{
+ wchar_t appPath[MAX_BUF];
+ if (!::GetModuleFileNameW(0, appPath, MAX_BUF))
+ return NS_ERROR_FAILURE;
+
+ mAppLongPath.Assign(appPath);
+
+ // Support short path to the exe so if it is already set the user is not
+ // prompted to set the default mail client again.
+ if (!::GetShortPathNameW(appPath, appPath, MAX_BUF))
+ return NS_ERROR_FAILURE;
+
+ mAppShortPath.Assign(appPath);
+
+ return NS_OK;
+}
+
+bool
+nsWindowsShellService::IsDefaultClientVista(uint16_t aApps, bool* aIsDefaultClient)
+{
+ IApplicationAssociationRegistration* pAAR;
+
+ HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration,
+ nullptr,
+ CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration,
+ (void**)&pAAR);
+
+ if (SUCCEEDED(hr)) {
+ BOOL isDefaultBrowser = true;
+ BOOL isDefaultMail = true;
+ BOOL isDefaultNews = true;
+ if (aApps & nsIShellService::BROWSER)
+ pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME, &isDefaultBrowser);
+ if (aApps & nsIShellService::MAIL)
+ pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME_MAIL, &isDefaultMail);
+ if (aApps & nsIShellService::NEWS)
+ pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME_NEWS, &isDefaultNews);
+
+ *aIsDefaultClient = isDefaultBrowser && isDefaultNews && isDefaultMail;
+
+ pAAR->Release();
+ return true;
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::IsDefaultClient(bool aStartupCheck, uint16_t aApps, bool *aIsDefaultClient)
+{
+ *aIsDefaultClient = true;
+
+ // for each type, check if it is the default app
+ // browser check needs to be at the top
+ if (aApps & nsIShellService::BROWSER) {
+ *aIsDefaultClient &= TestForDefault(gBrowserSettings, sizeof(gBrowserSettings)/sizeof(SETTING));
+ // Only check if this app is default on Vista if the previous checks
+ // indicate that this app is the default.
+ if (*aIsDefaultClient)
+ IsDefaultClientVista(nsIShellService::BROWSER, aIsDefaultClient);
+ }
+ if (aApps & nsIShellService::MAIL) {
+ *aIsDefaultClient &= TestForDefault(gMailSettings, sizeof(gMailSettings)/sizeof(SETTING));
+ // Only check if this app is default on Vista if the previous checks
+ // indicate that this app is the default.
+ if (*aIsDefaultClient)
+ IsDefaultClientVista(nsIShellService::MAIL, aIsDefaultClient);
+ }
+ if (aApps & nsIShellService::NEWS) {
+ *aIsDefaultClient &= TestForDefault(gNewsSettings, sizeof(gNewsSettings)/sizeof(SETTING));
+ // Only check if this app is default on Vista if the previous checks
+ // indicate that this app is the default.
+ if (*aIsDefaultClient)
+ IsDefaultClientVista(nsIShellService::NEWS, aIsDefaultClient);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDefaultClient(bool aForAllUsers,
+ bool aClaimAllTypes, uint16_t aApps)
+{
+ nsAutoString appHelperPath;
+ if (NS_FAILED(GetHelperPath(appHelperPath)))
+ return NS_ERROR_FAILURE;
+
+ if (aForAllUsers)
+ appHelperPath.AppendLiteral(" /SetAsDefaultAppGlobal");
+ else {
+ appHelperPath.AppendLiteral(" /SetAsDefaultAppUser");
+ if (aApps & nsIShellService::BROWSER)
+ appHelperPath.AppendLiteral(" Browser");
+
+ if (aApps & nsIShellService::MAIL)
+ appHelperPath.AppendLiteral(" Mail");
+
+ if (aApps & nsIShellService::NEWS)
+ appHelperPath.AppendLiteral(" News");
+ }
+
+ return LaunchHelper(appHelperPath);
+}
+
+static nsresult
+WriteBitmap(nsIFile* aFile, imgIContainer* aImage)
+{
+ nsresult rv;
+
+ RefPtr<SourceSurface> surface =
+ aImage->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ // For either of the following formats we want to set the biBitCount member
+ // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap
+ // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored
+ // for the BI_RGB value we use for the biCompression member.
+ MOZ_ASSERT(surface->GetFormat() == SurfaceFormat::B8G8R8A8 ||
+ surface->GetFormat() == SurfaceFormat::B8G8R8X8);
+
+ RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+ int32_t bytesPerPixel = 4 * sizeof(uint8_t);
+ int32_t bytesPerRow = bytesPerPixel * width;
+
+ // initialize these bitmap structs which we will later
+ // serialize directly to the head of the bitmap file
+ BITMAPINFOHEADER bmi;
+ bmi.biSize = sizeof(BITMAPINFOHEADER);
+ bmi.biWidth = width;
+ bmi.biHeight = height;
+ bmi.biPlanes = 1;
+ bmi.biBitCount = (WORD)bytesPerPixel*8;
+ bmi.biCompression = BI_RGB;
+ bmi.biSizeImage = bytesPerRow * height;
+ bmi.biXPelsPerMeter = 0;
+ bmi.biYPelsPerMeter = 0;
+ bmi.biClrUsed = 0;
+ bmi.biClrImportant = 0;
+
+ BITMAPFILEHEADER bf;
+ bf.bfType = 0x4D42; // 'BM'
+ bf.bfReserved1 = 0;
+ bf.bfReserved2 = 0;
+ bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
+ bf.bfSize = bf.bfOffBits + bmi.biSizeImage;
+
+ // get a file output stream
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write the bitmap headers and rgb pixel data to the file
+ rv = NS_ERROR_FAILURE;
+ if (stream) {
+ uint32_t written;
+ stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written);
+ if (written == sizeof(BITMAPFILEHEADER)) {
+ stream->Write((const char*)&bmi, sizeof(BITMAPINFOHEADER), &written);
+ if (written == sizeof(BITMAPINFOHEADER)) {
+ // write out the image data backwards because the desktop won't
+ // show bitmaps with negative heights for top-to-bottom
+ uint32_t i = map.mStride * height;
+ rv = NS_OK;
+ do {
+ i -= map.mStride;
+ stream->Write(((const char*)map.mData) + i, bytesPerRow, &written);
+ if (written != bytesPerRow) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ } while (i != 0);
+ }
+ }
+
+ stream->Close();
+ }
+
+ dataSurface->Unmap();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDesktopBackground(dom::Element* aElement,
+ int32_t aPosition,
+ const nsACString& aImageName)
+{
+ if (!aElement || !aElement->IsHTMLElement(nsGkAtoms::img)) {
+ // XXX write background loading stuff!
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIImageLoadingContent> imageContent =
+ do_QueryInterface(aElement, &rv);
+ if (!imageContent)
+ return rv;
+
+ // get the image container
+ nsCOMPtr<imgIRequest> request;
+ rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(request));
+ if (!request)
+ return rv;
+
+ nsCOMPtr<imgIContainer> container;
+ rv = request->GetImage(getter_AddRefs(container));
+ if (!container)
+ return NS_ERROR_FAILURE;
+
+ // get the file name from localized strings
+ nsCOMPtr<nsIStringBundleService> bundleService(
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundle> shellBundle;
+ rv = bundleService->CreateBundle(SHELLSERVICE_PROPERTIES,
+ getter_AddRefs(shellBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // e.g. "Desktop Background.bmp"
+ nsAutoString fileLeafName;
+ rv = shellBundle->GetStringFromName("desktopBackgroundLeafNameWin",
+ fileLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the profile root directory
+ nsCOMPtr<nsIFile> file;
+ rv = NS_GetSpecialDirectory(NS_APP_APPLICATION_REGISTRY_DIR,
+ getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // eventually, the path is "%APPDATA%\Mozilla\SeaMonkey\Desktop Background.bmp"
+ rv = file->Append(fileLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString path;
+ rv = file->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the bitmap to a file in the profile directory
+ rv = WriteBitmap(file, container);
+
+ // if the file was written successfully, set it as the system wallpaper
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIWindowsRegKey> key(do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Control Panel\\Desktop"_ns,
+ nsIWindowsRegKey::ACCESS_SET_VALUE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int style = 0;
+ switch (aPosition) {
+ case BACKGROUND_STRETCH:
+ style = 2;
+ break;
+ case BACKGROUND_FILL:
+ style = 10;
+ break;
+ case BACKGROUND_FIT:
+ style = 6;
+ break;
+ }
+
+ nsString value;
+ value.AppendInt(style);
+ rv = key->WriteStringValue(u"WallpaperStyle"_ns, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ value.Assign(aPosition == BACKGROUND_TILE ? '1' : '0');
+ rv = key->WriteStringValue(u"TileWallpaper"_ns, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ::SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, (PVOID)path.get(),
+ SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::GetDesktopBackgroundColor(uint32_t* aColor)
+{
+ uint32_t color = ::GetSysColor(COLOR_DESKTOP);
+ *aColor = (GetRValue(color) << 16) | (GetGValue(color) << 8) | GetBValue(color);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDesktopBackgroundColor(uint32_t aColor)
+{
+ int parameter = COLOR_DESKTOP;
+ BYTE r = (aColor >> 16);
+ BYTE g = (aColor << 16) >> 24;
+ BYTE b = (aColor << 24) >> 24;
+ COLORREF color = RGB(r,g,b);
+
+ ::SetSysColors(1, &parameter, &color);
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> key(do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Control Panel\\Colors"_ns,
+ nsIWindowsRegKey::ACCESS_SET_VALUE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ wchar_t rgb[12];
+ _snwprintf(rgb, 12, L"%u %u %u", r, g, b);
+ rv = key->WriteStringValue(u"Background"_ns,
+ nsDependentString(rgb));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return key->Close();
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::OpenApplicationWithURI(nsIFile* aApplication,
+ const nsACString& aURI)
+{
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process =
+ do_CreateInstance("@mozilla.org/process/util;1", &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ rv = process->Init(aApplication);
+ if (NS_FAILED(rv))
+ return rv;
+
+ const nsCString& spec = PromiseFlatCString(aURI);
+ const char* specStr = spec.get();
+ return process->Run(false, &specStr, 1);
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::GetDefaultFeedReader(nsIFile** _retval)
+{
+ *_retval = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> key(do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ u"feed\\shell\\open\\command"_ns,
+ nsIWindowsRegKey::ACCESS_READ);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString path;
+ rv = key->ReadStringValue(EmptyString(), path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (path.IsEmpty())
+ return NS_ERROR_FAILURE;
+
+ if (path.First() == '"') {
+ // Everything inside the quotes
+ path = Substring(path, 1, path.FindChar('"', 1) - 1);
+ } else {
+ // Everything up to the first space
+ path = Substring(path, 0, path.FindChar(' '));
+ }
+
+ nsCOMPtr<nsIFile> defaultReader =
+ do_CreateInstance("@mozilla.org/file/local;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultReader->InitWithPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists;
+ rv = defaultReader->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists)
+ return NS_ERROR_FAILURE;
+
+ NS_ADDREF(*_retval = defaultReader);
+ return NS_OK;
+}
diff --git a/comm/suite/components/shell/nsWindowsShellService.h b/comm/suite/components/shell/nsWindowsShellService.h
new file mode 100644
index 0000000000..a29dff2e5d
--- /dev/null
+++ b/comm/suite/components/shell/nsWindowsShellService.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nscore.h"
+#include "nsShellService.h"
+#include "nsString.h"
+#include "nsIShellService.h"
+#include "mozilla/Attributes.h"
+#include "nsSuiteCID.h"
+
+#include <windows.h>
+
+typedef struct {
+ const char* keyName;
+ const char* valueName;
+ const char* valueData;
+
+ int32_t flags;
+} SETTING;
+
+class nsWindowsShellService final : public nsIShellService
+{
+public:
+ nsWindowsShellService() {};
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+
+protected:
+ ~nsWindowsShellService() {}
+ bool IsDefaultClientVista(uint16_t aApps, bool* aIsDefaultClient);
+ bool TestForDefault(SETTING aSettings[], int32_t aSize);
+
+private:
+ nsString mAppLongPath;
+ nsString mAppShortPath;
+};
+
diff --git a/comm/suite/components/sidebar/SuiteSidebar.manifest b/comm/suite/components/sidebar/SuiteSidebar.manifest
new file mode 100644
index 0000000000..3506176b04
--- /dev/null
+++ b/comm/suite/components/sidebar/SuiteSidebar.manifest
@@ -0,0 +1,4 @@
+component {22117140-9c6e-11d3-aaf1-00805f8a4905} nsSidebar.js
+contract @mozilla.org/sidebar;1 {22117140-9c6e-11d3-aaf1-00805f8a4905}
+category JavaScript-global-property sidebar @mozilla.org/sidebar;1
+category JavaScript-global-property external @mozilla.org/sidebar;1
diff --git a/comm/suite/components/sidebar/content/PageNotFound.xul b/comm/suite/components/sidebar/content/PageNotFound.xul
new file mode 100644
index 0000000000..960357f947
--- /dev/null
+++ b/comm/suite/components/sidebar/content/PageNotFound.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE page SYSTEM "chrome://communicator/locale/sidebar/sidebarOverlay.dtd">
+
+<page xmlns ="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <description class="header">&sidebar.pagenotfound.label;</description>
+</page>
+
diff --git a/comm/suite/components/sidebar/content/customize-panel.js b/comm/suite/components/sidebar/content/customize-panel.js
new file mode 100644
index 0000000000..912619108a
--- /dev/null
+++ b/comm/suite/components/sidebar/content/customize-panel.js
@@ -0,0 +1,43 @@
+/* -*- Mode: Java -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// the rdf service
+var RDF = Cc["@mozilla.org/rdf/rdf-service;"]
+ .getService(Ci.nsIRDFService);
+
+var NC = "http://home.netscape.com/NC-rdf#";
+
+var sidebarObj = new Object;
+var customizeObj = new Object;
+
+function Init()
+{
+ customizeObj.id = window.arguments[0];
+ customizeObj.url = window.arguments[1];
+ sidebarObj.datasource_uri = window.arguments[2];
+ sidebarObj.resource = window.arguments[3];
+
+ sidebarObj.datasource = RDF.GetDataSource(sidebarObj.datasource_uri);
+
+ var customize_frame = document.getElementById('customize_frame');
+ customize_frame.setAttribute('src', customizeObj.url);
+}
+
+// Use an assertion to pass a "refresh" event to all the sidebars.
+// They use observers to watch for this assertion (in sidebarOverlay.js).
+function RefreshPanel() {
+ var sb_resource = RDF.GetResource(sidebarObj.resource);
+ var refresh_resource = RDF.GetResource(NC + "refresh_panel");
+ var panel_resource = RDF.GetLiteral(customizeObj.id);
+
+ sidebarObj.datasource.Assert(sb_resource,
+ refresh_resource,
+ panel_resource,
+ true);
+ sidebarObj.datasource.Unassert(sb_resource,
+ refresh_resource,
+ panel_resource);
+}
+
diff --git a/comm/suite/components/sidebar/content/customize-panel.xul b/comm/suite/components/sidebar/content/customize-panel.xul
new file mode 100644
index 0000000000..3fcd3908f1
--- /dev/null
+++ b/comm/suite/components/sidebar/content/customize-panel.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="navigator:browser"
+ onload="Init();"
+ onunload="RefreshPanel();;">
+
+ <script src="chrome://communicator/content/sidebar/customize-panel.js" />
+
+ <browser id="customize_frame"
+ type="content"
+ primary="true"
+ src="about:blank"
+ flex="1"/>
+</window>
diff --git a/comm/suite/components/sidebar/content/customize.js b/comm/suite/components/sidebar/content/customize.js
new file mode 100644
index 0000000000..e8400e909c
--- /dev/null
+++ b/comm/suite/components/sidebar/content/customize.js
@@ -0,0 +1,692 @@
+/* -*- Mode: Java; tab-width: 4; insert-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//////////////////////////////////////////////////////////////
+// Import modules
+//////////////////////////////////////////////////////////////
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+//////////////////////////////////////////////////////////////
+// Global variables
+//////////////////////////////////////////////////////////////
+
+// Set to true for noise
+const CUST_DEBUG = false;
+
+// the rdf service
+var RDF = Cc["@mozilla.org/rdf/rdf-service;1"]
+ .getService(Ci.nsIRDFService);
+var NC = "http://home.netscape.com/NC-rdf#";
+
+var sidebarObj = new Object;
+var allPanelsObj = new Object;
+var original_panels = new Array();
+
+//////////////////////////////////////////////////////////////
+// Sidebar Init/Destroy
+//////////////////////////////////////////////////////////////
+
+function sidebar_customize_init()
+{
+ allPanelsObj.datasources = window.arguments[0];
+ allPanelsObj.resource = window.arguments[1];
+ sidebarObj.datasource_uri = window.arguments[2];
+ sidebarObj.resource = window.arguments[3];
+
+ debug("Init: all panels datasources = " + allPanelsObj.datasources);
+ debug("Init: all panels resource = " + allPanelsObj.resource);
+ debug("Init: sidebarObj.datasource_uri = " + sidebarObj.datasource_uri);
+ debug("Init: sidebarObj.resource = " + sidebarObj.resource);
+
+ var all_panels = document.getElementById('other-panels');
+ var current_panels = document.getElementById('current-panels');
+
+ debug("Adding observer to all panels database.");
+ all_panels.database.AddObserver(panels_observer);
+
+ allPanelsObj.datasources = allPanelsObj.datasources.trim().split(/\s+/);
+ for (var ii = 0; ii < allPanelsObj.datasources.length; ii++) {
+ debug("Init: Adding "+allPanelsObj.datasources[ii]);
+
+ // This will load the datasource, if it isn't already.
+ var datasource = RDF.GetDataSource(allPanelsObj.datasources[ii]);
+ all_panels.database.AddDataSource(datasource);
+ }
+
+ // Add the datasource for current list of panels. It selects panels out
+ // of the other datasources.
+ debug("Init: Adding current panels, "+sidebarObj.datasource_uri);
+ sidebarObj.datasource = RDF.GetDataSource(sidebarObj.datasource_uri);
+
+ // Root the customize dialog at the correct place.
+ debug("Init: reset all panels ref, "+allPanelsObj.resource);
+ all_panels.setAttribute('ref', allPanelsObj.resource);
+
+ // Create a "container" wrapper around the current panels to
+ // manipulate the RDF:Seq more easily.
+ var panel_list = sidebarObj.datasource.GetTarget(RDF.GetResource(sidebarObj.resource), RDF.GetResource(NC + "panel-list"), true);
+ sidebarObj.container = Cc["@mozilla.org/rdf/container;1"].createInstance(Ci.nsIRDFContainer);
+ sidebarObj.container.Init(sidebarObj.datasource, panel_list);
+
+ // Add all the current panels to the tree
+ current_panels = sidebarObj.container.GetElements();
+ while (current_panels.hasMoreElements()) {
+ var panel = current_panels.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (add_node_to_current_list(sidebarObj.datasource, panel) >= 0) {
+ original_panels.push(panel.Value);
+ original_panels[panel.Value] = true;
+ }
+ }
+
+ var links =
+ all_panels.database.GetSources(RDF.GetResource(NC + "haslink"),
+ RDF.GetLiteral("true"), true);
+
+ while (links.hasMoreElements()) {
+ var folder =
+ links.getNext().QueryInterface(Ci.nsIRDFResource);
+ var folder_name = folder.Value;
+ debug("+++ fixing up remote container " + folder_name + "\n");
+ fixup_remote_container(folder_name);
+ }
+
+ sizeToContent();
+}
+
+function sidebar_customize_destruct()
+{
+ var all_panels = document.getElementById('other-panels');
+ debug("Removing observer from all_panels database.");
+ all_panels.database.RemoveObserver(panels_observer);
+}
+
+
+//////////////////////////////////////////////////////////////////
+// Panels' RDF Datasource Observer
+//////////////////////////////////////////////////////////////////
+var panels_observer = {
+ onAssert : function(ds,src,prop,target) {
+ //debug ("observer: assert");
+ // "refresh" is asserted by select menu and by customize.js.
+ if (prop == RDF.GetResource(NC + "link")) {
+ setTimeout(fixup_remote_container, 100, src.Value);
+ }
+ },
+ onUnassert : function(ds,src,prop,target) {
+ //debug ("observer: unassert");
+ },
+ onChange : function(ds,src,prop,old_target,new_target) {
+ //debug ("observer: change");
+ },
+ onMove : function(ds,old_src,new_src,prop,target) {
+ //debug ("observer: move");
+ },
+ onBeginUpdateBatch : function(ds) {
+ //debug ("observer: onBeginUpdateBatch");
+ },
+ onEndUpdateBatch : function(ds) {
+ //debug ("observer: onEndUpdateBatch");
+ }
+};
+
+function fixup_remote_container(id)
+{
+ debug('fixup_remote_container('+id+')');
+
+ var container = document.getElementById(id);
+ if (container) {
+ container.setAttribute('container', 'true');
+ container.removeAttribute('open');
+ }
+}
+
+function fixup_children(id) {
+ // Add container="true" on nodes with "link" attribute
+ var treeitem = document.getElementById(id);
+
+ var children = treeitem.childNodes.item(1).childNodes;
+ for (var ii=0; ii < children.length; ii++) {
+ var child = children.item(ii);
+ if (child.getAttribute('link') != '' &&
+ child.getAttribute('container') != 'true') {
+ child.setAttribute('container', 'true');
+ child.removeAttribute('open');
+ }
+ }
+}
+
+function get_attr(registry,service,attr_name)
+{
+ var attr = registry.GetTarget(service,
+ RDF.GetResource(NC + attr_name),
+ true);
+ if (attr)
+ attr = attr.QueryInterface(Ci.nsIRDFLiteral);
+ if (attr)
+ attr = attr.Value;
+ return attr;
+}
+
+function SelectChangeForOtherPanels(event, target)
+{
+ enable_buttons_for_other_panels();
+}
+
+function ClickOnOtherPanels(event)
+{
+ var tree = document.getElementById("other-panels");
+
+ var rowIndex = -1;
+ if (event.type == "click" && event.button == 0) {
+ var b = tree.treeBoxObject;
+ var cell = b.getCellAt(event.clientX, event.clientY);
+
+ if (cell.childElt == "twisty" || event.detail == 2) {
+ rowIndex = cell.row;
+ }
+ }
+
+ if (rowIndex < 0) return;
+
+ var treeitem = tree.contentView.getItemAtIndex(rowIndex);
+ var res = RDF.GetResource(treeitem.id);
+
+ if (treeitem.getAttribute('container') == 'true') {
+ if (treeitem.getAttribute('open') == 'true') {
+ var link = treeitem.getAttribute('link');
+ var loaded_link = treeitem.getAttribute('loaded_link');
+ if (link != '' && !loaded_link) {
+ debug("Has remote datasource: "+link);
+ add_datasource_to_other_panels(link);
+ treeitem.setAttribute('loaded_link', 'true');
+ } else {
+ setTimeout(fixup_children, 100, treeitem.getAttribute('id'));
+ }
+ }
+ }
+
+ // Remove the selection in the "current" panels list
+ var current_panels = document.getElementById('current-panels');
+ current_panels.view.selection.clearSelection();
+ enable_buttons_for_current_panels();
+}
+
+function add_datasource_to_other_panels(link) {
+ // Convert the |link| attribute into a URL
+ var url = document.location;
+ debug("Current URL: " +url);
+ debug("Current link: " +link);
+
+ var uri = Cc['@mozilla.org/network/standard-url;1'].createInstance();
+ uri = uri.QueryInterface(Ci.nsIURI);
+ uri.spec = url;
+ uri = uri.resolve(link);
+
+ debug("New URL: " +uri);
+
+ // Add the datasource to the tree
+ var all_panels = document.getElementById('other-panels');
+ all_panels.database.AddDataSource(RDF.GetDataSource(uri));
+
+ // XXX This is a hack to force re-display
+ //all_panels.setAttribute('ref', allPanelsObj.resource);
+}
+
+// Handle a selection change in the current panels.
+function SelectChangeForCurrentPanels() {
+ // Remove the selection in the available panels list
+ var all_panels = document.getElementById('other-panels');
+ all_panels.view.selection.clearSelection();
+
+ enable_buttons_for_current_panels();
+}
+
+// Move the selected item up the the current panels list.
+function MoveUp() {
+ var tree = document.getElementById('current-panels');
+ if (tree.view.selection.count == 1) {
+ var index = tree.currentIndex;
+ var selected = tree.contentView.getItemAtIndex(index);
+ var before = selected.previousSibling;
+ if (before) {
+ selected.remove();
+ before.parentNode.insertBefore(selected, before);
+ tree.view.selection.select(index-1);
+ tree.treeBoxObject.ensureRowIsVisible(index-1);
+ }
+ }
+}
+
+// Move the selected item down the the current panels list.
+function MoveDown() {
+ var tree = document.getElementById('current-panels');
+ if (tree.view.selection.count == 1) {
+ var index = tree.currentIndex;
+ var selected = tree.contentView.getItemAtIndex(index);
+ if (selected.nextSibling) {
+ if (selected.nextSibling.nextSibling)
+ selected.parentNode.insertBefore(selected, selected.nextSibling.nextSibling);
+ else
+ selected.parentNode.appendChild(selected);
+ tree.view.selection.select(index+1);
+ tree.treeBoxObject.ensureRowIsVisible(index+1);
+ }
+ }
+}
+
+function PreviewPanel()
+{
+ var tree = document.getElementById('other-panels');
+ var database = tree.database;
+ var sel = tree.view.selection;
+ var rangeCount = sel.getRangeCount();
+ for (var range = 0; range < rangeCount; ++range) {
+ var min = {}, max = {};
+ sel.getRangeAt(range, min, max);
+ for (var index = min.value; index <= max.value; ++index) {
+ var item = tree.contentView.getItemAtIndex(index);
+ var res = RDF.GetResource(item.id);
+
+ var preview_name = get_attr(database, res, 'title');
+ var preview_URL = get_attr(database, res, 'content');
+ if (!preview_URL || !preview_name) continue;
+
+ window.openDialog("chrome://communicator/content/sidebar/preview.xul",
+ "_blank", "chrome,resizable,close,dialog=no",
+ preview_name, preview_URL);
+ }
+ }
+}
+
+// Add the selected panel(s).
+function AddPanel()
+{
+ var added = -1;
+
+ var tree = document.getElementById('other-panels');
+ var database = tree.database;
+ var sel = tree.view.selection;
+ var ranges = sel.getRangeCount();
+ for (var range = 0; range < ranges; ++range) {
+ var min = {}, max = {};
+ sel.getRangeAt(range, min, max);
+ for (var index = min.value; index <= max.value; ++index) {
+ var item = tree.contentView.getItemAtIndex(index);
+ if (item.getAttribute("container") != "true") {
+ var res = RDF.GetResource(item.id);
+ // Add the panel to the current list.
+ added = add_node_to_current_list(database, res);
+ }
+ }
+ }
+
+ if (added >= 0) {
+ // Remove the selection in the other list.
+ // Selection will move to "current" list.
+ tree.view.selection.clearSelection();
+
+ var current_panels = document.getElementById('current-panels');
+ current_panels.view.selection.select(added);
+ current_panels.treeBoxObject.ensureRowIsVisible(added);
+ }
+}
+
+// Copy a panel node into a database such as the current panel list.
+function add_node_to_current_list(registry, service)
+{
+ debug("Adding "+service.Value);
+
+ // Copy out the attributes we want
+ var option_title = get_attr(registry, service, 'title');
+ var option_customize = get_attr(registry, service, 'customize');
+ var option_content = get_attr(registry, service, 'content');
+ if (!option_title || !option_content)
+ return -1;
+
+ var tree = document.getElementById('current-panels');
+ var tree_root = tree.lastChild;
+
+ // Check to see if the panel already exists...
+ var i = 0;
+ for (var treeitem = tree_root.firstChild; treeitem; treeitem = treeitem.nextSibling) {
+ if (treeitem.id == service.Value)
+ // The panel is already in the current panel list.
+ // Avoid adding it twice.
+ return i;
+ ++i;
+ }
+
+ // Create a treerow for the new panel
+ var item = document.createElement('treeitem');
+ var row = document.createElement('treerow');
+ var cell = document.createElement('treecell');
+
+ // Copy over the attributes
+ item.setAttribute('id', service.Value);
+ cell.setAttribute('label', option_title);
+
+ // Add it to the current panels tree
+ item.appendChild(row);
+ row.appendChild(cell);
+ tree_root.appendChild(item);
+ return i;
+}
+
+// Remove the selected panel(s) from the current list tree.
+function RemovePanel()
+{
+ var tree = document.getElementById('current-panels');
+ var sel = tree.view.selection;
+
+ var nextNode = -1;
+ var rangeCount = sel.getRangeCount();
+ for (var range = rangeCount-1; range >= 0; --range) {
+ var min = {}, max = {};
+ sel.getRangeAt(range, min, max);
+ for (var index = max.value; index >= min.value; --index) {
+ var item = tree.contentView.getItemAtIndex(index);
+ nextNode = item.nextSibling ? index : -1;
+ item.remove();
+ }
+ }
+
+ if (nextNode >= 0)
+ sel.select(nextNode);
+}
+
+// Bring up a new window with the customize url
+// for an individual panel.
+function CustomizePanel()
+{
+ var tree = document.getElementById('current-panels');
+ var numSelected = tree.view.selection.count;
+
+ if (numSelected == 1) {
+ var index = tree.currentIndex;
+ var selectedNode = tree.contentView.getItemAtIndex(index);
+ var panel_id = selectedNode.getAttribute('id');
+ var customize_url = selectedNode.getAttribute('customize');
+
+ debug("url = " + customize_url);
+
+ if (!customize_url) return;
+
+ window.openDialog('chrome://communicator/content/sidebar/customize-panel.xul',
+ '_blank',
+ 'chrome,resizable,width=690,height=600,dialog=no,close',
+ panel_id,
+ customize_url,
+ sidebarObj.datasource_uri,
+ sidebarObj.resource);
+ }
+}
+
+function BrowseMorePanels()
+{
+ var url = '';
+ var browser_url = "chrome://navigator/content/navigator.xul";
+ var locale;
+ try {
+ url = Services.prefs.getCharPref("sidebar.customize.more_panels.url");
+ var temp = Services.prefs.getCharPref("browser.chromeURL");
+ if (temp)
+ browser_url = temp;
+ } catch(ex) {
+ debug("Unable to get prefs: "+ex);
+ }
+ window.openDialog(browser_url, "_blank", "chrome,all,dialog=no", url);
+}
+
+function customize_getBrowserURL()
+{
+ return url;
+}
+
+// Serialize the new list of panels.
+function Save()
+{
+ persist_dialog_dimensions();
+
+ var all_panels = document.getElementById('other-panels');
+ var current_panels = document.getElementById('current-panels');
+
+ // See if list membership has changed
+ var panels = [];
+ var tree_root = current_panels.lastChild.childNodes;
+ var list_unchanged = (tree_root.length == original_panels.length);
+ for (var i = 0; i < tree_root.length; i++) {
+ var panel = tree_root[i].id;
+ panels.push(panel);
+ panels[panel] = true;
+ if (list_unchanged && original_panels[i] != panel)
+ list_unchanged = false;
+ }
+ if (list_unchanged)
+ return;
+
+ // Remove all the current panels from the datasource.
+ current_panels = sidebarObj.container.GetElements();
+ while (current_panels.hasMoreElements()) {
+ panel = current_panels.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ // "Check if the item is one of the broadcaster panels imported to RDF from
+ // mainBroadcasterSet. If so, then don't remove it from datasource.
+ var master_list = sidebarObj.datasource.GetTarget(RDF.GetResource(allPanelsObj.resource), RDF.GetResource(NC + "panel-list"), true);
+ var masterSeq = Cc["@mozilla.org/rdf/container;1"]
+ .createInstance(Ci.nsIRDFContainer);
+ masterSeq.Init(sidebarObj.datasource, master_list);
+ var inmaster = (masterSeq.IndexOf(panel) != -1);
+
+ if (panel.Value in panels || inmaster) {
+ // This panel will remain in the sidebar.
+ // Remove the resource, but keep all the other attributes.
+ // Removing it will allow it to be added in the correct order.
+ // Saving the attributes will preserve things such as the exclude state.
+ sidebarObj.container.RemoveElement(panel, false);
+ } else {
+ // Kiss it goodbye.
+ delete_resource_deeply(sidebarObj.container, panel);
+ }
+ }
+
+ // Add the new list of panels
+ for (var ii = 0; ii < panels.length; ++ii) {
+ var id = panels[ii];
+ var resource = RDF.GetResource(id);
+ if (id in original_panels) {
+ sidebarObj.container.AppendElement(resource);
+ } else {
+ copy_resource_deeply(all_panels.database, resource, sidebarObj.container);
+ }
+ }
+ refresh_all_sidebars();
+
+ // Write the modified panels out.
+ sidebarObj.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+}
+
+// Search for an element in an array
+function has_element(array, element) {
+ for (var ii=0; ii < array.length; ii++) {
+ if (array[ii] == element) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Search for targets from resource in datasource
+function has_targets(datasource, resource) {
+ var arcs = datasource.ArcLabelsOut(resource);
+ return arcs.hasMoreElements();
+}
+
+// Use an assertion to pass a "refresh" event to all the sidebars.
+// They use observers to watch for this assertion (in sidebarOverlay.js).
+function refresh_all_sidebars() {
+ sidebarObj.datasource.Assert(RDF.GetResource(sidebarObj.resource),
+ RDF.GetResource(NC + "refresh"),
+ RDF.GetLiteral("true"),
+ true);
+ sidebarObj.datasource.Unassert(RDF.GetResource(sidebarObj.resource),
+ RDF.GetResource(NC + "refresh"),
+ RDF.GetLiteral("true"));
+}
+
+// Remove a resource and all the arcs out from it.
+function delete_resource_deeply(container, resource) {
+ var arcs = container.DataSource.ArcLabelsOut(resource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext();
+ var targets = container.DataSource.GetTargets(resource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ container.DataSource.Unassert(resource, arc, target, true);
+ }
+ }
+ container.RemoveElement(resource, false);
+}
+
+// Copy a resource and all its arcs out to a new container.
+function copy_resource_deeply(source_datasource, resource, dest_container) {
+ var arcs = source_datasource.ArcLabelsOut(resource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext();
+ var targets = source_datasource.GetTargets(resource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ dest_container.DataSource.Assert(resource, arc, target, true);
+ }
+ }
+ dest_container.AppendElement(resource);
+}
+
+function enable_buttons_for_other_panels()
+{
+ var add_button = document.getElementById('add_button');
+ var preview_button = document.getElementById('preview_button');
+ var all_panels = document.getElementById('other-panels');
+
+ var sel = all_panels.view.selection;
+ var num_selected = sel ? sel.count : 0;
+ if (sel) {
+ var ranges = sel.getRangeCount();
+ for (var range = 0; range < ranges; ++range) {
+ var min = {}, max = {};
+ sel.getRangeAt(range, min, max);
+ for (var index = min; index <= max; ++index) {
+ var node = all_panels.contentView.getItemAtIndex(index);
+ if (node.getAttribute('container') != 'true') {
+ ++num_selected;
+ }
+ }
+ }
+ }
+
+ if (num_selected > 0) {
+ add_button.removeAttribute('disabled');
+ preview_button.removeAttribute('disabled');
+ } else {
+ add_button.setAttribute('disabled','true');
+ preview_button.setAttribute('disabled','true');
+ }
+}
+
+function enable_buttons_for_current_panels() {
+ var up = document.getElementById('up');
+ var down = document.getElementById('down');
+ var tree = document.getElementById('current-panels');
+ var customize = document.getElementById('customize-button');
+ var remove = document.getElementById('remove-button');
+
+ var numSelected = tree.view.selection.count;
+ var canMoveUp = false, canMoveDown = false, customizeURL = '';
+
+ if (numSelected == 1 && tree.view.selection.isSelected(tree.currentIndex)) {
+ var selectedNode = tree.view.getItemAtIndex(tree.currentIndex);
+ customizeURL = selectedNode.getAttribute('customize');
+ canMoveUp = selectedNode != selectedNode.parentNode.firstChild;
+ canMoveDown = selectedNode != selectedNode.parentNode.lastChild;
+ }
+
+ up.disabled = !canMoveUp;
+ down.disabled = !canMoveDown;
+ customize.disabled = !customizeURL;
+ remove.disabled = !numSelected;
+}
+
+function persist_dialog_dimensions() {
+ // Stole this code from navigator.js to
+ // insure the windows dimensions are saved.
+
+ // Get the current window position/size.
+ var x = window.screenX;
+ var y = window.screenY;
+ var h = window.outerHeight;
+ var w = window.outerWidth;
+
+ // Store these into the window attributes (for persistence).
+ var win = document.getElementById( "main-window" );
+ win.setAttribute( "x", x );
+ win.setAttribute( "y", y );
+ win.setAttribute( "height", h );
+ win.setAttribute( "width", w );
+}
+
+///////////////////////////////////////////////////////////////
+// Handy Debug Tools
+//////////////////////////////////////////////////////////////
+var debug = null;
+var dump_attributes = null;
+var dump_tree = null;
+var _dump_tree_recur = null;
+
+if (!CUST_DEBUG) {
+ debug = function (s) {};
+ dump_attributes = function (node, depth) {};
+ dump_tree = function (node) {};
+ _dump_tree_recur = function (node, depth, index) {};
+} else {
+ debug = function (s) { dump("-*- sb customize: " + s + "\n"); };
+
+ dump_attributes = function (node, depth) {
+ var attributes = node.attributes;
+ var indent = "| | | | | | | | | | | | | | | | | | | | | | | | | | | | . ";
+
+ if (!attributes || attributes.length == 0) {
+ debug(indent.substr(indent.length - depth*2) + "no attributes");
+ }
+ for (var ii=0; ii < attributes.length; ii++) {
+ var attr = attributes.item(ii);
+ debug(indent.substr(indent.length - depth*2) + attr.name +
+ "=" + attr.value);
+ }
+ }
+ dump_tree = function (node) {
+ _dump_tree_recur(node, 0, 0);
+ }
+ _dump_tree_recur = function (node, depth, index) {
+ if (!node) {
+ debug("dump_tree: node is null");
+ }
+ var indent = "| | | | | | | | | | | | | | | | | | | | | | | | | | | | + ";
+ debug(indent.substr(indent.length - depth*2) + index +
+ " " + node.nodeName);
+ if (node.nodeType != Node.TEXT_NODE) {
+ dump_attributes(node, depth);
+ }
+ var kids = node.childNodes;
+ for (var ii=0; ii < kids.length; ii++) {
+ _dump_tree_recur(kids[ii], depth + 1, ii);
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////////
+// Install the load/unload handlers
+//////////////////////////////////////////////////////////////
+addEventListener("load", sidebar_customize_init, false);
+addEventListener("unload", sidebar_customize_destruct, false);
diff --git a/comm/suite/components/sidebar/content/customize.xul b/comm/suite/components/sidebar/content/customize.xul
new file mode 100644
index 0000000000..0e76f65445
--- /dev/null
+++ b/comm/suite/components/sidebar/content/customize.xul
@@ -0,0 +1,137 @@
+<?xml version="1.0"?> <!-- -*- Mode: HTML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/customize.css"
+ type="text/css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % customizeDTD SYSTEM "chrome://communicator/locale/sidebar/customize.dtd" >
+%customizeDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+]>
+
+<dialog
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="main-window"
+ title="&sidebar.customize.title;"
+ windowtype="sidebar:customize"
+ height="400"
+ persist="screenX screenY width height"
+ buttons="accept,cancel,extra2"
+ spacerflex="1"
+ buttonlabelextra2="&sidebar.more.label;"
+ buttonaccesskeyextra2="&sidebar.more.accesskey;"
+ ondialogextra2="BrowseMorePanels();"
+ ondialogaccept="return Save();">
+
+ <script src="chrome://communicator/content/sidebar/customize.js"/>
+
+ <hbox flex="1">
+ <vbox flex="1">
+ <label accesskey="&sidebar.customize.additional.accesskey;"
+ control="other-panels" value="&sidebar.customize.additional.label;"
+ crop="right"/>
+
+ <tree id="other-panels" flex="1"
+ datasources="rdf:null" hidecolumnpicker="true"
+ containment="http://home.netscape.com/NC-rdf#panel-list"
+ onselect="SelectChangeForOtherPanels(event, event.target.parentNode.parentNode);"
+ onclick="if (event.detail == 2) { AddPanel(); } ClickOnOtherPanels(event);">
+
+ <template>
+ <rule>
+ <conditions>
+ <content uri="?uri"/>
+ <triple subject="?uri" object="?panel-list"
+ predicate="http://home.netscape.com/NC-rdf#panel-list"/>
+ <member container="?panel-list" child="?panel"/>
+ </conditions>
+
+ <bindings>
+ <binding subject="?panel" object="?title"
+ predicate="http://home.netscape.com/NC-rdf#title"/>
+ <binding subject="?panel" object="?link"
+ predicate="http://home.netscape.com/NC-rdf#link"/>
+ </bindings>
+
+ <action>
+ <treechildren>
+ <treeitem uri="?panel" link="?link">
+ <treerow>
+ <treecell label="?title"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </action>
+ </rule>
+ </template>
+
+ <treecols>
+ <treecol id="AvailNameCol" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ </tree>
+
+ <!-- xxxslamm Need to add descriptive panel text here -->
+ <hbox class="button-group">
+ <button id="add_button" oncommand="AddPanel()"
+ label="&sidebar.customize.add.label;"
+ accesskey="&sidebar.customize.add.accesskey;" disabled="true"/>
+
+ <button id="preview_button" oncommand="PreviewPanel()"
+ label="&sidebar.customize.preview.label;"
+ accesskey="&sidebar.customize.preview.accesskey;"
+ disabled="true"/>
+ </hbox>
+ </vbox>
+
+ <separator orient="vertical"/>
+
+ <!-- The panels that the user currently has chosen -->
+ <vbox flex="1">
+ <label value="&sidebar.customize.current2.label;"
+ accesskey="&sidebar.customize.current2.accesskey;"
+ control="current-panels" crop="right"/>
+ <tree id="current-panels" flex="1" hidecolumnpicker="true"
+ onselect="SelectChangeForCurrentPanels();">
+ <treecols>
+ <treecol id="CurrentNameCol" flex="1" hideheader="true"/>
+ </treecols>
+
+ <treechildren/>
+ </tree>
+
+ <hbox class="button-group">
+ <button id="customize-button" oncommand="CustomizePanel();"
+ label="&sidebar.customize.customize.label;" disabled="true"
+ accesskey="&sidebar.customize.customize.accesskey;"/>
+ <button id="remove-button" oncommand="RemovePanel()"
+ label="&sidebar.customize.remove.label;" disabled="true"
+ accesskey="&sidebar.customize.remove.accesskey;"/>
+ </hbox>
+ </vbox>
+
+ <separator orient="vertical" class="thin"/>
+
+ <!-- The 'reorder' buttons -->
+ <vbox id="reorder">
+ <spacer flex="1"/>
+ <button oncommand="MoveUp();" id="up" class="up"
+ disabled="true" label="&sidebar.customize.up.label;"
+ accesskey="&sidebar.customize.up.accesskey;"/>
+ <button oncommand="MoveDown();" id="down" class="down"
+ disabled="true" label="&sidebar.customize.down.label;"
+ accesskey="&sidebar.customize.down.accesskey;"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ </hbox>
+
+</dialog>
+
diff --git a/comm/suite/components/sidebar/content/preview.js b/comm/suite/components/sidebar/content/preview.js
new file mode 100644
index 0000000000..c5238beacd
--- /dev/null
+++ b/comm/suite/components/sidebar/content/preview.js
@@ -0,0 +1,15 @@
+/* -*- Mode: Java -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Init()
+{
+ var panel_name = window.arguments[0];
+ var panel_URL = window.arguments[1];
+
+ var panel_title = document.getElementById('paneltitle');
+ var preview_frame = document.getElementById('previewframe');
+ panel_title.setAttribute('label', panel_name);
+ preview_frame.setAttribute('src', panel_URL);
+}
diff --git a/comm/suite/components/sidebar/content/preview.xul b/comm/suite/components/sidebar/content/preview.xul
new file mode 100644
index 0000000000..ef2434c278
--- /dev/null
+++ b/comm/suite/components/sidebar/content/preview.xul
@@ -0,0 +1,30 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebar.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/preview.css"
+ type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://communicator/locale/sidebar/preview.dtd" >
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="Init();"
+ title="&sidebar.preview.title.label;">
+
+ <script src="chrome://communicator/content/sidebar/preview.js" />
+
+ <vbox id="panel-container" flex="1">
+
+ <hbox id="paneltitle" class="box-texttab texttab-sidebar" selected="true"/>
+ <!-- <iframe id="previewframe" type="content" src="about:blank" flex="1"/>-->
+ <iframe class="box-panel" id="previewframe" type="content" src="about:blank" flex="1"/>
+
+ </vbox>
+
+</window>
diff --git a/comm/suite/components/sidebar/content/sidebarBindings.xml b/comm/suite/components/sidebar/content/sidebarBindings.xml
new file mode 100644
index 0000000000..675d591957
--- /dev/null
+++ b/comm/suite/components/sidebar/content/sidebarBindings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="globalBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="texttab">
+ <content>
+ <xul:image class="box-texttab-left"/>
+ <xul:vbox class="box-texttab-text-container" xbl:inherits="value" flex="1">
+ <xul:spacer flex="1"/>
+ <xul:label class="box-texttab-text" xbl:inherits="value=label" crop="right"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ <xul:image class="box-texttab-right"/>
+ <xul:spacer class="box-texttab-right-space"/>
+ </content>
+ </binding>
+
+ <binding id="sidebar-header-box" extends="xul:box">
+ <content align="center">
+ <xul:label class="sidebar-header-text" xbl:inherits="value=label,crop" crop="right" flex="1"/>
+ <xul:box>
+ <children/>
+ </xul:box>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/sidebar/content/sidebarOverlay.css b/comm/suite/components/sidebar/content/sidebarOverlay.css
new file mode 100644
index 0000000000..0d4df5f16a
--- /dev/null
+++ b/comm/suite/components/sidebar/content/sidebarOverlay.css
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** sidebarOverlay.css [CONTENT]
+ * This file is for style rules essential for correct sidebar operation.
+ * These rules will not change on a skin-by-skin basis.
+ **/
+
+#sidebar-box {
+ width: 162px;
+ min-height: 10px;
+ min-width: 30px;
+ max-width: 400px;
+}
+
+#sidebar-panels {
+ min-width: 1px;
+ min-height: 10px;
+}
+
+.iframe-panel {
+ min-width: 1px;
+ min-height: 1px;
+}
+
+#sidebar-iframe-no-panels {
+ min-width: 1px;
+ min-height: 1px;
+ overflow: auto;
+}
+
+.browser-sidebar {
+ min-width: 1px;
+ min-height: 1px;
+}
+
+/*
+ * Sidebar and Panel title buttons
+ */
+sidebarheader[type="box"] {
+ -moz-binding: url(chrome://communicator/content/sidebar/sidebarBindings.xml#sidebar-header-box);
+}
+sidebarheader[type="splitter"] {
+ -moz-binding: url(chrome://communicator/content/sidebar/sidebarBindings.xml#sidebar-header-splitter);
+ /* a vertical splitter */
+ cursor: n-resize;
+}
+
+.sidebarheader-main {
+ min-width: 1px;
+ min-height: 1px;
+}
+
+/**
+ * texttab folder lookalike e.g. for sidebar panel headers
+ */
+ .box-texttab
+ {
+ min-height : 10px;
+ min-width : 10px;
+ }
+
+ .box-texttab-right-space
+ {
+ min-width : 1px;
+ }
+
+/**
+ * prevent the notification in the sidebar from being too wide
+ */
+.sidebar-notificationbox > notification {
+ -moz-binding: url(chrome://communicator/content/bindings/notification.xml#sidebar-notification);
+}
+
+.sidebar-notificationbox > notification[value="addon-progress"] {
+ -moz-binding: url(chrome://communicator/content/bindings/notification.xml#sidebar-addon-progress-notification);
+}
diff --git a/comm/suite/components/sidebar/content/sidebarOverlay.js b/comm/suite/components/sidebar/content/sidebarOverlay.js
new file mode 100644
index 0000000000..95b0af029d
--- /dev/null
+++ b/comm/suite/components/sidebar/content/sidebarOverlay.js
@@ -0,0 +1,1704 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// xul/id summary:
+//
+// <box> sidebar-box
+// <splitter> sidebar-panels-splitter
+// <box> sidebar-panels-splitter-box*
+// <sidebarheader> sidebar-title-box
+// <menubutton> sidebar-panel-picker*
+// <menupopup> sidebar-panel-picker-popup
+// <box> sidebar-panels
+// <template> sidebar-template*
+// <box> sidebar-iframe-no-panels
+// <splitter> sidebar-splitter
+// <menupopup> menu_View_Popup*
+// <menuitem> sidebar-menu
+
+//////////////////////////////////////////////////////////////
+// Import modules
+//////////////////////////////////////////////////////////////
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+//////////////////////////////////////////////////////////////
+// Global variables
+//////////////////////////////////////////////////////////////
+
+
+var gCurFrame;
+var gTimeoutID = null;
+var gMustInit = true;
+var gAboutToUncollapse = false;
+var gCheckMissingPanels = true;
+
+function setBlank()
+{
+ gTimeoutID = null;
+ gCurFrame.setAttribute('src', 'chrome://communicator/content/sidebar/PageNotFound.xul');
+}
+
+
+// Uncomment for debug output
+const SB_DEBUG = false;
+
+// pref for limiting number of tabs in view
+// initialized in sidebar_overlay_init()
+var gNumTabsInViewPref;
+
+// The rdf service
+var RDF = Cc["@mozilla.org/rdf/rdf-service;1"]
+ .getService(Ci.nsIRDFService);
+
+const NC = "http://home.netscape.com/NC-rdf#";
+
+// The directory services property to find panels.rdf
+const PANELS_RDF_FILE = "UPnls";
+const SIDEBAR_VERSION = "0.1";
+
+// The default sidebar:
+var sidebarObj = new Object;
+sidebarObj.never_built = true;
+
+//////////////////////////////////////////////////////////////////////
+// sbPanelList Class
+//
+// Wrapper around DOM representation of the sidebar. This UI event
+// handlers and the sidebar datasource observers call into this. This
+// class is responsible for keeping the DOM state of the sidebar up to
+// date.
+// This class does not make any changes to the sidebar rdf datasource.
+//////////////////////////////////////////////////////////////////////
+
+function sbPanelList(container_id)
+{
+ debug("sbPanelList("+container_id+")");
+ this.node = document.getElementById(container_id);
+ this.childNodes = this.node.childNodes;
+ this.initialized = false; // set after first display of tabs
+}
+
+sbPanelList.prototype.get_panel_from_id =
+function (id)
+{
+ debug("get_panel_from_id(" + id + ")");
+ var index = 0;
+ var header = null;
+ if (id && id != '') {
+ for (var ii=2; ii < this.node.childNodes.length; ii += 2) {
+ header = this.node.childNodes.item(ii);
+ if (header.getAttribute('id') == id) {
+ debug("get_panel_from_id: Found at index, " + ii);
+ index = ii;
+ break;
+ }
+ }
+ }
+ if (index > 0) {
+ return new sbPanel(id, header, index);
+ } else {
+ return null;
+ }
+}
+
+sbPanelList.prototype.get_panel_from_header_node =
+function (node)
+{
+ return this.get_panel_from_id(node.getAttribute('id'));
+}
+
+sbPanelList.prototype.get_panel_from_header_index =
+function (index)
+{
+ return this.get_panel_from_header_node(this.node.childNodes.item(index));
+}
+
+sbPanelList.prototype.find_first =
+function (panels)
+{
+ debug("pick_default_panel: length=" + this.node.childNodes.length);
+ for (var ii = 2; ii < this.node.childNodes.length; ii += 2) {
+ var panel = this.get_panel_from_header_index(ii);
+ if (!panel.is_excluded() && panel.is_in_view()) {
+ return panel;
+ }
+ }
+ return null;
+}
+
+sbPanelList.prototype.find_last =
+function (panels)
+{
+ debug("pick_default_panel: length=" + this.node.childNodes.length);
+ for (var ii=(this.node.childNodes.length - 1); ii >= 2; ii -= 2) {
+ var panel = this.get_panel_from_header_index(ii);
+ if (!panel.is_excluded() && panel.is_in_view()) {
+ return panel;
+ }
+ }
+ return null;
+}
+
+sbPanelList.prototype.visible_panels_exist =
+function ()
+{
+ var i;
+ var panels = this.node.childNodes;
+ for (i = 2; i < panels.length; i += 2)
+ {
+ if (!panels.item(i).hidden)
+ return true;
+ }
+ return false;
+}
+
+sbPanelList.prototype.num_panels_included =
+function ()
+{
+ var count = 0;
+ var panels = this.node.childNodes;
+ for (var i = 2; i < panels.length; i += 2)
+ {
+ var curr = this.get_panel_from_header_index(i);
+ if (!curr.is_excluded())
+ count++;
+ }
+ return count;
+}
+
+sbPanelList.prototype.num_panels_in_view =
+function ()
+{
+ var count = 0;
+ var panels = this.node.childNodes;
+ for (var i = 2; i < panels.length; i += 2)
+ {
+ var curr = this.get_panel_from_header_index(i);
+ if (curr.is_in_view())
+ count++;
+ }
+ return count;
+}
+
+sbPanelList.prototype.select =
+function (panel, force_reload)
+{
+ if (!force_reload && panel.is_selected()) {
+ return;
+ }
+ // select(): Open this panel and possibly reload it.
+ if (this.node.getAttribute('last-selected-panel') != panel.id) {
+ // "last-selected-panel" is used as a global variable.
+ // this.update() will reference "last-selected-panel".
+ // This way the value can be persisted in xulstore.json.
+ this.node.setAttribute('last-selected-panel', panel.id);
+ }
+ this.update(force_reload);
+}
+
+sbPanelList.prototype.exclude =
+function (panel)
+{
+ if (this.node.getAttribute('last-selected-panel') == panel.id) {
+ this.select_default_panel();
+ } else {
+ this.update(false);
+ }
+}
+
+
+sbPanelList.prototype.select_default_panel =
+function ()
+{
+ var default_panel = null
+
+ // First, check the XUL for the "defaultpanel" attribute of "sidebar-box".
+ var sidebar_container = document.getElementById('sidebar-box');
+ var content_default_id = sidebar_container.getAttribute('defaultpanel');
+ if (content_default_id != '') {
+ var content = sidebarObj.panels.get_panel_from_id(content_default_id);
+ if (content && !content.is_excluded() && content.is_in_view()) {
+ default_panel = content;
+ }
+ }
+
+ // Second, try to use the panel persisted in 'last-selected-panel'.
+ if (!default_panel) {
+ var last_selected_id = this.node.getAttribute('last-selected-panel');
+ if (last_selected_id != '') {
+ var last = sidebarObj.panels.get_panel_from_id(last_selected_id);
+ if (last && !last.is_excluded() && last.is_in_view()) {
+ default_panel = last;
+ }
+ }
+ }
+
+ // Finally, just use the last one in the list.
+ if (!default_panel) {
+ default_panel = this.find_last();
+ }
+
+ if (default_panel) {
+ this.node.setAttribute('last-selected-panel', default_panel.id);
+ }
+ this.update(false);
+}
+
+sbPanelList.prototype.refresh =
+function ()
+{
+ var last_selected_id = this.node.getAttribute('last-selected-panel');
+ var last_selected = sidebarObj.panels.get_panel_from_id(last_selected_id);
+ if (last_selected && last_selected.is_selected()) {
+ // The desired panel is already selected
+ this.update(false);
+ } else {
+ this.select_default_panel();
+ }
+}
+
+// panel_loader(): called from a timer that is set in sbPanelList.update()
+// Removes the "Loading..." screen when the panel has finished loading.
+function panel_loader() {
+ debug("panel_loader()");
+
+ if (gTimeoutID != null) {
+ clearTimeout(gTimeoutID);
+ gTimeoutID = null;
+ }
+
+ this.removeEventListener("load", panel_loader, true);
+ this.removeAttribute('collapsed');
+ // uncollapse the notificationbox element
+ this.parentNode.removeAttribute('collapsed');
+ // register a progress listener for the notificationbox,
+ // now that this browser has a docShell
+ this.parentNode.addProgressListener();
+ this.setAttribute('loadstate', 'loaded');
+ // hide the load area
+ this.parentNode.parentNode.firstChild.setAttribute('hidden', 'true');
+
+ if (this.hasAttribute('focusOnLoad')) {
+ var elementToFocus = this.contentDocument.documentElement.getAttribute('elementtofocus');
+ if (elementToFocus) {
+ var element = this.contentDocument.getElementById(elementToFocus);
+ if (element)
+ element.focus();
+ else
+ dump(elementToFocus + ' element was not found to focus!\n');
+ } else {
+ this.contentWindow.focus();
+ }
+ this.removeAttribute('focusOnLoad');
+ }
+}
+sbPanelList.prototype.update =
+function (force_reload)
+{
+ // This function requires that the attribute 'last-selected-panel'
+ // holds the id of a non-excluded panel. If it doesn't, no panel will
+ // be selected. The attribute is used instead of a function
+ // parameter to allow the value to be persisted in xulstore.json.
+ var selected_id = this.node.getAttribute('last-selected-panel');
+
+ if (sidebar_is_collapsed()) {
+ sidebarObj.collapsed = true;
+ } else {
+ sidebarObj.collapsed = false;
+ }
+
+ var num_included = sidebarObj.panels.num_panels_included();
+ if (num_included > gNumTabsInViewPref)
+ document.getElementById("nav-buttons-box").hidden = false;
+ else
+ document.getElementById("nav-buttons-box").hidden = true;
+
+ var have_set_top = 0;
+ var have_set_after_selected = 0;
+ var is_after_selected = 0;
+ var last_header = 0;
+ var num_in_view = 0;
+ debug("this.initialized: " + this.initialized);
+ for (var ii=2; ii < this.node.childNodes.length; ii += 2) {
+ var header = this.node.childNodes.item(ii);
+ var content = this.node.childNodes.item(ii+1);
+ var id = header.getAttribute('id');
+ var panel = new sbPanel(id, header, ii);
+ var excluded = panel.is_excluded();
+ var in_view = false;
+ if (!this.initialized)
+ {
+ if (num_in_view < gNumTabsInViewPref)
+ in_view = true;
+ }
+ else
+ {
+ if (header.getAttribute("in-view") == "true")
+ in_view = true;
+ }
+ if (excluded || !in_view)
+ {
+ debug("item("+ii/2+") excluded: " + excluded +
+ " in view: " + in_view);
+ header.setAttribute('hidden','true');
+ content.setAttribute('hidden','true');
+ if (!in_view)
+ {
+ header.setAttribute("in-view", false);
+ header.removeAttribute("top-panel");
+ header.removeAttribute("last-panel");
+ }
+ } else {
+ // only set if in view
+ if (!this.initialized || (num_in_view < gNumTabsInViewPref))
+ last_header = header;
+ header.removeAttribute('last-panel');
+ // only set if in view
+ if (!have_set_top &&
+ (!this.initialized || (header.getAttribute("in-view") == "true")))
+ {
+ header.setAttribute('top-panel','true');
+ have_set_top = 1;
+ } else {
+ header.removeAttribute('top-panel');
+ }
+ if (!have_set_after_selected && is_after_selected) {
+ header.setAttribute('first-panel-after-selected','true');
+ have_set_after_selected = 1
+ } else {
+ header.removeAttribute('first-panel-after-selected');
+ }
+ header.removeAttribute('hidden');
+ header.setAttribute("in-view", true);
+ num_in_view++;
+
+ // (a) when we have hit the maximum number of tabs that can be in view and no tab
+ // has been selected yet
+ // -or-
+ // (b) when we have reached the last tab we are about to display
+ if ( ((num_in_view == num_included) ||
+ (num_in_view == gNumTabsInViewPref)) &&
+ !is_after_selected )
+ {
+ selected_id = id;
+ this.node.setAttribute('last-selected-panel', id);
+ }
+
+ // Pick sandboxed, or unsandboxed iframe
+ var iframe = panel.get_iframe();
+ var notificationbox = iframe.parentNode;
+ var load_state;
+
+ if (selected_id == id) {
+ is_after_selected = 1
+ debug("item("+ii/2+") selected");
+ header.setAttribute('selected', 'true');
+ content.removeAttribute('hidden');
+ content.removeAttribute('collapsed');
+
+ if (sidebarObj.collapsed && panel.is_sandboxed()) {
+ if (!panel.is_persistent()) {
+ debug(" set src=about:blank");
+ iframe.setAttribute('src', 'about:blank');
+ }
+ } else {
+ var saved_src = iframe.getAttribute('content');
+ var src = iframe.getAttribute('src');
+ // either we have been requested to force_reload or the
+ // panel src has changed so we must restore the original src
+ if (force_reload || (saved_src != src)) {
+ debug(" set src="+saved_src);
+ iframe.setAttribute('src', saved_src);
+
+ if (gTimeoutID != null)
+ clearTimeout(gTimeoutID);
+
+ gCurFrame = iframe;
+ gTimeoutID = setTimeout(setBlank, 20000);
+ }
+ }
+
+ load_state = content.getAttribute('loadstate');
+ if (load_state == 'stopped') {
+ load_state = 'never loaded';
+ toggleLoadarea(content);
+ }
+ if (load_state == 'never loaded') {
+ iframe.removeAttribute('hidden');
+ iframe.setAttribute('loadstate', 'loading');
+ iframe.addEventListener('load', panel_loader, true);
+ }
+ } else {
+ debug("item("+ii/2+")");
+ header.removeAttribute('selected');
+ content.setAttribute('collapsed','true');
+
+ if (!panel.is_persistent()) {
+ iframe.setAttribute('src', 'about:blank');
+ load_state = content.getAttribute('loadstate');
+ if (load_state == 'loading') {
+ iframe.removeEventListener("load", panel_loader, true);
+ content.setAttribute('hidden','true');
+ iframe.setAttribute('loadstate', 'never loaded');
+ }
+ }
+ }
+ }
+ }
+ if (last_header) {
+ last_header.setAttribute('last-panel','true');
+ }
+
+ var no_panels_iframe = document.getElementById('sidebar-iframe-no-panels');
+ if (have_set_top) {
+ no_panels_iframe.setAttribute('hidden','true');
+ // The hide and show of 'sidebar-panels' should not be needed,
+ // but some old profiles may have this persisted as hidden (50973).
+ this.node.removeAttribute('hidden');
+ } else {
+ no_panels_iframe.removeAttribute('hidden');
+ }
+
+ this.initialized = true;
+}
+
+
+//////////////////////////////////////////////////////////////////////
+// sbPanel Class
+//
+// Like sbPanelList, this class is a wrapper around DOM representation
+// of individual panels in the sidebar. This UI event handlers and the
+// sidebar datasource observers call into this. This class is
+// responsible for keeping the DOM state of the sidebar up to date.
+// This class does not make any changes to the sidebar rdf datasource.
+//////////////////////////////////////////////////////////////////////
+
+function sbPanel(id, header, index)
+{
+ // This constructor should only be called by sbPanelList class.
+ // To create a panel instance, use the helper functions in sbPanelList:
+ // sb_panel.get_panel_from_id(id)
+ // sb_panel.get_panel_from_header_node(dom_node)
+ // sb_panel.get_panel_from_header_index(index)
+ this.id = id;
+ this.header = header;
+ this.index = index;
+ this.parent = sidebarObj.panels;
+}
+
+sbPanel.prototype.get_header =
+function ()
+{
+ return this.header;
+}
+
+sbPanel.prototype.get_content =
+function ()
+{
+ return this.get_header().nextSibling;
+}
+
+sbPanel.prototype.is_sandboxed =
+function ()
+{
+ if (typeof this.sandboxed == "undefined") {
+ var notificationbox = this.get_content().childNodes.item(1);
+ var unsandboxed_iframe = notificationbox.firstChild;
+ this.sandboxed = !unsandboxed_iframe.getAttribute('content').match(/^chrome:/);
+ }
+ return this.sandboxed;
+}
+
+sbPanel.prototype.get_iframe =
+function ()
+{
+ if (typeof this.iframe == "undefined") {
+ var notificationbox = this.get_content().childNodes.item(1);
+ this.iframe = this.is_sandboxed() ? notificationbox.lastChild :
+ notificationbox.firstChild;
+ }
+ return this.iframe;
+}
+
+// This exclude function is used on panels and on the panel picker menu.
+// That is why it is hanging out in the global name space instead of
+// minding its own business in the class.
+function sb_panel_is_excluded(node)
+{
+ var exclude = node.getAttribute('exclude');
+ return ( exclude && exclude != '' &&
+ exclude.includes(sidebarObj.component));
+}
+sbPanel.prototype.is_excluded =
+function ()
+{
+ return sb_panel_is_excluded(this.get_header());
+}
+
+sbPanel.prototype.is_in_view =
+function()
+{
+ return (this.header.getAttribute("in-view") == "true");
+}
+
+sbPanel.prototype.is_selected =
+function (panel_id)
+{
+ return 'true' == this.get_header().getAttribute('selected');
+}
+
+sbPanel.prototype.is_persistent =
+function ()
+{
+ var rv = false;
+ var datasource = sidebarObj.datasource;
+ var persistNode = datasource.GetTarget(RDF.GetResource(this.id),
+ RDF.GetResource(NC + "persist"),
+ true);
+ if (persistNode)
+ {
+ persistNode =
+ persistNode.QueryInterface(Ci.nsIRDFLiteral);
+ rv = persistNode.Value == 'true';
+ }
+
+ return rv;
+}
+
+sbPanel.prototype.select =
+function (force_reload)
+{
+ this.parent.select(this, force_reload);
+}
+
+sbPanel.prototype.stop_load =
+function ()
+{
+ var iframe = this.get_iframe();
+ var content = this.get_content();
+ var load_state = iframe.getAttribute('loadstate');
+ if (load_state == "loading") {
+ debug("Stop the presses");
+ iframe.removeEventListener("load", panel_loader, true);
+ content.setAttribute("loadstate", "stopped");
+ iframe.setAttribute('src', 'about:blank');
+ toggleLoadarea(content);
+ }
+}
+
+function toggleLoadarea(content)
+{
+ // toggle between "loading" and "load stopped" in the UI
+ var widgetBox = content.firstChild.firstChild;
+ var widgetBoxKids = widgetBox.childNodes;
+ var stopButton = widgetBoxKids.item(3);
+ var reloadButton = widgetBoxKids.item(4);
+ var loadingImage = widgetBox.firstChild;
+ var loadingText = loadingImage.nextSibling;
+ var loadStoppedText = loadingText.nextSibling;
+
+ // sanity check
+ if (stopButton.getAttribute("type") != "stop")
+ {
+ debug("Error: Expected button of type=\"stop\" but didn't get one!");
+ return;
+ }
+
+ if (!stopButton.hidden)
+ {
+ // change button from "stop" to "reload"
+ stopButton.hidden = "true";
+ reloadButton.removeAttribute("hidden");
+
+ // hide the loading image and set text to "load stopped"
+ loadingImage.hidden = "true";
+ loadingText.hidden = "true";
+ loadStoppedText.removeAttribute("hidden");
+ }
+ else
+ {
+ // change button from "reload" to "stop"
+ stopButton.removeAttribute("hidden");
+ reloadButton.hidden = "true";
+
+ // show the loading image and set text to "loading"
+ loadingImage.removeAttribute("hidden");
+ loadingText.removeAttribute("hidden");
+ loadStoppedText.hidden = "true";
+ }
+}
+
+sbPanel.prototype.exclude =
+function ()
+{
+ // Exclusion is handled by the datasource,
+ // but we need to make sure this panel is no longer selected.
+ this.get_header().removeAttribute('selected');
+ this.parent.exclude(this);
+}
+
+sbPanel.prototype.reload =
+function ()
+{
+ if (!this.is_excluded()) {
+ this.select(true);
+ }
+}
+
+//////////////////////////////////////////////////////////////////
+// Panels' RDF Datasource Observer
+//
+// This observer will ensure that the Sidebar UI stays current
+// when the datasource changes.
+// - When "refresh" is asserted, the sidebar refreshed.
+// Currently this happens when a panel is included/excluded or
+// added/removed (the later comes from the customize dialog).
+// - When "refresh_panel" is asserted, the targeted panel is reloaded.
+// Currently this happens when the customize panel dialog is closed.
+//////////////////////////////////////////////////////////////////
+var panel_observer = {
+ onAssert : function(ds,src,prop,target) {
+ //debug ("observer: assert");
+ // "refresh" is asserted by select menu and by customize.js.
+ if (prop == RDF.GetResource(NC + "refresh")) {
+ sidebarObj.panels.initialized = false; // reset so panels are put in view
+ sidebarObj.panels.refresh();
+ } else if (prop == RDF.GetResource(NC + "refresh_panel")) {
+ var panel_id = target.QueryInterface(Ci.nsIRDFLiteral).Value;
+ var panel = sidebarObj.panels.get_panel_from_id(panel_id);
+ panel.reload();
+ }
+ },
+ onUnassert : function(ds,src,prop,target) {
+ //debug ("observer: unassert");
+ },
+ onChange : function(ds,src,prop,old_target,new_target) {
+ //debug ("observer: change");
+ },
+ onMove : function(ds,old_src,new_src,prop,target) {
+ //debug ("observer: move");
+ },
+ onBeginUpdateBatch : function(ds) {
+ //debug ("observer: onBeginUpdateBatch");
+ },
+ onEndUpdateBatch : function(ds) {
+ //debug ("observer: onEndUpdateBatch");
+ }
+};
+
+// Use an assertion to pass a "refresh" event to all the sidebars.
+// They use observers to watch for this assertion (see above).
+function refresh_all_sidebars() {
+ sidebarObj.datasource.Assert(RDF.GetResource(sidebarObj.resource),
+ RDF.GetResource(NC + "refresh"),
+ RDF.GetLiteral("true"),
+ true);
+ sidebarObj.datasource.Unassert(RDF.GetResource(sidebarObj.resource),
+ RDF.GetResource(NC + "refresh"),
+ RDF.GetLiteral("true"));
+}
+
+//////////////////////////////////////////////////////////////
+// Sidebar Init
+//////////////////////////////////////////////////////////////
+function sidebar_overlay_init() {
+ if (sidebar_is_collapsed() && !gAboutToUncollapse)
+ return;
+ gMustInit = false;
+ sidebarObj.panels = new sbPanelList('sidebar-panels');
+ sidebarObj.datasource_uri = get_sidebar_datasource_uri();
+ sidebarObj.datasource = RDF.GetDataSourceBlocking(sidebarObj.datasource_uri);
+ sidebarObj.resource = 'urn:sidebar:current-panel-list';
+
+ sidebarObj.master_datasources = sidebarObj.datasource_uri;
+ sidebarObj.master_resource = 'urn:sidebar:master-panel-list';
+ sidebarObj.component = gPrivate ? "navigator:browser" :
+ document.documentElement.getAttribute('windowtype');
+ debug("sidebarObj.component is " + sidebarObj.component);
+
+ // Sync RDF with broadcasters.
+ SidebarBroadcastersToRDF();
+
+ // Initialize the display
+ var sidebar_element = document.getElementById('sidebar-box');
+ var sidebar_menuitem = document.getElementById('sidebar-menu');
+ if (sidebar_is_hidden()) {
+ if (sidebar_menuitem) {
+ sidebar_menuitem.setAttribute('checked', 'false');
+ }
+ } else {
+ if (sidebar_menuitem) {
+ sidebar_menuitem.setAttribute('checked', 'true');
+ }
+
+ // for old profiles that don't persist the hidden attribute when splitter is not hidden.
+ var sidebar_splitter = document.getElementById('sidebar-splitter')
+ if (sidebar_splitter)
+ sidebar_splitter.setAttribute('hidden', 'false');
+
+ if (sidebarObj.never_built) {
+ sidebarObj.never_built = false;
+
+ debug("sidebar = " + sidebarObj);
+ debug("sidebarObj.resource = " + sidebarObj.resource);
+ debug("sidebarObj.datasource_uri = " + sidebarObj.datasource_uri);
+
+ // Obtain the pref for limiting the number of tabs in view, defaults to 8.
+ gNumTabsInViewPref = Services.prefs.getIntPref("sidebar.num_tabs_in_view", 8);
+
+ // Show the header for the panels area. Use a splitter if there
+ // is stuff over the panels area.
+ var sidebar_panels_splitter = document.getElementById('sidebar-panels-splitter');
+ if (sidebar_element.firstChild != sidebar_panels_splitter) {
+ debug("Showing the panels splitter");
+ sidebar_panels_splitter.removeAttribute('hidden');
+ }
+ }
+ if (sidebar_is_collapsed()) {
+ sidebarObj.collapsed = true;
+ } else {
+ sidebarObj.collapsed = false;
+ }
+
+ sidebar_open_default_panel(100, 0);
+ }
+}
+
+function sidebar_overlay_destruct() {
+ var panels = document.getElementById('sidebar-panels');
+ debug("Removing observer from database.");
+ panels.database.RemoveObserver(panel_observer);
+}
+
+var gBusyOpeningDefault = false;
+
+function sidebar_open_default_panel(wait, tries) {
+ // check for making function reentrant
+ if (gBusyOpeningDefault)
+ return;
+ gBusyOpeningDefault = true;
+
+ var ds = sidebarObj.datasource;
+ var currentListRes = RDF.GetResource("urn:sidebar:current-panel-list");
+ var panelListRes = RDF.GetResource("http://home.netscape.com/NC-rdf#panel-list");
+ var container = ds.GetTarget(currentListRes, panelListRes, true);
+ if (container) {
+ // Add the user's current panel choices to the template builder,
+ // which will aggregate it with the other datasources that describe
+ // the individual panel's title, customize URL, and content URL.
+ var panels = document.getElementById('sidebar-panels');
+ panels.database.AddDataSource(ds);
+
+ debug("Adding observer to database.");
+ panels.database.AddObserver(panel_observer);
+
+ // XXX This is a hack to force re-display
+ panels.builder.rebuild();
+ } else {
+ if (tries < 3) {
+ // No children yet, try again later
+ setTimeout(sidebar_open_default_panel, wait, wait*2, ++tries);
+ gBusyOpeningDefault = false;
+ return;
+ } else {
+ sidebar_fixup_datasource();
+ }
+ }
+
+ sidebarObj.panels.refresh();
+ gBusyOpeningDefault = false;
+ if (gCheckMissingPanels)
+ check_for_missing_panels();
+}
+
+function SidebarRebuild() {
+ sidebarObj.panels.initialized = false; // reset so panels are brought in view
+ var panels = document.getElementById("sidebar-panels");
+ panels.builder.rebuild();
+ sidebar_open_default_panel(100, 0);
+}
+
+function check_for_missing_panels() {
+ var tabs = sidebarObj.panels.node.childNodes;
+ var currHeader;
+ var currTab;
+ for (var i = 2; i < tabs.length; i += 2) {
+ currHeader = tabs[i];
+ currTab = new sbPanel(currHeader.getAttribute("id"), currHeader, i);
+ if (!currTab.is_excluded()) {
+ if (currHeader.hasAttribute("prereq") && currHeader.getAttribute("prereq") != "") {
+ var prereq_file = currHeader.getAttribute("prereq");
+ var channel =
+ Services.io.newChannelFromURI(Services.io.newURI(prereq_file),
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ try {
+ channel.open();
+ }
+ catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ sidebarObj.datasource.Assert(RDF.GetResource(currHeader.getAttribute("id")),
+ RDF.GetResource(NC + "exclude"),
+ RDF.GetLiteral(sidebarObj.component),
+ true);
+ currTab.exclude();
+ }
+ }
+ }
+ }
+ gCheckMissingPanels = false;
+}
+
+//////////////////////////////////////////////////////////////
+// Sidebar File and Datasource functions
+//////////////////////////////////////////////////////////////
+
+function sidebar_get_panels_file() {
+ try {
+ // Use the fileLocator to look in the profile directory to find
+ // 'panels.rdf', which is the database of the user's currently
+ // selected panels.
+ // If <profile>/panels.rdf doesn't exist, GetFileLocation() will copy
+ // bin/defaults/profile/panels.rdf to <profile>/panels.rdf
+ var sidebar_file = GetSpecialDirectory(PANELS_RDF_FILE);
+ if (!sidebar_file.exists()) {
+ // This should not happen, as GetFileLocation() should copy
+ // defaults/panels.rdf to the users profile directory
+ debug("Sidebar panels file does not exist");
+ throw("Panels file does not exist");
+ }
+ return sidebar_file;
+ } catch (ex) {
+ // This should not happen
+ debug("Error: Unable to grab panels file.\n");
+ throw(ex);
+ }
+ return null;
+}
+
+function sidebar_revert_to_default_panels() {
+ try {
+ var sidebar_file = sidebar_get_panels_file();
+
+ sidebar_file.remove(false);
+
+ // Since we just removed the panels file,
+ // this should copy the defaults over.
+ sidebar_file = sidebar_get_panels_file();
+
+ debug("sidebar defaults reloaded");
+ var datasource = sidebarObj.datasource;
+ datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Refresh(true);
+ } catch (ex) {
+ debug("Error: Unable to reload panel defaults file.\n");
+ }
+ return null;
+}
+
+function get_sidebar_datasource_uri() {
+ try {
+ var sidebar_file = sidebar_get_panels_file();
+
+ var fileHandler = Services.io.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler);
+
+ return fileHandler.getURLSpecFromFile(sidebar_file);
+ } catch (ex) {
+ // This should not happen
+ debug("Error: Unable to load panels file.\n");
+ }
+ return null;
+}
+
+function sidebar_fixup_datasource() {
+ var datasource = sidebarObj.datasource;
+ var resource = RDF.GetResource(sidebarObj.resource);
+
+ var panel_list = datasource.GetTarget(resource,
+ RDF.GetResource(NC+"panel-list"),
+ true);
+ if (!panel_list) {
+ debug("Sidebar datasource is an old format or busted\n");
+ sidebar_revert_to_default_panels();
+ } else {
+ // The datasource is ok, but it just has no panels.
+ // sidebar_refresh() will display some helper content.
+ // Do nothing here.
+ }
+}
+
+//////////////////////////////////////////////////////////////
+// Sidebar Interface for XUL
+//////////////////////////////////////////////////////////////
+
+// Change the sidebar content to the selected panel.
+// Called when a panel title is clicked.
+function SidebarSelectPanel(header, should_popopen, should_unhide) {
+ debug("SidebarSelectPanel("+header+","+should_popopen+","+should_unhide+")");
+ var panel = sidebarObj.panels.get_panel_from_header_node(header);
+
+ if (!panel) {
+ return false;
+ }
+
+ var popopen = false;
+ var unhide = false;
+
+ if (panel.is_excluded()) {
+ return false;
+ }
+ if (sidebar_is_hidden()) {
+ if (should_unhide) {
+ unhide = true;
+ } else {
+ return false;
+ }
+ }
+ if (sidebar_is_collapsed()) {
+ if (should_popopen) {
+ popopen = true;
+ } else {
+ return false;
+ }
+ }
+ if (unhide) SidebarShowHide();
+ if (popopen) SidebarExpandCollapse();
+
+ try {
+ panel.get_iframe().setAttribute('focusOnLoad', true);
+ } catch (ex) {
+ // ignore exception for cases where content isn't built yet
+ // e.g., auto opening search tab: we don't want to focus search field
+ }
+ if (!panel.is_selected()) panel.select(false);
+
+ return true;
+}
+
+function SidebarGetLastSelectedPanel()
+{
+ return (sidebarObj.panels &&
+ sidebarObj.panels.node.getAttribute('last-selected-panel'));
+}
+
+function SidebarGetRelativePanel(direction)
+{
+ // direction == 1 to view next panel, -1 to view prev panel
+
+ if (sidebar_is_hidden())
+ SidebarShowHide();
+ if (sidebar_is_collapsed())
+ SidebarExpandCollapse();
+
+ var currentPanel = sidebarObj.panels.get_panel_from_id(SidebarGetLastSelectedPanel());
+ if (!currentPanel) {
+ sidebarObj.panels.select_default_panel();
+ return;
+ }
+
+ var newPanel = currentPanel;
+
+ do {
+ var newPanelIndex = newPanel.index + (direction * 2);
+ if (newPanelIndex < 2 || newPanelIndex >= sidebarObj.panels.node.childNodes.length)
+ newPanel = (direction == 1)? sidebarObj.panels.find_first(): sidebarObj.panels.find_last();
+ else
+ newPanel = sidebarObj.panels.get_panel_from_header_index(newPanelIndex);
+
+ if (!newPanel)
+ break;
+
+ if (!newPanel.is_excluded()) {
+ SidebarSelectPanel(newPanel.header, true, true); // found a panel that's not excluded to select -- do it
+ break;
+ }
+ } while (newPanel != currentPanel); // keep looking for a panel, but don't loop infinitely
+}
+
+function SidebarStopPanelLoad(header) {
+ var panel = sidebarObj.panels.get_panel_from_header_node(header);
+ panel.stop_load();
+}
+
+function SidebarReloadPanel(header) {
+ var panel = sidebarObj.panels.get_panel_from_header_node(header);
+ panel.reload();
+}
+
+// No one is calling this right now.
+function SidebarReload() {
+ sidebarObj.panels.refresh();
+}
+
+// Set up a lame hack to avoid opening two customize
+// windows on a double click.
+var gDisableCustomize = false;
+function enable_customize() {
+ gDisableCustomize = false;
+}
+
+// Bring up the Sidebar customize dialog.
+function SidebarCustomize() {
+ // Use a single sidebar customize dialog
+ var customizeWindow = Services.wm.getMostRecentWindow('sidebar:customize');
+
+ if (customizeWindow) {
+ debug("Reuse existing customize dialog");
+ customizeWindow.focus();
+ } else {
+ debug("Open a new customize dialog");
+
+ if (false == gDisableCustomize) {
+ debug("First time creating customize dialog");
+ gDisableCustomize = true;
+
+ var panels = document.getElementById('sidebar-panels');
+
+ customizeWindow = window.openDialog(
+ 'chrome://communicator/content/sidebar/customize.xul',
+ '_blank','centerscreen,chrome,resizable,dialog=no,dependent',
+ sidebarObj.master_datasources,
+ sidebarObj.master_resource,
+ sidebarObj.datasource_uri,
+ sidebarObj.resource);
+ setTimeout(enable_customize, 2000);
+ }
+ }
+}
+
+function BrowseMorePanels()
+{
+ var url = '';
+ var browser_url = "chrome://navigator/content/navigator.xul";
+ var locale;
+ try {
+ url = Services.prefs.getCharPref("sidebar.customize.directory.url");
+ var temp = Services.prefs.getCharPref("browser.chromeURL");
+ if (temp)
+ browser_url = temp;
+ } catch(ex) {
+ debug("Unable to get prefs: "+ex);
+ }
+ window.openDialog(browser_url, "_blank", "chrome,all,dialog=no", url);
+}
+
+
+
+function sidebar_is_collapsed() {
+ var sidebar_splitter = document.getElementById('sidebar-splitter');
+ return (sidebar_splitter &&
+ sidebar_splitter.getAttribute('state') == 'collapsed');
+}
+
+function SidebarExpandCollapse() {
+ var sidebar_splitter = document.getElementById('sidebar-splitter');
+ var sidebar_box = document.getElementById('sidebar-box');
+ if (sidebar_splitter.getAttribute('state') == 'collapsed') {
+ if (gMustInit)
+ sidebar_overlay_init();
+ debug("Expanding the sidebar");
+ sidebar_splitter.removeAttribute('state');
+ sidebar_box.removeAttribute('collapsed');
+ SidebarSetButtonOpen(true);
+ } else {
+ debug("Collapsing the sidebar");
+ sidebar_splitter.setAttribute('state', 'collapsed');
+ sidebar_box.setAttribute('collapsed', 'true');
+ SidebarSetButtonOpen(false);
+ }
+}
+
+// sidebar_is_hidden() - Helper function for SidebarShowHide().
+function sidebar_is_hidden() {
+ var sidebar_title = document.getElementById('sidebar-title-box');
+ var sidebar_box = document.getElementById('sidebar-box');
+ return sidebar_box.getAttribute('hidden') == 'true'
+ || sidebar_title.getAttribute('hidden') == 'true';
+}
+
+// Show/Hide the entire sidebar.
+// Invoked by the "View / Sidebar" menu option.
+function SidebarShowHide() {
+ var sidebar_box = document.getElementById('sidebar-box');
+ var title_box = document.getElementById('sidebar-title-box');
+ var sidebar_panels_splitter = document.getElementById('sidebar-panels-splitter');
+ var sidebar_panels_splitter_box = document.getElementById('sidebar-panels-splitter-box');
+ var sidebar_splitter = document.getElementById('sidebar-splitter');
+ var sidebar_menu_item = document.getElementById('sidebar-menu');
+ var tabs_menu = document.getElementById('sidebar-panel-picker');
+
+ if (sidebar_is_hidden()) {
+ debug("Showing the sidebar");
+
+ // for older profiles:
+ sidebar_box.setAttribute('hidden', 'false');
+ sidebar_panels_splitter_box.setAttribute('hidden', 'false');
+
+ sidebar_box.removeAttribute('collapsed');
+ if (sidebar_splitter.getAttribute('state') == 'collapsed')
+ sidebar_splitter.removeAttribute('state');
+ title_box.removeAttribute('hidden');
+ sidebar_panels_splitter_box.removeAttribute('collapsed');
+ sidebar_splitter.setAttribute('hidden', 'false');
+ if (sidebar_box.firstChild != sidebar_panels_splitter) {
+ debug("Showing the panels splitter");
+ sidebar_panels_splitter.removeAttribute('hidden');
+ if (sidebar_panels_splitter.getAttribute('state') == 'collapsed')
+ sidebar_panels_splitter.removeAttribute('state');
+ }
+ sidebar_overlay_init();
+ sidebar_menu_item.setAttribute('checked', 'true');
+ tabs_menu.removeAttribute('hidden');
+ SidebarSetButtonOpen(true);
+ } else {
+ debug("Hiding the sidebar");
+ var hide_everything = sidebar_panels_splitter.getAttribute('hidden') == 'true';
+ if (hide_everything) {
+ debug("Hide everything");
+ sidebar_box.setAttribute('collapsed', 'true');
+ sidebar_splitter.setAttribute('hidden', 'true');
+ } else {
+ sidebar_panels_splitter.setAttribute('hidden', 'true');
+ }
+ title_box.setAttribute('hidden', 'true');
+ sidebar_panels_splitter_box.setAttribute('collapsed', 'true');
+ sidebar_menu_item.setAttribute('checked', 'false');
+ tabs_menu.setAttribute('hidden', 'true');
+ SidebarSetButtonOpen(false);
+ }
+ // Immediately save persistent values
+ document.persist('sidebar-title-box', 'hidden');
+ PersistWidth();
+ window.content.focus();
+}
+
+function SidebarGetState() {
+ if (sidebar_is_hidden())
+ return "hidden";
+ if (sidebar_is_collapsed())
+ return "collapsed";
+ return "visible";
+}
+
+function SidebarSetState(aState) {
+ document.getElementById("sidebar-box").hidden = aState != "visible";
+ document.getElementById("sidebar-splitter").hidden = aState == "hidden";
+}
+
+function SidebarBuildPickerPopup() {
+ var menu = document.getElementById('sidebar-panel-picker-popup');
+ menu.database.AddDataSource(sidebarObj.datasource);
+ menu.builder.rebuild();
+
+ for (var ii=3; ii < menu.childNodes.length; ii++) {
+ var panel_menuitem = menu.childNodes.item(ii);
+ if (sb_panel_is_excluded(panel_menuitem)) {
+ debug(ii+": "+panel_menuitem.getAttribute('label')+ ": excluded; uncheck.");
+ panel_menuitem.removeAttribute('checked');
+ } else {
+ debug(ii+": "+panel_menuitem.getAttribute('label')+ ": included; check.");
+ panel_menuitem.setAttribute('checked', 'true');
+ }
+ }
+}
+
+function SidebarTogglePanel(panel_menuitem) {
+ if (!panel_menuitem.classList.contains("menuitem-sidebar") &&
+ !panel_menuitem.classList.contains("texttab-sidebar"))
+ return;
+
+ // Create a "container" wrapper around the current panels to
+ // manipulate the RDF:Seq more easily.
+
+ var did_exclude = false;
+ var panel_id = panel_menuitem.getAttribute('id');
+ var panel = sidebarObj.panels.get_panel_from_id(panel_id);
+ var panel_exclude = panel_menuitem.getAttribute('exclude')
+ if (panel_exclude == '') {
+ // Nothing excluded for this panel yet, so add this component to the list.
+ debug("Excluding " + panel_id + " from " + sidebarObj.component);
+ sidebarObj.datasource.Assert(RDF.GetResource(panel_id),
+ RDF.GetResource(NC + "exclude"),
+ RDF.GetLiteral(sidebarObj.component),
+ true);
+ panel.exclude();
+ did_exclude = true;
+ } else {
+ // Panel has an exclude string, but it may or may not have the
+ // current component listed in the string.
+ debug("Current exclude string: " + panel_exclude);
+ var new_exclude = panel_exclude;
+ if (sb_panel_is_excluded(panel_menuitem)) {
+ debug("Plucking this component out of the exclude list");
+ var replace_pat = new RegExp(sidebarObj.component + "\s*");
+ new_exclude = new_exclude.replace(replace_pat, "").trimLeft();
+ // did_exclude remains false
+ } else {
+ debug("Adding this component to the exclude list");
+ new_exclude = new_exclude + " " + sidebarObj.component;
+ panel.exclude();
+ did_exclude = true;
+ }
+ if (new_exclude == '') {
+ debug("Removing exclude list");
+ sidebarObj.datasource.Unassert(RDF.GetResource(panel_id),
+ RDF.GetResource(NC + "exclude"),
+ RDF.GetLiteral(sidebarObj.component));
+ } else {
+ debug("New exclude string: " + new_exclude);
+ exclude_target =
+ sidebarObj.datasource.GetTarget(RDF.GetResource(panel_id),
+ RDF.GetResource(NC + "exclude"),
+ true);
+ sidebarObj.datasource.Change(RDF.GetResource(panel_id),
+ RDF.GetResource(NC + "exclude"),
+ exclude_target,
+ RDF.GetLiteral(new_exclude));
+ }
+ }
+
+ var tabs = sidebarObj.panels.node.childNodes;
+
+ if (did_exclude)
+ {
+ // if we excluded a tab in view then add another one
+ if (panel.is_in_view())
+ {
+ // we excluded one so let's try to bring a non-excluded one into view
+ var newFirst = null;
+ var added = false;
+ for (var i = 2; i < tabs.length ; i += 2)
+ {
+ var currTab = sidebarObj.panels.get_panel_from_header_index(i);
+ var hasPotential = !currTab.is_excluded() && !currTab.is_in_view();
+
+ // set potential new first tab in case we can't find one after the
+ // tab that was just excluded
+ if (!newFirst && hasPotential)
+ newFirst = currTab;
+
+ if (i > panel.index && hasPotential)
+ {
+ currTab.header.setAttribute("in-view", true);
+ added = true;
+ break;
+ }
+ }
+ if (!added && newFirst)
+ newFirst.header.setAttribute("in-view", true);
+
+ // lose it from current view
+ panel.header.setAttribute("in-view", false);
+ }
+ }
+ else
+ {
+ panel.header.setAttribute("in-view", true);
+
+ // if we have one too many tabs we better get rid of an old one
+ if (sidebarObj.panels.num_panels_in_view() > gNumTabsInViewPref)
+ {
+ // we included a new tab so let's take the last one out of view
+ for (i = 2; i < tabs.length; i += 2)
+ {
+ var currHeader = tabs[i];
+ if (currHeader.hasAttribute("last-panel"))
+ currHeader.setAttribute("in-view", false);
+ }
+ }
+
+ panel.select(false);
+ }
+
+ if (did_exclude && !sidebarObj.panels.visible_panels_exist())
+ // surrender focus to main content area
+ window.content.focus();
+ else
+ // force all the sidebars to update
+ refresh_all_sidebars();
+
+ // Write the modified panels out.
+ sidebarObj.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+}
+
+function SidebarNavigate(aDirection)
+{
+ debug("SidebarNavigate " + aDirection);
+
+ var tabs = sidebarObj.panels.node.childNodes;
+ var i;
+ var currHeader;
+ var currTab;
+ // move forward a tab (down in the template)
+ if (aDirection > 0)
+ {
+ // ensure we have a tab below the last one
+ var foundLast = false;
+ var oldFirst = null;
+ for (i = 2; i < tabs.length; i += 2)
+ {
+ currHeader = tabs[i];
+ currTab = new sbPanel(currHeader.getAttribute("id"), currHeader, i);
+
+ if (!currTab.is_excluded())
+ {
+ if (foundLast)
+ {
+ debug("toggling old first and new last");
+ debug("new last: " + currHeader.getAttribute("id"));
+ debug("old first: " + oldFirst.getAttribute("id"));
+ currHeader.setAttribute("in-view", true);
+ oldFirst.setAttribute("in-view", false);
+
+ // if old first was selected select new first instead
+ if (oldFirst.getAttribute("id") ==
+ sidebarObj.panels.node.getAttribute("last-selected-panel"))
+ {
+ sidebarObj.panels.node.setAttribute('last-selected-panel',
+ currTab.id);
+ }
+
+ break;
+ }
+
+ if (!foundLast && currHeader.hasAttribute("last-panel"))
+ {
+ debug("found last");
+ foundLast = true;
+ }
+
+ // set the old first in case we find a new last below
+ // the old last and need to toggle the new first's ``in-view''
+ if (!oldFirst && currTab.is_in_view())
+ oldFirst = currHeader;
+ }
+ }
+ }
+
+ // move back a tab (up in the template)
+ else if (aDirection < 0)
+ {
+ var newFirst = null, newLast = null;
+ var foundFirst = false;
+ for (i = 2; i < tabs.length; i += 2)
+ {
+ currHeader = tabs[i];
+ currTab = new sbPanel(currHeader.getAttribute("id"), currHeader, i);
+
+ if (!currTab.is_excluded())
+ {
+ if (!foundFirst && currHeader.hasAttribute("top-panel"))
+ {
+ debug("found first");
+ foundFirst = true;
+ }
+ if (!foundFirst)
+ {
+ debug("setting newFirst");
+ newFirst = currHeader;
+ }
+
+ if (currHeader.hasAttribute("last-panel"))
+ {
+ debug("found last");
+
+ // ensure we have a tab above the first one
+ if (newFirst)
+ {
+ debug("toggling new first and old last");
+ debug("new first: " + newFirst.getAttribute("id"));
+ debug("old last: " + currHeader.getAttribute("id"));
+
+ newFirst.setAttribute("in-view", true);
+ currHeader.setAttribute("in-view", false); // hide old last
+
+ // if old last was selected, now select one above it
+ if (sidebarObj.panels.node.getAttribute("last-selected-panel") ==
+ currTab.id)
+ {
+ sidebarObj.panels.node.setAttribute("last-selected-panel",
+ newLast.getAttribute("id"));
+ }
+
+ break;
+ }
+ }
+ if (currTab.is_in_view())
+ newLast = currHeader;
+ }
+ }
+ }
+
+ if (aDirection)
+ sidebarObj.panels.update(false);
+}
+
+//////////////////////////////////////////////////////////////
+// Sidebar Hacks and Work-arounds
+//////////////////////////////////////////////////////////////
+
+// SidebarCleanUpExpandCollapse() - Respond to grippy click.
+function SidebarCleanUpExpandCollapse() {
+ // XXX Mini hack. Persist isn't working too well. Force the persist,
+ // but wait until the change has commited.
+ if (gMustInit) {
+ gAboutToUncollapse = true;
+ sidebar_overlay_init();
+ }
+
+ setTimeout(Persist, 100, "sidebar-box", "collapsed");
+ setTimeout(() => sidebarObj.panels.refresh(), 100);
+}
+
+function PersistHeight() {
+ // XXX Mini hack. Persist isn't working too well. Force the persist,
+ // but wait until the last drag has been committed.
+ // May want to do something smarter here like only force it if the
+ // height has really changed.
+ setTimeout(Persist, 100, "sidebar-panels-splitter-box", "height");
+}
+
+function PersistWidth() {
+ // XXX Mini hack. Persist isn't working too well. Force the persist,
+ // but wait until the width change has commited. Also see bug 16516.
+ setTimeout(Persist, 100, "sidebar-box", "width");
+
+ var is_collapsed = document.getElementById("sidebar-box")
+ .getAttribute("collapsed") == "true";
+ SidebarSetButtonOpen(!is_collapsed);
+}
+
+function Persist(aAttribute, aValue) {
+ document.persist(aAttribute, aValue);
+}
+
+function SidebarFinishClick() {
+ PersistWidth();
+
+ var is_collapsed = document.getElementById('sidebar-box').getAttribute('collapsed') == 'true';
+ debug("collapsed: " + is_collapsed);
+ if (is_collapsed != sidebarObj.collapsed) {
+ if (gMustInit)
+ sidebar_overlay_init();
+ }
+}
+
+function SidebarSetButtonOpen(aSidebarNowOpen)
+{
+ // change state so toolbar icon can be updated
+ var pt = document.getElementById("PersonalToolbar");
+ if (pt) {
+ pt.setAttribute("prefixopen", aSidebarNowOpen);
+
+ // set tooltip for toolbar icon
+ var header = document.getElementById("sidebar-title-box");
+ var tooltip = header.getAttribute(aSidebarNowOpen ?
+ "tooltipclose" : "tooltipopen");
+ pt.setAttribute("prefixtooltip", tooltip);
+ }
+}
+
+function SidebarInitContextMenu(aMenu, aPopupNode)
+{
+ var panel = sidebarObj.panels.get_panel_from_header_node(aPopupNode);
+ var switchItem = document.getElementById("switch-ctx-item");
+ var reloadItem = document.getElementById("reload-ctx-item");
+ var stopItem = document.getElementById("stop-ctx-item");
+
+ // the current panel can be reloaded, but other panels are not showing
+ // any content, so we only allow you to switch to other panels
+ if (panel.is_selected())
+ {
+ switchItem.setAttribute("collapsed", "true");
+ reloadItem.removeAttribute("disabled");
+ }
+ else
+ {
+ switchItem.removeAttribute("collapsed");
+ reloadItem.setAttribute("disabled", "true");
+ }
+
+ // only if a panel is currently loading enable the ``Stop'' item
+ if (panel.get_iframe().getAttribute("loadstate") == "loading")
+ stopItem.removeAttribute("disabled");
+ else
+ stopItem.setAttribute("disabled", "true");
+}
+
+///////////////////////////////////////////////////////////////
+// Handy Debug Tools
+//////////////////////////////////////////////////////////////
+var debug = null;
+var dump_attributes = null;
+var dump_tree = null;
+if (!SB_DEBUG) {
+ debug = function (s) {};
+ dump_attributes = function (node, depth) {};
+ dump_tree = function (node) {};
+ var _dump_tree_recur = function (node, depth, index) {};
+} else {
+ debug = function (s) { dump("-*- sbOverlay: " + s + "\n"); };
+
+ dump_attributes = function (node, depth) {
+ var attributes = node.attributes;
+ var indent = "| | | | | | | | | | | | | | | | | | | | | | | | | | | | . ";
+
+ if (!attributes || attributes.length == 0) {
+ debug(indent.substr(indent.length - depth*2) + "no attributes");
+ }
+ for (var ii=0; ii < attributes.length; ii++) {
+ var attr = attributes.item(ii);
+ debug(indent.substr(indent.length - depth*2) + attr.name +
+ "=" + attr.value);
+ }
+ }
+ dump_tree = function (node) {
+ _dump_tree_recur(node, 0, 0);
+ }
+ _dump_tree_recur = function (node, depth, index) {
+ if (!node) {
+ debug("dump_tree: node is null");
+ }
+ var indent = "| | | | | | | | | | | | | | | | | | | | | | | | | | | | + ";
+ debug(indent.substr(indent.length - depth*2) + index +
+ " " + node.nodeName);
+ if (node.nodeType != Node.TEXT_NODE) {
+ dump_attributes(node, depth);
+ }
+ var kids = node.childNodes;
+ for (var ii=0; ii < kids.length; ii++) {
+ _dump_tree_recur(kids[ii], depth + 1, ii);
+ }
+ }
+}
+
+function SidebarBroadcastersToRDF()
+{
+ // Only the broadcasters in browser are synced to panels.rdf
+ if (sidebarObj.component != "navigator:browser")
+ return;
+
+ // Translation rules to translate between new broadcaster id and old RDF id.
+ const TRANSLATE = {viewBookmarksSidebar: "bookmarks",
+ viewHistorySidebar: "history",
+ viewSearchSidebar: "search",
+ viewAddressbookSidebar: "addressbook"};
+ const URN_PREFIX = "urn:sidebar:panel:";
+
+ const RDFCU = Cc['@mozilla.org/rdf/container-utils;1']
+ .getService(Ci.nsIRDFContainerUtils);
+
+ /*
+ * Initialize RDF stuff.
+ */
+ let ds = sidebarObj.datasource;
+ let panelListRes = RDF.GetResource(NC + "panel-list");
+
+ let currentListRes = RDF.GetResource(sidebarObj.resource);
+ let masterListRes = RDF.GetResource(sidebarObj.master_resource);
+ let currentTarget = ds.GetTarget(currentListRes, panelListRes, true);
+ let masterTarget = ds.GetTarget(masterListRes, panelListRes, true);
+ if (!masterTarget) {
+ // No "master-panel-list" found, so create it.
+ masterTarget = RDF.GetAnonymousResource();
+ ds.Assert(masterListRes, panelListRes, masterTarget, true);
+ }
+ let currentSeq = RDFCU.MakeSeq(ds, currentTarget);
+ let masterSeq = RDFCU.MakeSeq(ds, masterTarget);
+
+ /*
+ * Run over broadcasters in browser window and add/update RDF entries
+ * based on them.
+ */
+ let titleRes = RDF.GetResource(NC + "title");
+ let urlRes = RDF.GetResource(NC + "content");
+
+ let bset = document.getElementById("mainBroadcasterSet");
+ let broadcasters = bset.getElementsByTagName("broadcaster");
+ let bclist = {};
+ for (let bId = 0; bId < broadcasters.length; bId++) {
+ let curBC = broadcasters[bId];
+ let title = curBC.getAttribute("sidebartitle") || curBC.getAttribute("label");
+ let url = curBC.getAttribute("sidebarurl");
+ let bcid = (curBC.id in TRANSLATE) ? TRANSLATE[curBC.id] : curBC.id;
+
+ if (!url || !title || !bcid)
+ continue;
+
+ // This one is needed later to check for obsolete sidebars.
+ bclist[bcid] = 1;
+
+ let panelRes = RDF.GetResource(URN_PREFIX + bcid);
+
+ // Literals of values that should be in RDF.
+ let titleLit = RDF.GetLiteral(title);
+ let urlLit = RDF.GetLiteral(url);
+ // Literals of values that are in RDF.
+ let curtitleLit = ds.GetTarget(panelRes, titleRes, true);
+ let cururlLit = ds.GetTarget(panelRes, urlRes, true);
+
+ // If the item doesn't already exist, create it.
+ if (!curtitleLit && !cururlLit) {
+ ds.Assert(panelRes, titleRes, titleLit, true);
+ ds.Assert(panelRes, urlRes, urlLit, true);
+ masterSeq.AppendElement(panelRes);
+ if (currentSeq.IndexOf(panelRes) == -1)
+ currentSeq.AppendElement(panelRes);
+ }
+ // Item already exists, but perhaps we need to update...
+ else {
+ let curtitle = curtitleLit.QueryInterface(Ci.nsIRDFLiteral).Value;
+ let cururl = cururlLit.QueryInterface(Ci.nsIRDFLiteral).Value;
+
+ if (curtitle != title)
+ ds.Change(panelRes, titleRes, curtitleLit, titleLit);
+
+ if (cururl != url)
+ ds.Change(panelRes, urlRes, cururlLit, urlLit);
+ }
+ }
+
+ /*
+ * Do the same the other way around to delete obsolete sidebars.
+ */
+
+ let masterElements = masterSeq.GetElements();
+ while (masterElements.hasMoreElements()) {
+ let curElementRes = masterElements.getNext();
+ let curId = curElementRes.QueryInterface(Ci.nsIRDFResource).Value;
+
+ if (curId.substr(0, URN_PREFIX.length) != URN_PREFIX)
+ continue;
+
+ curId = curId.substr(URN_PREFIX.length);
+ if (!(curId in bclist)) {
+ let properties = ds.ArcLabelsOut(curElementRes);
+ while(properties.hasMoreElements()) {
+ let propertyRes = properties.getNext();
+ let valueLit = ds.GetTarget(curElementRes, propertyRes, true);
+ ds.Unassert(curElementRes, propertyRes, valueLit);
+ }
+ masterSeq.RemoveElement(curElementRes, true);
+ if (currentSeq.IndexOf(curElementRes) != -1)
+ currentSeq.RemoveElement(curElementRes, true);
+ }
+ }
+
+ // Write modified data.
+ sidebarObj.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+}
+
+
+//////////////////////////////////////////////////////////////
+// Install the load/unload handlers
+//////////////////////////////////////////////////////////////
+addEventListener("load", sidebar_overlay_init, false);
+addEventListener("unload", sidebar_overlay_destruct, false);
diff --git a/comm/suite/components/sidebar/content/sidebarOverlay.xul b/comm/suite/components/sidebar/content/sidebarOverlay.xul
new file mode 100644
index 0000000000..0c1aa08566
--- /dev/null
+++ b/comm/suite/components/sidebar/content/sidebarOverlay.xul
@@ -0,0 +1,247 @@
+<?xml version="1.0"?> <!-- -*- Mode: HTML; indent-tabs-mode: nil -*- -->
+<!--
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This overlay requires that the files it overlays has the menupopup
+ contentAreaContextMenu defined for context menus to work correctly in
+ certain custom tabs -->
+
+<?xml-stylesheet href="chrome://communicator/content/sidebar/sidebarOverlay.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sidebar/sidebar.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+<!ENTITY % sidebarOverlayDTD SYSTEM "chrome://communicator/locale/sidebar/sidebarOverlay.dtd" >
+%sidebarOverlayDTD;
+]>
+
+<overlay id="sidebarOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Overlay of broadcasterset to get our panels in -->
+ <broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="viewBookmarksSidebar"
+ autoCheck="false"
+ type="checkbox"
+ group="sidebar"
+ sidebartitle="&sidebar.client-bookmarks.label;"
+ sidebarurl="chrome://communicator/content/bookmarks/bookmarksPanel.xul"
+ oncommand="toggleSidebar('viewBookmarksSidebar');"/>
+ <broadcaster id="viewHistorySidebar"
+ autoCheck="false"
+ type="checkbox"
+ group="sidebar"
+ sidebartitle="&sidebar.client-history.label;"
+ sidebarurl="chrome://communicator/content/history/history-panel.xul"
+ oncommand="toggleSidebar('viewHistorySidebar');"/>
+ <broadcaster id="viewSearchSidebar"
+ autoCheck="false"
+ type="checkbox"
+ group="sidebar"
+ sidebartitle="&sidebar.search.label;"
+ sidebarurl="chrome://communicator/content/search/search-panel.xul"
+ oncommand="toggleSidebar('viewSearchSidebar');"/>
+ <broadcaster id="viewAddressbookSidebar"
+ autoCheck="false"
+ type="checkbox"
+ group="sidebar"
+ sidebartitle="&sidebar.client-addressbook.label;"
+ sidebarurl="chrome://messenger/content/addressbook/addressbook-panel.xul"
+ oncommand="toggleSidebar('viewAddressbookSidebar');"/>
+ </broadcasterset>
+
+ <command id="toggleSidebar" oncommand="SidebarShowHide();"/>
+#ifndef XP_MACOSX
+ <key id="showHideSidebar"
+ keycode="VK_F9"
+ command="toggleSidebar"/>
+#else
+ <key id="showHideSidebar"
+ key="&showHideSidebarCmd.key;"
+ modifiers="accel,alt"
+ command="toggleSidebar"/>
+#endif
+ <menupopup id="sidebarPopup"
+ onpopupshowing="SidebarInitContextMenu(this, document.popupNode);">
+ <menuitem id="switch-ctx-item" label="&sidebar.switch.label;"
+ accesskey="&sidebar.switch.accesskey;" default="true"
+ oncommand="SidebarSelectPanel(document.popupNode,false,false);"/>
+ <menuitem id="reload-ctx-item" label="&sidebar.reload.label;"
+ accesskey="&sidebar.reload.accesskey;" disabled="true"
+ oncommand="SidebarReloadPanel(document.popupNode);"/>
+ <menuitem id="stop-ctx-item" label="&sidebar.loading.stop.label;"
+ accesskey="&sidebar.loading.stop.accesskey;" disabled="true"
+ oncommand="SidebarStopPanelLoad(document.popupNode);"/>
+ <menuseparator/>
+ <menuitem id="hide-ctx-item" label="&sidebar.hide.label;"
+ accesskey="&sidebar.hide.accesskey;"
+ oncommand="SidebarTogglePanel(document.popupNode);"/>
+ <menuseparator/>
+ <menuitem id="customize-ctx-item" label="&sidebar.customize.label;"
+ accesskey="&sidebar.customize.accesskey;"
+ oncommand="SidebarCustomize();"/>
+ </menupopup>
+
+ <!-- Overlay the sidebar panels -->
+ <vbox id="sidebar-box" hidden="true" persist="hidden width collapsed">
+ <splitter id="sidebar-panels-splitter" collapse="after" persist="state"
+ onmouseup="PersistHeight();" hidden="true">
+ <grippy/>
+ </splitter>
+ <vbox id="sidebar-panels-splitter-box" flex="1"
+ persist="collapsed">
+ <sidebarheader id="sidebar-title-box" class="sidebarheader-main"
+ label="&sidebar.panels.label;" persist="hidden" type="box"
+ collapse="after" onmouseup="PersistHeight();"
+ tooltipopen="&sidebar.open.tooltip;"
+ tooltipclose="&sidebar.close.tooltip;">
+ <toolbarbutton type="menu" id="sidebar-panel-picker" class="tabbable"
+ onpopupshowing="SidebarBuildPickerPopup();"
+ label="&sidebar.picker.label;" >
+ <menupopup id="sidebar-panel-picker-popup"
+ datasources="rdf:null"
+ ref="urn:sidebar:current-panel-list"
+ oncommand="SidebarTogglePanel(event.target);" >
+ <template>
+ <rule>
+ <conditions>
+ <content uri="?uri"/>
+ <triple subject="?uri"
+ predicate="http://home.netscape.com/NC-rdf#panel-list"
+ object="?panel-list"/>
+ <member container="?panel-list" child="?panel"/>
+ <triple subject="?panel"
+ predicate="http://home.netscape.com/NC-rdf#title"
+ object="?title" />
+ </conditions>
+ <bindings>
+ <binding subject="?panel"
+ predicate="http://home.netscape.com/NC-rdf#exclude"
+ object="?exclude"/>
+ <binding subject="?panel"
+ predicate="http://home.netscape.com/NC-rdf#prereq"
+ object="?prereq"/>
+ </bindings>
+ <action>
+ <menuitem uri="?panel" type="checkbox" class="menuitem-sidebar"
+ label="?title" exclude="?exclude" prereq="?prereq"/>
+ </action>
+ </rule>
+ </template>
+ <menuitem label="&sidebar.customize.label;" accesskey="&sidebar.customize.accesskey;"
+ oncommand="SidebarCustomize();" />
+ <menuitem label="&sidebar.sbDirectory.label;"
+ oncommand="BrowseMorePanels();" />
+ <menuseparator />
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="sidebar-close-button" oncommand="SidebarShowHide();"
+ tooltiptext="&sidebar.close.tooltip;"/>
+ </sidebarheader>
+
+ <vbox id="sidebar-panels"
+ datasources="rdf:null"
+ ref="urn:sidebar:current-panel-list"
+ last-selected-panel="urn:sidebar:panel:bookmarks"
+ persist="last-selected-panel height collapsed" flex="1"
+ onclick="return contentAreaClick(event);">
+ <template id="sidebar-template">
+ <rule>
+ <conditions>
+ <content uri="?uri"/>
+ <triple subject="?uri" object="?panel-list"
+ predicate="http://home.netscape.com/NC-rdf#panel-list" />
+ <member container="?panel-list" child="?panel"/>
+ <triple subject="?panel" object="?title"
+ predicate="http://home.netscape.com/NC-rdf#title" />
+ <triple subject="?panel" object="?content"
+ predicate="http://home.netscape.com/NC-rdf#content" />
+ </conditions>
+ <bindings>
+ <binding subject="?panel" object="?exclude"
+ predicate="http://home.netscape.com/NC-rdf#exclude" />
+ <binding subject="?panel" object="?prereq"
+ predicate="http://home.netscape.com/NC-rdf#prereq" />
+ </bindings>
+ <action>
+ <hbox uri="?panel" class="box-texttab texttab-sidebar"
+ oncommand="SidebarSelectPanel(this,false,false)"
+ hidden="true" label="?title" exclude="?exclude"
+ prereq="?prereq" context="sidebarPopup"/>
+ <vbox uri="?panel" flex="1" hidden="true"
+ loadstate="never loaded">
+ <vbox flex="1" class="iframe-panel loadarea">
+ <hbox flex="1" align="center">
+ <image class="image-panel-loading"/>
+ <label class="text-panel-loading"
+ value="&sidebar.loading.label;"/>
+ <label class="text-panel-loading" hidden="true"
+ loading="false"
+ value="&sidebar.loadstopped.label;"/>
+ <button type="stop" label="&sidebar.loading.stop.label;"
+ oncommand="SidebarStopPanelLoad(this.parentNode.parentNode.parentNode.previousSibling);"/>
+ <button label="&sidebar.reload.label;" hidden="true"
+ oncommand="SidebarReload();"/>
+ </hbox>
+ <spacer flex="100%"/>
+ </vbox>
+ <notificationbox flex="1" collapsed="true" class="sidebar-notificationbox browser-notificationbox">
+ <browser flex="1" class="browser-sidebar" src="about:blank"
+ hidden="true" collapsed="true" content="?content"
+ disablehistory="true"/>
+ <browser flex="1" class="browser-sidebar" src="about:blank"
+ hidden="true" collapsed="true" content="?content"
+ type="content" context="contentAreaContextMenu"
+ disablehistory="true" tooltip="aHTMLTooltip"/>
+ </notificationbox>
+ </vbox>
+ </action>
+ </rule>
+ </template>
+ <vbox id="sidebar-iframe-no-panels" class="iframe-panel" flex="1"
+ hidden="true">
+ <description>&sidebar.no-panels.state;</description>
+ <description>&sidebar.no-panels.add;</description>
+ <description>&sidebar.no-panels.hide;</description>
+ </vbox>
+ </vbox>
+ <vbox flex="0">
+ <hbox id="nav-buttons-box" hidden="true">
+ <toolbarbutton flex="1" pack="center"
+ class="sidebar-nav-button tab-fwd" onclick="SidebarNavigate(-1);"/>
+ <toolbarbutton flex="1" pack="center"
+ class="sidebar-nav-button tab-back" onclick="SidebarNavigate(1);"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+
+ <!-- Splitter on the right of sidebar -->
+ <splitter id="sidebar-splitter" collapse="before" persist="state hidden"
+ class="chromeclass-extrachrome sidebar-splitter" align="center"
+ hidden="true" onmouseup="SidebarFinishClick();">
+ <grippy class="sidebar-splitter-grippy"
+ onclick="SidebarCleanUpExpandCollapse();"/>
+ </splitter>
+
+ <!-- View->Sidebar toggle -->
+ <menupopup id="menu_View_Popup">
+ <menu id="menu_Toolbars">
+ <menupopup id="view_toolbars_popup">
+ <menuseparator/>
+ <menuitem id="sidebar-menu" type="checkbox"
+ label="&sidebarCmd.label;"
+ accesskey="&sidebarCmd.accesskey;"
+ command="toggleSidebar"
+ key="showHideSidebar"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <!-- Scripts go last, because they peek at state to tweak menus -->
+ <script src="chrome://communicator/content/sidebar/sidebarOverlay.js"/>
+
+</overlay>
+
diff --git a/comm/suite/components/sidebar/jar.mn b/comm/suite/components/sidebar/jar.mn
new file mode 100644
index 0000000000..d6ab848a98
--- /dev/null
+++ b/comm/suite/components/sidebar/jar.mn
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/sidebar/customize-panel.js (content/customize-panel.js)
+ content/communicator/sidebar/customize-panel.xul (content/customize-panel.xul)
+ content/communicator/sidebar/customize.js (content/customize.js)
+ content/communicator/sidebar/customize.xul (content/customize.xul)
+ content/communicator/sidebar/PageNotFound.xul (content/PageNotFound.xul)
+ content/communicator/sidebar/preview.js (content/preview.js)
+ content/communicator/sidebar/preview.xul (content/preview.xul)
+ content/communicator/sidebar/sidebarBindings.xml (content/sidebarBindings.xml)
+ content/communicator/sidebar/sidebarOverlay.css (content/sidebarOverlay.css)
+ content/communicator/sidebar/sidebarOverlay.js (content/sidebarOverlay.js)
+* content/communicator/sidebar/sidebarOverlay.xul (content/sidebarOverlay.xul)
diff --git a/comm/suite/components/sidebar/moz.build b/comm/suite/components/sidebar/moz.build
new file mode 100644
index 0000000000..7a2e523961
--- /dev/null
+++ b/comm/suite/components/sidebar/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsISidebar.idl",
+]
+
+XPIDL_MODULE = "suite-sidebar"
+
+EXTRA_COMPONENTS += [
+ "nsSidebar.js",
+ "SuiteSidebar.manifest",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/sidebar/nsISidebar.idl b/comm/suite/components/sidebar/nsISidebar.idl
new file mode 100644
index 0000000000..515b939872
--- /dev/null
+++ b/comm/suite/components/sidebar/nsISidebar.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+
+ The Sidebar API for 3rd parties
+
+*/
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(97bfa970-8222-4c3f-bbe8-42141e4c7982)]
+interface nsISidebar : nsISupports
+{
+ void addPanel(in AString aTitle, in AString aContentURL,
+ in AString aCustomizeURL);
+ void addPersistentPanel(in AString aTitle, in AString aContentURL,
+ in AString aCustomizeURL);
+ void addSearchEngine(in AString engineURL, in AString iconURL,
+ in AString suggestedTitle, in AString suggestedCategory);
+ void AddSearchProvider(in AString aDescriptionURL);
+ unsigned long IsSearchProviderInstalled(in AString aSearchURL);
+};
diff --git a/comm/suite/components/sidebar/nsSidebar.js b/comm/suite/components/sidebar/nsSidebar.js
new file mode 100644
index 0000000000..472ec25a5d
--- /dev/null
+++ b/comm/suite/components/sidebar/nsSidebar.js
@@ -0,0 +1,348 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * No magic constructor behaviour, as is de rigeur for XPCOM.
+ * If you must perform some initialization, and it could possibly fail (even
+ * due to an out-of-memory condition), you should use an Init method, which
+ * can convey failure appropriately (thrown exception in JS,
+ * NS_FAILED(nsresult) return in C++).
+ *
+ * In JS, you can actually cheat, because a thrown exception will cause the
+ * CreateInstance call to fail in turn, but not all languages are so lucky.
+ * (Though ANSI C++ provides exceptions, they are verboten in Mozilla code
+ * for portability reasons -- and even when you're building completely
+ * platform-specific code, you can't throw across an XPCOM method boundary.)
+ */
+
+const DEBUG = false; /* set to false to suppress debug messages */
+const PANELS_RDF_FILE = "UPnls"; /* directory services property to find panels.rdf */
+
+const SIDEBAR_CONTRACTID = "@mozilla.org/sidebar;1";
+const SIDEBAR_CID = Components.ID("{22117140-9c6e-11d3-aaf1-00805f8a4905}");
+const CONTAINER_CONTRACTID = "@mozilla.org/rdf/container;1";
+const NETSEARCH_CONTRACTID = "@mozilla.org/rdf/datasource;1?name=internetsearch"
+const nsISupports = Ci.nsISupports;
+const nsISidebar = Ci.nsISidebar;
+const nsIRDFContainer = Ci.nsIRDFContainer;
+const nsIProperties = Ci.nsIProperties;
+const nsIFileURL = Ci.nsIFileURL;
+const nsIRDFRemoteDataSource = Ci.nsIRDFRemoteDataSource;
+const nsIClassInfo = Ci.nsIClassInfo;
+
+// File extension for Sherlock search plugin description files
+const SHERLOCK_FILE_EXT_REGEXP = /\.src$/i;
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function nsSidebar()
+{
+ const RDF_CONTRACTID = "@mozilla.org/rdf/rdf-service;1";
+ const nsIRDFService = Ci.nsIRDFService;
+
+ this.rdf = Cc[RDF_CONTRACTID].getService(nsIRDFService);
+ this.datasource_uri = getSidebarDatasourceURI(PANELS_RDF_FILE);
+ gDebugLog('datasource_uri is ' + this.datasource_uri);
+ this.resource = 'urn:sidebar:current-panel-list';
+ this.datasource = this.rdf.GetDataSource(this.datasource_uri);
+}
+
+nsSidebar.prototype.nc = "http://home.netscape.com/NC-rdf#";
+
+nsSidebar.prototype.isPanel =
+function (aContentURL)
+{
+ var container =
+ Cc[CONTAINER_CONTRACTID].createInstance(nsIRDFContainer);
+
+ container.Init(this.datasource, this.rdf.GetResource(this.resource));
+
+ /* Create a resource for the new panel and add it to the list */
+ var panel_resource =
+ this.rdf.GetResource("urn:sidebar:3rdparty-panel:" + aContentURL);
+
+ return (container.IndexOf(panel_resource) != -1);
+}
+
+function sidebarURLSecurityCheck(url)
+{
+ if (!/(^http:|^ftp:|^https:)/i.test(url))
+ throw "Script attempted to add sidebar panel from illegal source";
+}
+
+/* decorate prototype to provide ``class'' methods and property accessors */
+nsSidebar.prototype.addPanel =
+function (aTitle, aContentURL, aCustomizeURL)
+{
+ gDebugLog("addPanel(" + aTitle + ", " + aContentURL + ", " +
+ aCustomizeURL + ")");
+
+ return this.addPanelInternal(aTitle, aContentURL, aCustomizeURL, false);
+}
+
+nsSidebar.prototype.addPersistentPanel =
+function(aTitle, aContentURL, aCustomizeURL)
+{
+ gDebugLog("addPersistentPanel(" + aTitle + ", " + aContentURL + ", " +
+ aCustomizeURL + ")\n");
+
+ return this.addPanelInternal(aTitle, aContentURL, aCustomizeURL, true);
+}
+
+nsSidebar.prototype.addPanelInternal =
+function (aTitle, aContentURL, aCustomizeURL, aPersist)
+{
+ sidebarURLSecurityCheck(aContentURL);
+
+ // Create a "container" wrapper around the current panels to
+ // manipulate the RDF:Seq more easily.
+ var panel_list = this.datasource.GetTarget(this.rdf.GetResource(this.resource), this.rdf.GetResource(nsSidebar.prototype.nc+"panel-list"), true);
+ if (panel_list) {
+ panel_list.QueryInterface(Ci.nsIRDFResource);
+ } else {
+ // Datasource is busted. Start over.
+ gDebugLog("Sidebar datasource is busted\n");
+ }
+
+ var container = Cc[CONTAINER_CONTRACTID].createInstance(nsIRDFContainer);
+ container.Init(this.datasource, panel_list);
+
+ /* Create a resource for the new panel and add it to the list */
+ var panel_resource =
+ this.rdf.GetResource("urn:sidebar:3rdparty-panel:" + aContentURL);
+ var panel_index = container.IndexOf(panel_resource);
+ var stringBundle, titleMessage, dialogMessage;
+ if (panel_index != -1)
+ {
+ try {
+ stringBundle = Services.strings.createBundle("chrome://communicator/locale/sidebar/sidebar.properties");
+ if (stringBundle) {
+ titleMessage = stringBundle.GetStringFromName("dupePanelAlertTitle");
+ dialogMessage = stringBundle.GetStringFromName("dupePanelAlertMessage2");
+ dialogMessage = dialogMessage.replace(/%url%/, aContentURL);
+ }
+ }
+ catch (e) {
+ titleMessage = "Sidebar";
+ dialogMessage = aContentURL + " already exists in Sidebar. No string bundle";
+ }
+
+ Services.prompt.alert(null, titleMessage, dialogMessage);
+
+ return;
+ }
+
+ try {
+ stringBundle = Services.strings.createBundle("chrome://communicator/locale/sidebar/sidebar.properties");
+ if (stringBundle) {
+ titleMessage = stringBundle.GetStringFromName("addPanelConfirmTitle");
+ dialogMessage = stringBundle.GetStringFromName("addPanelConfirmMessage2");
+ if (aPersist)
+ {
+ var warning = stringBundle.GetStringFromName("persistentPanelWarning2");
+ dialogMessage += "\n" + warning;
+ }
+ dialogMessage = dialogMessage.replace(/%title%/, aTitle);
+ dialogMessage = dialogMessage.replace(/%url%/, aContentURL);
+ dialogMessage = dialogMessage.replace(/#/g, "\n");
+ }
+ }
+ catch (e) {
+ titleMessage = "Add Tab to Sidebar";
+ dialogMessage = "No string bundle. Add the Tab '" + aTitle + "' to Sidebar?\n\n" + "Source: " + aContentURL;
+ }
+
+ var rv = Services.prompt.confirm(null, titleMessage, dialogMessage);
+
+ if (!rv)
+ return;
+
+ /* Now make some sidebar-ish assertions about it... */
+ this.datasource.Assert(panel_resource,
+ this.rdf.GetResource(this.nc + "title"),
+ this.rdf.GetLiteral(aTitle),
+ true);
+ this.datasource.Assert(panel_resource,
+ this.rdf.GetResource(this.nc + "content"),
+ this.rdf.GetLiteral(aContentURL),
+ true);
+ if (aCustomizeURL)
+ this.datasource.Assert(panel_resource,
+ this.rdf.GetResource(this.nc + "customize"),
+ this.rdf.GetLiteral(aCustomizeURL),
+ true);
+ var persistValue = aPersist ? "true" : "false";
+ this.datasource.Assert(panel_resource,
+ this.rdf.GetResource(this.nc + "persist"),
+ this.rdf.GetLiteral(persistValue),
+ true);
+
+ container.AppendElement(panel_resource);
+
+ // Use an assertion to pass a "refresh" event to all the sidebars.
+ // They use observers to watch for this assertion (in sidebarOverlay.js).
+ this.datasource.Assert(this.rdf.GetResource(this.resource),
+ this.rdf.GetResource(this.nc + "refresh"),
+ this.rdf.GetLiteral("true"),
+ true);
+ this.datasource.Unassert(this.rdf.GetResource(this.resource),
+ this.rdf.GetResource(this.nc + "refresh"),
+ this.rdf.GetLiteral("true"));
+
+ /* Write the modified panels out. */
+ this.datasource.QueryInterface(nsIRDFRemoteDataSource).Flush();
+}
+
+nsSidebar.prototype.validateSearchEngine =
+function (engineURL, iconURL)
+{
+ try
+ {
+ // Make sure the URLs are HTTP, HTTPS, or FTP.
+ var isWeb = /^(https?|ftp):\/\//i;
+
+ if (!isWeb.test(engineURL))
+ throw "Unsupported search engine URL";
+
+ if (iconURL && !isWeb.test(iconURL))
+ throw "Unsupported search icon URL.";
+ }
+ catch(ex)
+ {
+ gDebugLog(ex);
+ Cu.reportError("Invalid argument passed to window.sidebar.addSearchEngine: " + ex);
+
+ var searchBundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
+ var brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ var brandName = brandBundle.GetStringFromName("brandShortName");
+ var title = searchBundle.GetStringFromName("error_invalid_engine_title");
+ var msg = searchBundle.formatStringFromName("error_invalid_engine_msg",
+ [brandName], 1);
+ Services.ww.getNewPrompter(null).alert(title, msg);
+ return false;
+ }
+
+ return true;
+}
+
+// The suggestedTitle and suggestedCategory parameters are ignored, but remain
+// for backward compatibility.
+nsSidebar.prototype.addSearchEngine =
+function (engineURL, iconURL, suggestedTitle, suggestedCategory)
+{
+ gDebugLog("addSearchEngine(" + engineURL + ", " + iconURL + ", " +
+ suggestedCategory + ", " + suggestedTitle + ")");
+
+ if (!this.validateSearchEngine(engineURL, iconURL))
+ return;
+
+ // OpenSearch files will likely be far more common than Sherlock files, and
+ // have less consistent suffixes, so we assume that ".src" is a Sherlock
+ // (text) file, and anything else is OpenSearch (XML).
+ var dataType;
+ if (SHERLOCK_FILE_EXT_REGEXP.test(engineURL))
+ dataType = Ci.nsISearchEngine.DATA_TEXT;
+ else
+ dataType = Ci.nsISearchEngine.DATA_XML;
+
+ Services.search.addEngine(engineURL, dataType, iconURL, true);
+}
+
+// This function exists largely to implement window.external.AddSearchProvider(),
+// to match other browsers' APIs. The capitalization, although nonstandard here,
+// is therefore important.
+nsSidebar.prototype.AddSearchProvider =
+function (aDescriptionURL)
+{
+ // Get the favicon URL for the current page, or our best guess at the current
+ // page since we don't have easy access to the active document. Most search
+ // engines will override this with an icon specified in the OpenSearch
+ // description anyway.
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+ var browser = win.getBrowser();
+ var iconURL = "";
+ // Use documentURIObject in the check for shouldLoadFavIcon so that we
+ // do the right thing with about:-style error pages. Bug 453442
+ if (browser.shouldLoadFavIcon(browser.selectedBrowser
+ .contentDocument
+ .documentURIObject))
+ iconURL = browser.getIcon();
+
+ if (!this.validateSearchEngine(aDescriptionURL, iconURL))
+ return;
+
+ const typeXML = Ci.nsISearchEngine.DATA_XML;
+ Services.search.addEngine(aDescriptionURL, typeXML, iconURL, true);
+}
+
+// This function exists to implement window.external.IsSearchProviderInstalled(),
+// for compatibility with other browsers. It will return an integer value
+// indicating whether the given engine is installed for the current user.
+// However, it is currently stubbed out due to security/privacy concerns
+// stemming from difficulties in determining what domain issued the request.
+// See bug 340604 and
+// http://msdn.microsoft.com/en-us/library/aa342526%28VS.85%29.aspx .
+// XXX Implement this!
+nsSidebar.prototype.IsSearchProviderInstalled =
+function (aSearchURL)
+{
+ return 0;
+}
+
+nsSidebar.prototype.classInfo = XPCOMUtils.generateCI({
+ classID: SIDEBAR_CID,
+ contractID: SIDEBAR_CONTRACTID,
+ classDescription: "Sidebar",
+ interfaces: [nsISidebar],
+ flags: nsIClassInfo.DOM_OBJECT});
+
+nsSidebar.prototype.QueryInterface =
+ XPCOMUtils.generateQI([nsISidebar]);
+
+nsSidebar.prototype.classID = SIDEBAR_CID;
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSidebar]);
+
+var gDebugLog;
+
+/* static functions */
+if (DEBUG)
+ gDebugLog = function (s) { dump("-*- sidebar component: " + s + "\n"); }
+else
+ gDebugLog = function (s) {}
+
+function getSidebarDatasourceURI(panels_file_id)
+{
+ try
+ {
+ /* use the fileLocator to look in the profile directory
+ * to find 'panels.rdf', which is the
+ * database of the user's currently selected panels.
+ * if <profile>/panels.rdf doesn't exist, get will copy
+ *bin/defaults/profile/panels.rdf to <profile>/panels.rdf */
+ var sidebar_file = Services.dirsvc.get(panels_file_id,
+ Ci.nsIFile);
+
+ if (!sidebar_file.exists())
+ {
+ /* this should not happen, as GetFileLocation() should copy
+ * defaults/panels.rdf to the users profile directory */
+ gDebugLog("sidebar file does not exist");
+ return null;
+ }
+
+ var file_handler = Services.io.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler);
+ var sidebar_uri = file_handler.getURLSpecFromFile(sidebar_file);
+ gDebugLog("sidebar uri is " + sidebar_uri);
+ return sidebar_uri;
+ }
+ catch (ex)
+ {
+ /* this should not happen */
+ gDebugLog("caught " + ex + " getting sidebar datasource uri");
+ return null;
+ }
+}
diff --git a/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml b/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml
new file mode 100644
index 0000000000..6365bf55fc
--- /dev/null
+++ b/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="tabBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="tab-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox>
+ <xul:image class="tabIcon"
+ xbl:inherits="src=icon"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label xbl:inherits="value=title,selected"
+ crop="end" flex="1" class="title"/>
+ <xul:label xbl:inherits="value=url,selected"
+ crop="end" flex="1" class="url"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <handlers>
+ <handler event="dblclick" button="0">
+ <![CDATA[
+ RemoteTabViewer.openSelected();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="client-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox align="center" onfocus="event.target.blur();" onselect="return false;">
+ <xul:image/>
+ <xul:label xbl:inherits="value=clientName"
+ class="clientName"
+ crop="center" flex="1"/>
+ </xul:hbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/comm/suite/components/sync/content/aboutSyncTabs.css b/comm/suite/components/sync/content/aboutSyncTabs.css
new file mode 100644
index 0000000000..83782b4d5f
--- /dev/null
+++ b/comm/suite/components/sync/content/aboutSyncTabs.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+richlistitem[type="tab"] {
+ -moz-binding: url(chrome://communicator/content/aboutSyncTabs-bindings.xml#tab-listing);
+}
+
+richlistitem[type="client"] {
+ -moz-binding: url(chrome://communicator/content/aboutSyncTabs-bindings.xml#client-listing);
+}
diff --git a/comm/suite/components/sync/content/aboutSyncTabs.js b/comm/suite/components/sync/content/aboutSyncTabs.js
new file mode 100644
index 0000000000..e13c2be685
--- /dev/null
+++ b/comm/suite/components/sync/content/aboutSyncTabs.js
@@ -0,0 +1,293 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+const {PlacesUIUtils} = ChromeUtils.import("resource:///modules/PlacesUIUtils.jsm");
+const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var RemoteTabViewer = {
+ _tabsList: null,
+
+ init: function () {
+ Services.obs.addObserver(this, "weave:service:login:finish");
+ Services.obs.addObserver(this, "weave:engine:sync:finish");
+
+ this._tabsList = document.getElementById("tabsList");
+
+ this.buildList(true);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "weave:service:login:finish");
+ Services.obs.removeObserver(this, "weave:engine:sync:finish");
+ },
+
+ buildList: function(force) {
+ if (!Weave.Service.isLoggedIn || !this._refetchTabs(force))
+ return;
+ //XXXzpao We should say something about not being logged in & not having data
+ // or tell the appropriate condition. (bug 583344)
+
+ this._generateTabList();
+ },
+
+ createItem: function(attrs) {
+ let item = document.createElement("richlistitem");
+
+ // Copy the attributes from the argument into the item
+ for (let attr in attrs)
+ item.setAttribute(attr, attrs[attr]);
+
+ if (attrs["type"] == "tab")
+ item.label = attrs.title || attrs.url;
+
+ return item;
+ },
+
+ filterTabs: function(event) {
+ let val = event.target.value.toLowerCase();
+ let numTabs = this._tabsList.getRowCount();
+ let client = null;
+ for (let i = 0; i < numTabs; i++) {
+ let item = this._tabsList.getItemAtIndex(i);
+ let hide = false; // By default, make sure the item is visible.
+ switch (item.getAttribute("type")) {
+ case "tab":
+ if (item.getAttribute("url").toLowerCase().indexOf(val) == -1 &&
+ item.getAttribute("title").toLowerCase().indexOf(val) == -1)
+ hide = true;
+ else
+ client = null; // This client should not be hidden.
+ break;
+ case "client":
+ if (client)
+ client.hidden = true; // Hide the last client, it had no visible tabs.
+ client = item;
+ break;
+ }
+ item.hidden = hide;
+ }
+ if (client)
+ client.hidden = true; // Hide the last client, it had no visible tabs.
+ },
+
+ openSelected: function() {
+ let items = this._tabsList.selectedItems;
+ let urls = [];
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "tab") {
+ urls.push(items[i].getAttribute("url"));
+ let index = this._tabsList.getIndexOfItem(items[i]);
+ this._tabsList.removeItemAt(index);
+ }
+ }
+ if (urls.length) {
+ getTopWin().getBrowser().loadTabs(urls);
+ this._tabsList.clearSelection();
+ }
+ },
+
+ bookmarkSingleTab: function() {
+ let item = this._tabsList.selectedItems[0];
+ let uri = Weave.Utils.makeURI(item.getAttribute("url"));
+ let title = item.getAttribute("title");
+ PlacesUIUtils.showMinimalAddBookmarkUI(uri, title);
+ },
+
+ bookmarkSelectedTabs: function() {
+ let items = this._tabsList.selectedItems;
+ let URIs = [];
+ let titles = [];
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "tab") {
+ let uri = Weave.Utils.makeURI(items[i].getAttribute("url"));
+ if (!uri)
+ continue;
+
+ URIs.push(uri);
+ titles.push(items[i].getAttribute("title"));
+ }
+ }
+ if (URIs.length)
+ PlacesUIUtils.showMinimalAddMultiBookmarkUI(URIs, titles);
+ },
+
+ getIcon: function (iconUri, defaultIcon) {
+ try {
+ let iconURI = Weave.Utils.makeURI(iconUri);
+ return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
+ } catch(ex) {
+ // Do nothing.
+ }
+
+ // Just give the provided default icon or the system's default.
+ return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec;
+ },
+
+ _generateTabList: function() {
+ let engine = Weave.Service.engineManager.get("tabs");
+ let list = this._tabsList;
+
+ // clear out existing richlistitems
+ let count = list.getRowCount();
+ if (count > 0) {
+ for (let i = count - 1; i >= 0; i--)
+ list.removeItemAt(i);
+ }
+
+ let seenURLs = new Set();
+ let localURLs = engine.getOpenURLs();
+
+ for (let [guid, client] of Object.entries(engine.getAllClients())) {
+ let appendClient = true;
+
+ client.tabs.forEach(function({title, urlHistory, icon}) {
+ let url = urlHistory[0];
+ if (!url || localURLs.has(url) || seenURLs.has(url))
+ return;
+
+ seenURLs.add(url);
+
+ if (appendClient) {
+ let attrs = {
+ type: "client",
+ clientName: client.clientName,
+ class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop"
+ };
+ let clientEnt = this.createItem(attrs);
+ list.appendChild(clientEnt);
+ appendClient = false;
+ clientEnt.disabled = true;
+ }
+ let attrs = {
+ type: "tab",
+ title: title || url,
+ url: url,
+ icon: this.getIcon(icon)
+ }
+ let tab = this.createItem(attrs);
+ list.appendChild(tab);
+ }, this);
+ }
+ },
+
+ adjustContextMenu: function(event) {
+ let mode = "all";
+ switch (this._tabsList.selectedItems.length) {
+ case 0:
+ break;
+ case 1:
+ mode = "single"
+ break;
+ default:
+ mode = "multiple";
+ break;
+ }
+ let menu = document.getElementById("tabListContext");
+ let el = menu.firstChild;
+ while (el) {
+ let showFor = el.getAttribute("showFor");
+ if (showFor)
+ el.hidden = showFor != mode && showFor != "all";
+ else // menuseparator
+ el.hidden = mode == "all";
+ el = el.nextSibling;
+ }
+ },
+
+ _refetchTabs: function(force) {
+ if (!force) {
+ // Don't bother refetching tabs if we already did so recently
+ let lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch", 0);
+ let now = Math.floor(Date.now() / 1000);
+ if (now - lastFetch < 30)
+ return false;
+ }
+
+ // if Clients hasn't synced yet this session, need to sync it as well
+ if (Weave.Service.clientsEngine.lastSync == 0)
+ Weave.Service.clientsEngine.sync();
+
+ // Force a sync only for the tabs engine
+ let engine = Weave.Service.engineManager.get("tabs");
+ engine.lastModified = null;
+ engine.sync();
+ Services.prefs.setIntPref("services.sync.lastTabFetch",
+ Math.floor(Date.now() / 1000));
+
+ return true;
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "weave:service:login:finish":
+ this.buildList(true);
+ break;
+ case "weave:engine:sync:finish":
+ if (subject == "tabs")
+ this._generateTabList();
+ break;
+ }
+ },
+
+ handleClick: function(event) {
+ if (event.target.getAttribute("type") == "tab" && event.button == 1) {
+ let url = event.target.getAttribute("url");
+ openUILink(url, event);
+ let index = this._tabsList.getIndexOfItem(event.target);
+ this._tabsList.removeItemAt(index);
+ }
+ }
+};
+
+var EventDirector = {
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "click":
+ RemoteTabViewer.handleClick(event);
+ break;
+ case "contextmenu":
+ RemoteTabViewer.adjustContextMenu(event);
+ break;
+ case "command":
+ switch (event.target.id) {
+ case "openSingleTab":
+ case "openSelectedTabs":
+ RemoteTabViewer.openSelected();
+ break;
+ case "bookmarkSingleTab":
+ RemoteTabViewer.bookmarkSingleTab();
+ break;
+ case "bookmarkSelectedTabs":
+ RemoteTabViewer.bookmarkSelectedTabs();
+ break;
+ case "buildList":
+ RemoteTabViewer.buildList();
+ break;
+ case "filterTabs":
+ RemoteTabViewer.filterTabs(event);
+ break;
+ }
+ break;
+ }
+ }
+};
+
+window.onload = function() {
+ RemoteTabViewer.init();
+
+ let tabsList = document.getElementById("tabsList");
+ tabsList.addEventListener("click", EventDirector);
+ tabsList.addEventListener("contextmenu", EventDirector);
+
+ document.getElementById("tabListContext")
+ .addEventListener("command", EventDirector);
+ document.getElementById("filterTabs")
+ .addEventListener("command", EventDirector);
+}
+
+window.onunload = function() {
+ RemoteTabViewer.uninit();
+}
diff --git a/comm/suite/components/sync/content/aboutSyncTabs.xul b/comm/suite/components/sync/content/aboutSyncTabs.xul
new file mode 100644
index 0000000000..b0355557de
--- /dev/null
+++ b/comm/suite/components/sync/content/aboutSyncTabs.xul
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/aboutSyncTabs.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/content/aboutSyncTabs.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://communicator/locale/aboutSyncTabs.dtd">
+ %aboutSyncTabsDTD;
+]>
+
+<window id="tabs-display"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="&tabs.otherComputers.label;">
+
+ <script src="chrome://communicator/content/aboutSyncTabs.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+
+ <html:head>
+ <html:link rel="icon" href="chrome://communicator/skin/sync/sync-16.png"/>
+ </html:head>
+
+ <popupset id="contextmenus">
+ <menupopup id="tabListContext">
+ <menuitem id="openSingleTab"
+ label="&tabs.context.openTab.label;"
+ accesskey="&tabs.context.openTab.accesskey;"
+ showFor="single"/>
+ <menuitem id="bookmarkSingleTab"
+ label="&tabs.context.bookmarkSingleTab.label;"
+ accesskey="&tabs.context.bookmarkSingleTab.accesskey;"
+ showFor="single"/>
+ <menuitem id="openSelectedTabs"
+ label="&tabs.context.openMultipleTabs.label;"
+ accesskey="&tabs.context.openMultipleTabs.accesskey;"
+ showFor="multiple"/>
+ <menuitem id="bookmarkSelectedTabs"
+ label="&tabs.context.bookmarkMultipleTabs.label;"
+ accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;"
+ showFor="multiple"/>
+ <menuseparator/>
+ <menuitem id="buildList"
+ label="&tabs.context.refreshList.label;"
+ accesskey="&tabs.context.refreshList.accesskey;"
+ showFor="all"/>
+ </menupopup>
+ </popupset>
+ <richlistbox id="tabsList"
+ context="tabListContext"
+ seltype="multiple"
+ class="plain"
+ align="center"
+ flex="1">
+ <hbox id="headers" align="center">
+ <label id="tabsListHeading"
+ value="&tabs.otherComputers.label;"/>
+ <spacer flex="1"/>
+ <textbox id="filterTabs"
+ type="search"
+ aria-controls="tabsList"
+ emptytext="&tabs.searchText.label;"/>
+ </hbox>
+ </richlistbox>
+</window>
diff --git a/comm/suite/components/sync/content/syncAddDevice.js b/comm/suite/components/sync/content/syncAddDevice.js
new file mode 100644
index 0000000000..d986b54f00
--- /dev/null
+++ b/comm/suite/components/sync/content/syncAddDevice.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PIN_PART_LENGTH = 4;
+
+const ADD_DEVICE_PAGE = 0;
+const SYNC_KEY_PAGE = 1;
+const DEVICE_CONNECTED_PAGE = 2;
+
+var gSyncAddDevice = {
+ init: function init() {
+ this.nextFocusEl = { pin1: this.pin2,
+ pin2: this.pin3,
+ pin3: this.wizard.getButton("next") };
+
+ this.throbber = document.getElementById("add-device-throbber");
+ this.errorRow = document.getElementById("errorRow");
+ },
+
+ onPageShow: function onPageShow() {
+ this.wizard.getButton("back").hidden = true;
+
+ switch (this.wizard.pageIndex) {
+ case ADD_DEVICE_PAGE:
+ this.onTextBoxInput();
+ this.wizard.canRewind = false;
+ this.wizard.getButton("next").hidden = false;
+ this.pin1.focus();
+ break;
+ case SYNC_KEY_PAGE:
+ this.wizard.canAdvance = false;
+ this.wizard.canRewind = true;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.getButton("next").hidden = true;
+ document.getElementById("weavePassphrase").value =
+ Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
+ break;
+ case DEVICE_CONNECTED_PAGE:
+ this.wizard.canAdvance = true;
+ this.wizard.canRewind = false;
+ this.wizard.getButton("cancel").hidden = true;
+ break;
+ }
+ },
+
+ onWizardAdvance: function onWizardAdvance() {
+ switch (this.wizard.pageIndex) {
+ case ADD_DEVICE_PAGE:
+ this.startTransfer();
+ return false;
+ case DEVICE_CONNECTED_PAGE:
+ window.close();
+ return false;
+ }
+ return true;
+ },
+
+ startTransfer: function startTransfer() {
+ this.errorRow.hidden = true;
+ // When onAbort is called, Weave may already be gone.
+ const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
+
+ let self = this;
+ let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
+ onPaired: function onPaired() {
+ let credentials = {account: Weave.Service.identity.account,
+ password: Weave.Service.identity.basicPassword,
+ synckey: Weave.Service.identity.syncKey,
+ serverURL: Weave.Service.serverURL};
+ jpakeclient.sendAndComplete(credentials);
+ },
+ onComplete: function onComplete() {
+ delete self._jpakeclient;
+ self.wizard.pageIndex = DEVICE_CONNECTED_PAGE;
+
+ // Schedule a sync for soonish to fetch the data uploaded by the
+ // device with which we just paired.
+ Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval);
+ },
+ onAbort: function onAbort(error) {
+ delete self._jpakeclient;
+
+ // Aborted by user, ignore.
+ if (error == JPAKE_ERROR_USERABORT)
+ return;
+
+ self.errorRow.hidden = false;
+ self.throbber.hidden = true;
+ self.pin1.value = self.pin2.value = self.pin3.value = "";
+ self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false;
+ self.pin1.focus();
+ }
+ });
+ this.throbber.hidden = false;
+ this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true;
+ this.wizard.canAdvance = false;
+
+ let pin = this.pin1.value + this.pin2.value + this.pin3.value;
+ let expectDelay = false;
+ jpakeclient.pairWithPIN(pin, expectDelay);
+ },
+
+ onWizardBack: function onWizardBack() {
+ if (this.wizard.pageIndex != SYNC_KEY_PAGE)
+ return true;
+
+ this.wizard.pageIndex = ADD_DEVICE_PAGE;
+ return false;
+ },
+
+ onWizardCancel: function onWizardCancel() {
+ if (this._jpakeclient) {
+ this._jpakeclient.abort();
+ delete this._jpakeclient;
+ }
+ return true;
+ },
+
+ onTextBoxInput: function onTextBoxInput(textbox) {
+ this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH &&
+ this.pin2.value.length == PIN_PART_LENGTH &&
+ this.pin3.value.length == PIN_PART_LENGTH);
+ if (textbox && textbox.value.length == PIN_PART_LENGTH)
+ this.nextFocusEl[textbox.id].focus();
+ },
+
+ goToSyncKeyPage: function goToSyncKeyPage() {
+ this.wizard.pageIndex = SYNC_KEY_PAGE;
+ }
+};
+
+// onWizardAdvance() and onPageShow() are run before init() so we'll set
+// these up as lazy getters.
+["wizard", "pin1", "pin2", "pin3"].forEach(function (id) {
+ XPCOMUtils.defineLazyGetter(gSyncAddDevice, id, function() {
+ return document.getElementById(id);
+ });
+});
diff --git a/comm/suite/components/sync/content/syncAddDevice.xul b/comm/suite/components/sync/content/syncAddDevice.xul
new file mode 100644
index 0000000000..e7a30d2aa3
--- /dev/null
+++ b/comm/suite/components/sync/content/syncAddDevice.xul
@@ -0,0 +1,128 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="wizard"
+ title="&addDevice.title.label;"
+ windowtype="Sync:AddDevice"
+ persist="screenX screenY"
+ onwizardnext="return gSyncAddDevice.onWizardAdvance();"
+ onwizardback="return gSyncAddDevice.onWizardBack();"
+ onwizardcancel="gSyncAddDevice.onWizardCancel();"
+ onload="gSyncAddDevice.init();">
+
+ <script src="chrome://communicator/content/sync/syncAddDevice.js"/>
+ <script src="chrome://communicator/content/sync/syncUtils.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+ <script src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="addDevicePage"
+ label="&addDevice.title.label;"
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <description>
+ &addDevice.dialog.description.label;
+ <label class="text-link"
+ value="&addDevice.showMeHow.label;"
+ href="https://services.mozilla.com/sync/help/add-device"/>
+ </description>
+ <spacer flex="1"/>
+ <description>
+ &addDevice.dialog.enterCode.label;
+ </description>
+ <spacer flex="1"/>
+ <vbox align="center">
+ <textbox id="pin1"
+ class="pin"
+ size="4"
+ maxlength="4"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"/>
+ <textbox id="pin2"
+ class="pin"
+ size="4"
+ maxlength="4"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"/>
+ <textbox id="pin3"
+ class="pin"
+ size="4"
+ maxlength="4"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"/>
+ </vbox>
+ <spacer flex="1"/>
+ <vbox id="add-device-throbber" align="center" hidden="true">
+ <image/>
+ </vbox>
+ <hbox id="errorRow" pack="center" hidden="true">
+ <image class="statusIcon" status="error"/>
+ <label class="status"
+ value="&addDevice.dialog.tryAgain.label;"/>
+ </hbox>
+ <spacer flex="3"/>
+ <label class="text-link"
+ value="&addDevice.dontHaveDevice.label;"
+ onclick="gSyncAddDevice.goToSyncKeyPage();"/>
+ </wizardpage>
+
+ <!-- Need a non-empty label here, otherwise we get a default label on Mac -->
+ <wizardpage id="syncKeyPage"
+ label=" "
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <description>
+ &addDevice.dialog.recoveryKey.label;
+ </description>
+ <spacer/>
+
+ <groupbox>
+ <label value="&recoveryKeyEntry.label;"
+ accesskey="&recoveryKeyEntry.accesskey;"
+ control="weavePassphrase"/>
+ <textbox id="weavePassphrase"
+ readonly="true"/>
+ </groupbox>
+
+ <groupbox align="center">
+ <description>&recoveryKeyBackup.description;</description>
+ <hbox>
+ <button id="printSyncKeyButton"
+ label="&button.syncKeyBackup.print.label;"
+ accesskey="&button.syncKeyBackup.print.accesskey;"
+ oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
+ <button id="saveSyncKeyButton"
+ label="&button.syncKeyBackup.save.label;"
+ accesskey="&button.syncKeyBackup.save.accesskey;"
+ oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/>
+ </hbox>
+ </groupbox>
+ </wizardpage>
+
+ <wizardpage id="deviceConnectedPage"
+ label="&addDevice.dialog.connected.label;"
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <vbox align="center">
+ <image id="successPageIcon"/>
+ </vbox>
+ <separator/>
+ <description class="normal">
+ &addDevice.dialog.successful.label;
+ </description>
+ </wizardpage>
+
+</wizard>
diff --git a/comm/suite/components/sync/content/syncGenericChange.js b/comm/suite/components/sync/content/syncGenericChange.js
new file mode 100644
index 0000000000..13cab89811
--- /dev/null
+++ b/comm/suite/components/sync/content/syncGenericChange.js
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var Change = {
+ _dialog: null,
+ _dialogType: null,
+ _status: null,
+ _statusIcon: null,
+ _firstBox: null,
+ _secondBox: null,
+ _passphraseBox: null,
+
+ get _currentPasswordInvalid() {
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ get _updatingPassphrase() {
+ return this._dialogType == "UpdatePassphrase";
+ },
+
+ onLoad: function Change_onLoad() {
+ /* Load labels */
+ let introText = document.getElementById("introText");
+ let warningText = document.getElementById("warningText");
+
+ // load some other elements & info from the window
+ this._dialog = document.getElementById("change-dialog");
+ this._dialogType = window.arguments[0];
+ this._duringSetup = window.arguments[1];
+ this._status = document.getElementById("status");
+ this._statusIcon = document.getElementById("statusIcon");
+ this._statusRow = document.getElementById("statusRow");
+ this._firstBox = document.getElementById("textBox1");
+ this._secondBox = document.getElementById("textBox2");
+ this._passphraseBox = document.getElementById("passphraseBox");
+
+ this._dialog.getButton("finish").disabled = true;
+ this._dialog.getButton("back").hidden = true;
+
+ this._stringBundle =
+ Services.strings.createBundle("chrome://communicator/locale/sync/syncGenericChange.properties");
+
+ switch (this._dialogType) {
+ case "UpdatePassphrase":
+ case "ResetPassphrase":
+ document.getElementById("textBox1Row").hidden = true;
+ document.getElementById("textBox2Row").hidden = true;
+ document.getElementById("passphraseLabel").value
+ = this._str("new.recoverykey.label");
+ document.getElementById("passphraseSpacer").hidden = false;
+
+ if (this._updatingPassphrase) {
+ document.getElementById("passphraseHelpBox").hidden = false;
+ document.title = this._str("new.recoverykey.title");
+ introText.textContent = this._str("new.recoverykey.introText");
+ this._dialog.getButton("finish").label
+ = this._str("new.recoverykey.acceptButton");
+ }
+ else {
+ document.getElementById("generatePassphraseButton").hidden = false;
+ document.getElementById("passphraseBackupButtons").hidden = false;
+ this._passphraseBox.readOnly = true;
+ let pp = Weave.Service.identity.syncKey;
+ if (Weave.Utils.isPassphrase(pp))
+ pp = Weave.Utils.hyphenatePassphrase(pp);
+ this._passphraseBox.value = pp;
+ this._passphraseBox.focus();
+ document.title = this._str("change.recoverykey.title");
+ introText.textContent = this._str("change.recoverykey.introText2");
+ warningText.textContent = this._str("change.recoverykey.warningText");
+ this._dialog.getButton("finish").label
+ = this._str("change.recoverykey.acceptButton");
+ if (this._duringSetup)
+ this._dialog.getButton("finish").disabled = false;
+ }
+ break;
+ case "ChangePassword":
+ document.getElementById("passphraseRow").hidden = true;
+ let box1label = document.getElementById("textBox1Label");
+ let box2label = document.getElementById("textBox2Label");
+ box1label.value = this._str("new.password.label");
+
+ if (this._currentPasswordInvalid) {
+ document.title = this._str("new.password.title");
+ introText.textContent = this._str("new.password.introText");
+ this._dialog.getButton("finish").label
+ = this._str("new.password.acceptButton");
+ document.getElementById("textBox2Row").hidden = true;
+ }
+ else {
+ document.title = this._str("change.password.title");
+ box2label.value = this._str("new.password.confirm");
+ introText.textContent = this._str("change.password3.introText");
+ warningText.textContent = this._str("change.password.warningText");
+ this._dialog.getButton("finish").label
+ = this._str("change.password.acceptButton");
+ }
+ break;
+ }
+ document.getElementById("change-page")
+ .setAttribute("label", document.title);
+ },
+
+ _clearStatus: function _clearStatus() {
+ this._status.textContent = "";
+ this._statusIcon.removeAttribute("status");
+ },
+
+ _updateStatus: function Change__updateStatus(str, state) {
+ this._updateStatusWithString(this._str(str), state);
+ },
+
+ _updateStatusWithString: function Change__updateStatusWithString(string, state) {
+ this._statusRow.hidden = false;
+ this._status.textContent = string;
+ this._statusIcon.setAttribute("status", state);
+
+ let error = state == "error";
+ this._dialog.getButton("cancel").disabled = !error;
+ this._dialog.getButton("finish").disabled = !error;
+ document.getElementById("printSyncKeyButton").disabled = !error;
+ document.getElementById("saveSyncKeyButton").disabled = !error;
+
+ if (state == "success")
+ window.setTimeout(window.close, 1500);
+ },
+
+ onDialogAccept: function() {
+ switch (this._dialogType) {
+ case "UpdatePassphrase":
+ case "ResetPassphrase":
+ return this.doChangePassphrase();
+ break;
+ case "ChangePassword":
+ return this.doChangePassword();
+ break;
+ }
+ },
+
+ doGeneratePassphrase: function () {
+ let passphrase = Weave.Utils.generatePassphrase();
+ this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase);
+ this._clearStatus();
+ this._dialog.getButton("finish").disabled = false;
+ },
+
+ doChangePassphrase: function Change_doChangePassphrase() {
+ let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
+ if (this._updatingPassphrase) {
+ Weave.Service.identity.syncKey = pp;
+ if (Weave.Service.login()) {
+ this._updateStatus("change.recoverykey.success", "success");
+ Weave.Service.persistLogin();
+ }
+ else {
+ this._updateStatus("new.passphrase.status.incorrect", "error");
+ }
+ }
+ else {
+ this._updateStatus("change.recoverykey.label", "active");
+
+ if (Weave.Service.changePassphrase(pp))
+ this._updateStatus("change.recoverykey.success", "success");
+ else
+ this._updateStatus("change.recoverykey.error", "error");
+ }
+
+ return false;
+ },
+
+ doChangePassword: function Change_doChangePassword() {
+ if (this._currentPasswordInvalid) {
+ Weave.Service.identity.basicPassword = this._firstBox.value;
+ if (Weave.Service.login()) {
+ this._updateStatus("change.password.status.success", "success");
+ Weave.Service.persistLogin();
+ }
+ else {
+ this._updateStatus("new.password.status.incorrect", "error");
+ }
+ }
+ else {
+ this._updateStatus("change.password.status.active", "active");
+
+ if (Weave.Service.changePassword(this._firstBox.value))
+ this._updateStatus("change.password.status.success", "success");
+ else
+ this._updateStatus("change.password.status.error", "error");
+ }
+
+ return false;
+ },
+
+ validate: function () {
+ let valid = false;
+ let errorString = "";
+
+ if (this._dialogType == "ChangePassword") {
+ if (this._currentPasswordInvalid)
+ [valid, errorString] = gSyncUtils.validatePassword(this._firstBox);
+ else
+ [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox);
+ }
+ else {
+ if (!this._updatingPassphrase)
+ return;
+
+ valid = this._passphraseBox.value != "";
+ }
+
+ if (errorString == "")
+ this._clearStatus();
+ else
+ this._updateStatusWithString(errorString, "error");
+
+ this._statusRow.hidden = valid;
+ this._dialog.getButton("finish").disabled = !valid;
+ },
+
+ _str: function Change__string(str) {
+ try {
+ return this._stringBundle.GetStringFromName(str);
+ } catch (e) {
+ Cu.reportError("Missing string: " + str);
+ throw e;
+ }
+ }
+};
diff --git a/comm/suite/components/sync/content/syncGenericChange.xul b/comm/suite/components/sync/content/syncGenericChange.xul
new file mode 100644
index 0000000000..aac4a004fa
--- /dev/null
+++ b/comm/suite/components/sync/content/syncGenericChange.xul
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="change-dialog"
+ windowtype="Weave:ChangeSomething"
+ persist="screenX screenY"
+ onwizardnext="Change.onLoad();"
+ onwizardfinish="return Change.onDialogAccept();">
+
+ <script src="chrome://communicator/content/sync/syncGenericChange.js"/>
+ <script src="chrome://communicator/content/sync/syncUtils.js"/>
+ <script src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="change-page"
+ label="">
+
+ <description id="introText"/>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <grid>
+ <columns>
+ <column align="right"/>
+ <column flex="3"/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row id="textBox1Row" align="center">
+ <label id="textBox1Label" control="textBox1"/>
+ <textbox id="textBox1" type="password" oninput="Change.validate();"/>
+ <spacer/>
+ </row>
+ <row id="textBox2Row" align="center">
+ <label id="textBox2Label" control="textBox2"/>
+ <textbox id="textBox2" type="password" oninput="Change.validate();"/>
+ <spacer/>
+ </row>
+ </rows>
+ </grid>
+
+ <vbox id="passphraseRow">
+ <hbox flex="1">
+ <label id="passphraseLabel" control="passphraseBox"/>
+ <spacer flex="1"/>
+ <label id="generatePassphraseButton"
+ hidden="true"
+ value="&recoveryGenerateNewKey.label;"
+ class="text-link inline-link"
+ onclick="event.stopPropagation();
+ Change.doGeneratePassphrase();"/>
+ </hbox>
+ <textbox id="passphraseBox"
+ flex="1"
+ onfocus="this.select();"
+ oninput="Change.validate();"/>
+ </vbox>
+
+ <vbox id="feedback" pack="center">
+ <hbox id="statusRow" align="center">
+ <image id="statusIcon" class="statusIcon"/>
+ <label id="status" class="status" value=" "/>
+ </hbox>
+ </vbox>
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <hbox id="passphraseBackupButtons"
+ hidden="true"
+ pack="center">
+ <button id="printSyncKeyButton"
+ label="&button.syncKeyBackup.print.label;"
+ accesskey="&button.syncKeyBackup.print.accesskey;"
+ oncommand="gSyncUtils.passphrasePrint('passphraseBox');"/>
+ <button id="saveSyncKeyButton"
+ label="&button.syncKeyBackup.save.label;"
+ accesskey="&button.syncKeyBackup.save.accesskey;"
+ oncommand="gSyncUtils.passphraseSave('passphraseBox');"/>
+ </hbox>
+
+ <vbox id="passphraseHelpBox"
+ hidden="true">
+ <description>
+ &existingRecoveryKey.description;
+ <label class="text-link"
+ href="https://services.mozilla.com/sync/help/manual-setup">
+ &addDevice.showMeHow.label;
+ </label>
+ </description>
+ </vbox>
+
+ <spacer id="passphraseSpacer" flex="1" hidden="true"/>
+
+ <description id="warningText" class="data"/>
+
+ <spacer flex="1"/>
+ </wizardpage>
+</wizard>
diff --git a/comm/suite/components/sync/content/syncKey.xhtml b/comm/suite/components/sync/content/syncKey.xhtml
new file mode 100644
index 0000000000..6ce35fa1cc
--- /dev/null
+++ b/comm/suite/components/sync/content/syncKey.xhtml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+ %syncBrandDTD;
+ <!ENTITY % syncKeyDTD SYSTEM "chrome://communicator/locale/sync/syncKey.dtd">
+ %syncKeyDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&syncKey.page.title;</title>
+ <meta name="robots" content="noindex"/>
+ <style type="text/css">
+ #synckey { font-size: 1.5em; }
+ footer { font-size: 0.67em; }
+ </style>
+</head>
+
+<body>
+<h1>&syncKey.page.title;</h1>
+
+<p id="synckey">SYNCKEY</p>
+
+<p>&syncKey.page.description;</p>
+
+<div id="column1">
+ <h2>&syncKey.keepItSecret.heading;</h2>
+ <p>&syncKey.keepItSecret.description;</p>
+</div>
+
+<div id="column2">
+ <h2>&syncKey.keepItSafe.heading;</h2>
+ <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4.description;</p>
+</div>
+
+<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p>
+
+<footer>
+ &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label;
+</footer>
+
+</body>
+</html>
diff --git a/comm/suite/components/sync/content/syncNotification.xml b/comm/suite/components/sync/content/syncNotification.xml
new file mode 100644
index 0000000000..a33a06c030
--- /dev/null
+++ b/comm/suite/components/sync/content/syncNotification.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
+%notificationDTD;
+]>
+
+<bindings id="notificationBindings" xmlns="http://www.mozilla.org/xbl">
+
+ <binding id="notificationbox" extends="chrome://global/content/bindings/notification.xml#notificationbox">
+ <implementation>
+ <constructor><![CDATA[
+ let localScope = {};
+ ChromeUtils.import("resource://services-common/observers.js", localScope);
+ ChromeUtils.import("resource://services-sync/notifications.js", localScope);
+
+ localScope.Observers.add("weave:notification:added", this.onNotificationAdded, this);
+ localScope.Observers.add("weave:notification:removed", this.onNotificationRemoved, this);
+ localScope.Notifications.notifications.forEach(this._appendNotification, this);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ let localScope = {};
+ ChromeUtils.import("resource://services-common/observers.js", localScope);
+ localScope.Observers.remove("weave:notification:added", this.onNotificationAdded, this);
+ localScope.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this);
+ ]]></destructor>
+
+ <method name="onNotificationAdded">
+ <parameter name="subject"/>
+ <parameter name="data"/>
+ <body><![CDATA[
+ this._appendNotification(subject);
+ ]]></body>
+ </method>
+
+ <method name="onNotificationRemoved">
+ <parameter name="subject"/>
+ <parameter name="data"/>
+ <body><![CDATA[
+ // If the view of the notification hasn't been removed yet, remove it.
+ var notifications = this.allNotifications;
+ for (let notification of notifications) {
+ if (notification.notification == subject) {
+ notification.close();
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_appendNotification">
+ <parameter name="notification"/>
+ <body><![CDATA[
+ var node = this.appendNotification(notification.description,
+ notification.title,
+ notification.iconURL,
+ notification.priority,
+ notification.buttons);
+ node.notification = notification;
+ ]]></body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="notification" extends="chrome://global/content/bindings/notification.xml#notification">
+ <implementation>
+ <method name="close">
+ <body><![CDATA[
+ let localScope = {};
+ ChromeUtils.import("resource://services-sync/notifications.js", localScope);
+ localScope.Notifications.remove(this.notification);
+
+ // We should be able to call the base class's close method here
+ // to remove the notification element from the notification box,
+ // but we can't because of bug 373652, so instead we copied its code
+ // and execute it below.
+ var control = this.control;
+ if (control)
+ control.removeNotification(this);
+ else
+ this.hidden = true;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/components/sync/content/syncQuota.js b/comm/suite/components/sync/content/syncQuota.js
new file mode 100644
index 0000000000..c4d2d03676
--- /dev/null
+++ b/comm/suite/components/sync/content/syncQuota.js
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+
+var gSyncQuota = {
+
+ init: function init() {
+ this.bundle = document.getElementById("quotaStrings");
+ let caption = document.getElementById("treeCaption");
+ caption.textContent = this.bundle.getString("quota.treeCaption.label");
+
+ gUsageTreeView.init();
+ this.tree = document.getElementById("usageTree");
+ this.tree.view = gUsageTreeView;
+
+ this.loadData();
+ },
+
+ loadData: function loadData() {
+ this._usage_req = Weave.Service.getStorageInfo(Weave.INFO_COLLECTION_USAGE,
+ function (error, usage) {
+ delete gSyncQuota._usage_req;
+ // displayUsageData handles null values, so no need to check 'error'.
+ gUsageTreeView.displayUsageData(usage);
+ });
+
+ let usageLabel = document.getElementById("usageLabel");
+ let bundle = this.bundle;
+ this._quota_req = Weave.Service.getStorageInfo(Weave.INFO_QUOTA,
+ function (error, quota) {
+ delete gSyncQuota._quota_req;
+ if (error) {
+ usageLabel.value = bundle.getString("quota.usageError.label");
+ return;
+ }
+ let used = gSyncQuota.convertKB(quota[0]);
+ if (!quota[1]) {
+ // No quota on the server.
+ usageLabel.value = bundle.getFormattedString(
+ "quota.usageNoQuota.label", used);
+ return;
+ }
+ let percent = Math.round(100 * quota[0] / quota[1]);
+ let total = gSyncQuota.convertKB(quota[1]);
+ usageLabel.value = bundle.getFormattedString(
+ "quota.usagePercentage.label", [percent].concat(used).concat(total));
+ });
+ },
+
+ uninit: function uninit() {
+ if (this._usage_req)
+ this._usage_req.abort();
+ if (this._quota_req)
+ this._quota_req.abort();
+ },
+
+ onAccept: function onAccept() {
+ let engines = gUsageTreeView.getEnginesToDisable();
+ for (let engine of engines) {
+ Weave.Service.engineManager.get(engine).enabled = false;
+ }
+ if (engines.length) {
+ // The 'Weave' object will disappear once the window closes.
+ let Service = Weave.Service;
+ Weave.Utils.nextTick(function() { Service.sync(); });
+ }
+ return true;
+ },
+
+ convertKB: function convertKB(value) {
+ return DownloadUtils.convertByteUnits(value * 1024);
+ }
+
+};
+
+var gUsageTreeView = {
+
+ _ignored: {keys: true,
+ meta: true,
+ clients: true},
+
+ /*
+ * Internal data structures underlaying the tree.
+ */
+ _collections: [],
+ _byname: {},
+
+ init: function init() {
+ let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label");
+ for (let engine of Weave.Service.engineManager.getEnabled()) {
+ if (this._ignored[engine.name])
+ continue;
+
+ // Some engines use the same pref, which means they can only be turned on
+ // and off together. We need to combine them here as well.
+ let existing = this._byname[engine.prefName];
+ if (existing) {
+ existing.engines.push(engine.name);
+ continue;
+ }
+
+ let obj = {name: engine.prefName,
+ title: this._collectionTitle(engine),
+ engines: [engine.name],
+ enabled: true,
+ sizeLabel: retrievingLabel};
+ this._collections.push(obj);
+ this._byname[engine.prefName] = obj;
+ }
+ },
+
+ _collectionTitle: function _collectionTitle(engine) {
+ try {
+ return gSyncQuota.bundle.getString(
+ "collection." + engine.prefName + ".label");
+ } catch (ex) {
+ return engine.Name;
+ }
+ },
+
+ /*
+ * Process the quota information as returned by info/collection_usage.
+ */
+ displayUsageData: function displayUsageData(data) {
+ for (let coll of this._collections) {
+ coll.size = 0;
+ // If we couldn't retrieve any data, just blank out the label.
+ if (!data) {
+ coll.sizeLabel = "";
+ continue;
+ }
+
+ for (let engineName of coll.engines)
+ coll.size += data[engineName] || 0;
+ let sizeLabel = "";
+ sizeLabel = gSyncQuota.bundle.getFormattedString(
+ "quota.sizeValueUnit.label", gSyncQuota.convertKB(coll.size));
+ coll.sizeLabel = sizeLabel;
+ }
+ let sizeColumn = this.treeBox.columns.getNamedColumn("size");
+ this.treeBox.invalidateColumn(sizeColumn);
+ },
+
+ /*
+ * Handle click events on the tree.
+ */
+ onTreeClick: function onTreeClick(event) {
+ if (event.button == 2)
+ return;
+
+ let cell = this.treeBox.getCellAt(event.clientX, event.clientY);
+ if (cell.col && cell.col.id == "enabled")
+ this.toggle(cell.row);
+ },
+
+ /*
+ * Toggle enabled state of an engine.
+ */
+ toggle: function toggle(row) {
+ // Update the tree
+ let collection = this._collections[row];
+ collection.enabled = !collection.enabled;
+ this.treeBox.invalidateRow(row);
+
+ // Display which ones will be removed
+ let freeup = 0;
+ let toremove = [];
+ for (let collection of this._collections) {
+ if (collection.enabled)
+ continue;
+ toremove.push(collection.name);
+ freeup += collection.size;
+ }
+
+ let caption = document.getElementById("treeCaption");
+ if (!toremove.length) {
+ caption.className = "";
+ caption.textContent = gSyncQuota.bundle.getString("quota.treeCaption.label");
+ return;
+ }
+
+ toremove = toremove.map(coll => this._byname[coll].title);
+ toremove = toremove.join(gSyncQuota.bundle.getString("quota.list.separator"));
+ caption.textContent = gSyncQuota.bundle.getFormattedString(
+ "quota.removal.label", [toremove]);
+ if (freeup)
+ caption.textContent += gSyncQuota.bundle.getFormattedString(
+ "quota.freeup.label", gSyncQuota.convertKB(freeup));
+ caption.className = "captionWarning";
+ },
+
+ /*
+ * Return a list of engines (or rather their pref names) that should be
+ * disabled.
+ */
+ getEnginesToDisable: function getEnginesToDisable() {
+ return this._collections.filter(coll => !coll.enabled).map(coll => coll.name);
+ },
+
+ // nsITreeView
+
+ get rowCount() {
+ return this._collections.length;
+ },
+
+ getRowProperties: function(index) { return ""; },
+ getCellProperties: function(row, col) { return ""; },
+ getColumnProperties: function(col) { return ""; },
+ isContainer: function(index) { return false; },
+ isContainerOpen: function(index) { return false; },
+ isContainerEmpty: function(index) { return false; },
+ isSeparator: function(index) { return false; },
+ isSorted: function() { return false; },
+ canDrop: function(index, orientation, dataTransfer) { return false; },
+ drop: function(row, orientation, dataTransfer) {},
+ getParentIndex: function(rowIndex) {},
+ hasNextSibling: function(rowIndex, afterIndex) { return false; },
+ getLevel: function(index) { return 0; },
+ getImageSrc: function(row, col) {},
+
+ getCellValue: function(row, col) {
+ return this._collections[row].enabled;
+ },
+
+ getCellText: function getCellText(row, col) {
+ let collection = this._collections[row];
+ switch (col.id) {
+ case "collection":
+ return collection.title;
+ case "size":
+ return collection.sizeLabel;
+ default:
+ return "";
+ }
+ },
+
+ setTree: function setTree(tree) {
+ this.treeBox = tree;
+ },
+
+ toggleOpenState: function(index) {},
+ cycleHeader: function(col) {},
+ selectionChanged: function() {},
+ cycleCell: function(row, col) {},
+ isEditable: function(row, col) { return false; },
+ isSelectable: function (row, col) { return false; },
+ setCellValue: function(row, col, value) {},
+ setCellText: function(row, col, value) {},
+};
diff --git a/comm/suite/components/sync/content/syncQuota.xul b/comm/suite/components/sync/content/syncQuota.xul
new file mode 100644
index 0000000000..f7b1fd7aa4
--- /dev/null
+++ b/comm/suite/components/sync/content/syncQuota.xul
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncQuota.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+<!ENTITY % syncQuotaDTD SYSTEM "chrome://communicator/locale/sync/syncQuota.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncQuotaDTD;
+]>
+<dialog id="quotaDialog"
+ windowtype="Sync:ViewQuota"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gSyncQuota.init();"
+ onunload="gSyncQuota.uninit();"
+ buttons="accept,cancel"
+ title="&quota.dialogTitle.label;"
+ ondialogaccept="return gSyncQuota.onAccept();">
+
+ <script src="chrome://communicator/content/sync/syncQuota.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="quotaStrings"
+ src="chrome://communicator/locale/sync/syncQuota.properties"/>
+ </stringbundleset>
+
+ <label id="usageLabel"
+ value="&quota.retrievingInfo.label;"/>
+ <separator/>
+ <tree id="usageTree"
+ seltype="single"
+ hidecolumnpicker="true"
+ onclick="gUsageTreeView.onTreeClick(event);"
+ flex="1">
+ <treecols>
+ <treecol id="enabled"
+ type="checkbox"
+ fixed="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="collection"
+ label="&quota.typeColumn.label;"
+ flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="size"
+ label="&quota.sizeColumn.label;"
+ flex="1"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <separator/>
+ <description id="treeCaption"/>
+
+</dialog>
diff --git a/comm/suite/components/sync/content/syncSetup.js b/comm/suite/components/sync/content/syncSetup.js
new file mode 100644
index 0000000000..c648948568
--- /dev/null
+++ b/comm/suite/components/sync/content/syncSetup.js
@@ -0,0 +1,961 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// page consts
+
+const INTRO_PAGE = 0;
+const NEW_ACCOUNT_START_PAGE = 1;
+const NEW_ACCOUNT_PP_PAGE = 2;
+const NEW_ACCOUNT_CAPTCHA_PAGE = 3;
+const EXISTING_ACCOUNT_CONNECT_PAGE = 4;
+const EXISTING_ACCOUNT_LOGIN_PAGE = 5;
+const OPTIONS_PAGE = 6;
+const OPTIONS_CONFIRM_PAGE = 7;
+const SETUP_SUCCESS_PAGE = 8;
+
+// Broader than we'd like, but after this changed from api-secure.recaptcha.net
+// we had no choice. At least we only do this for the duration of setup.
+// See discussion in Bugs 508112 and 653307.
+const RECAPTCHA_DOMAIN = "https://www.google.com";
+
+const {Weave} = ChromeUtils.import("resource://services-sync/main.js");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+
+var gSyncSetup = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+ captchaBrowser: null,
+ wizard: null,
+ _disabledSites: [],
+
+ status: {
+ password: false,
+ email: false,
+ server: true
+ },
+
+ get _remoteSites() {
+ return [Weave.Service.serverURL, RECAPTCHA_DOMAIN];
+ },
+
+ get _usingMainServers() {
+ if (this._settingUpNew)
+ return document.getElementById("server").selectedIndex == 0;
+ return document.getElementById("existingServer").selectedIndex == 0;
+ },
+
+ init: function () {
+ let obs = [
+ ["weave:service:changepassphrase", "onResetPassphrase"],
+ ["weave:service:login:start", "onLoginStart"],
+ ["weave:service:login:error", "onLoginEnd"],
+ ["weave:service:login:finish", "onLoginEnd"]];
+
+ // Add the observers now and remove them on unload
+ let self = this;
+ let addRem = function(add) {
+ obs.forEach(function([topic, func]) {
+ //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
+ // of `this`. Fix in a followup. (bug 583347)
+ if (add)
+ Weave.Svc.Obs.add(topic, self[func], self);
+ else
+ Weave.Svc.Obs.remove(topic, self[func], self);
+ });
+ };
+ addRem(true);
+ window.addEventListener("unload", () => addRem(false));
+
+ setTimeout(function () {
+ // Force Service to be loaded so that engines are registered.
+ // See Bug 670082.
+ Weave.Service;
+ }, 0);
+
+ this.captchaBrowser = document.getElementById("captcha");
+ this.wizard = document.getElementById("accountSetup");
+
+ if (window.arguments && window.arguments[0] == true) {
+ // we're resetting sync
+ this._resettingSync = true;
+ this.wizard.pageIndex = OPTIONS_PAGE;
+ }
+ else {
+ this.wizard.canAdvance = false;
+ this.captchaBrowser.addProgressListener(this);
+ Weave.Svc.Prefs.set("firstSync", "notReady");
+ }
+
+ this.wizard.getButton("extra1").label =
+ this._stringBundle.GetStringFromName("button.syncOptions.label");
+
+ // Remember these values because the options pages change them temporarily.
+ this._nextButtonLabel = this.wizard.getButton("next").label;
+ this._nextButtonAccesskey = this.wizard.getButton("next")
+ .getAttribute("accesskey");
+ this._backButtonLabel = this.wizard.getButton("back").label;
+ this._backButtonAccesskey = this.wizard.getButton("back")
+ .getAttribute("accesskey");
+ },
+
+ startNewAccountSetup: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return false;
+ this._settingUpNew = true;
+ this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE;
+ },
+
+ useExistingAccount: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return false;
+ this._settingUpNew = false;
+ this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE;
+ },
+
+ resetPassphrase: function resetPassphrase() {
+ // Apply the existing form fields so that
+ // Weave.Service.changePassphrase() has the necessary credentials.
+ Weave.Service.identity.account = document.getElementById("existingAccountName").value;
+ Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value;
+
+ // Generate a new passphrase so that Weave.Service.login() will
+ // actually do something.
+ let passphrase = Weave.Utils.generatePassphrase();
+ Weave.Service.identity.syncKey = passphrase;
+
+ // Only open the dialog if username + password are actually correct.
+ Weave.Service.login();
+ if (![Weave.LOGIN_FAILED_INVALID_PASSPHRASE,
+ Weave.LOGIN_FAILED_NO_PASSPHRASE,
+ Weave.LOGIN_SUCCEEDED].includes(Weave.Status.login))
+ return;
+
+ // Hide any errors about the passphrase, we know it's not right.
+ let feedback = document.getElementById("existingPassphraseFeedbackRow");
+ feedback.hidden = true;
+ let el = document.getElementById("existingPassphrase");
+ el.value = Weave.Utils.hyphenatePassphrase(passphrase);
+
+ // changePassphrase() will sync, make sure we set the "firstSync" pref
+ // according to the user's pref.
+ Weave.Svc.Prefs.reset("firstSync");
+ this.setupInitialSync();
+ gSyncUtils.resetPassphrase(true);
+ },
+
+ onResetPassphrase: function () {
+ document.getElementById("existingPassphrase").value =
+ Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
+ this.checkFields();
+ this.wizard.advance();
+ },
+
+ onLoginStart: function () {
+ this.toggleLoginFeedback(false);
+ },
+
+ onLoginEnd: function () {
+ this.toggleLoginFeedback(true);
+ },
+
+ toggleLoginFeedback: function (stop) {
+ document.getElementById("login-throbber").hidden = stop;
+ let password = document.getElementById("existingPasswordFeedbackRow");
+ let server = document.getElementById("existingServerFeedbackRow");
+ let passphrase = document.getElementById("existingPassphraseFeedbackRow");
+
+ if (!stop || Weave.Status.login == Weave.LOGIN_SUCCEEDED) {
+ password.hidden = server.hidden = passphrase.hidden = true;
+ return;
+ }
+
+ let feedback;
+ switch (Weave.Status.login) {
+ case Weave.LOGIN_FAILED_NETWORK_ERROR:
+ case Weave.LOGIN_FAILED_SERVER_ERROR:
+ feedback = server;
+ break;
+ case Weave.LOGIN_FAILED_LOGIN_REJECTED:
+ case Weave.LOGIN_FAILED_NO_USERNAME:
+ case Weave.LOGIN_FAILED_NO_PASSWORD:
+ feedback = password;
+ break;
+ case Weave.LOGIN_FAILED_INVALID_PASSPHRASE:
+ feedback = passphrase;
+ break;
+ }
+ this._setFeedbackMessage(feedback, false, Weave.Status.login);
+ },
+
+ setupInitialSync: function () {
+ let action = document.getElementById("mergeChoiceRadio").value;
+ switch (action) {
+ case "resetClient":
+ // if we're not resetting sync, we don't need to explicitly
+ // call resetClient
+ if (!this._resettingSync)
+ return;
+ // otherwise, fall through
+ case "wipeClient":
+ case "wipeRemote":
+ Weave.Svc.Prefs.set("firstSync", action);
+ break;
+ }
+ },
+
+ // fun with validation!
+ checkFields: function () {
+ this.wizard.canAdvance = this.readyToAdvance();
+ },
+
+ readyToAdvance: function () {
+ switch (this.wizard.pageIndex) {
+ case INTRO_PAGE:
+ return false;
+ case NEW_ACCOUNT_START_PAGE:
+ for (let i in this.status) {
+ if (!this.status[i])
+ return false;
+ }
+ if (this._usingMainServers)
+ return document.getElementById("tos").checked;
+
+ return true;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ let hasUser = document.getElementById("existingAccountName").value != "";
+ let hasPass = document.getElementById("existingPassword").value != "";
+ let hasKey = document.getElementById("existingPassphrase").value != "";
+ if (hasUser && hasPass && hasKey) {
+ if (this._usingMainServers)
+ return true;
+
+ if (this._validateServer(document.getElementById("existingServer"), false))
+ return true;
+ }
+ return false;
+ }
+ // Default, e.g. wizard's special page -1 etc.
+ return true;
+ },
+
+ onEmailInput: function () {
+ // Check account validity when the user stops typing for 1 second.
+ if (this._checkAccountTimer)
+ window.clearTimeout(this._checkAccountTimer);
+ this._checkAccountTimer = window.setTimeout(function () {
+ gSyncSetup.checkAccount();
+ }, 1000);
+ },
+
+ checkAccount: function() {
+ delete this._checkAccountTimer;
+ let value = Weave.Utils.normalizeAccount(
+ document.getElementById("weaveEmail").value);
+ if (!value) {
+ this.status.email = false;
+ this.checkFields();
+ return;
+ }
+
+ let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ let feedback = document.getElementById("emailFeedbackRow");
+ let valid = re.test(value);
+
+ let str = "";
+ if (!valid) {
+ str = "invalidEmail.label";
+ } else {
+ let availCheck = Weave.Service.checkAccount(value);
+ valid = availCheck == "available";
+ if (!valid) {
+ if (availCheck == "notAvailable")
+ str = "usernameNotAvailable.label";
+ else
+ str = availCheck;
+ }
+ }
+
+ this._setFeedbackMessage(feedback, valid, str);
+ this.status.email = valid;
+ if (valid)
+ Weave.Service.identity.account = value;
+ this.checkFields();
+ },
+
+ onPasswordChange: function () {
+ let password = document.getElementById("weavePassword");
+ let valid, str;
+ if (password.value == document.getElementById("weavePassphrase").value) {
+ // xxxmpc - hack, sigh
+ valid = false;
+ str = Weave.Utils.getErrorString("change.password.pwSameAsRecoveryKey");
+ }
+ else {
+ let pwconfirm = document.getElementById("weavePasswordConfirm");
+ [valid, str] = gSyncUtils.validatePassword(password, pwconfirm);
+ }
+
+ let feedback = document.getElementById("passwordFeedbackRow");
+ this._setFeedback(feedback, valid, str);
+
+ this.status.password = valid;
+ this.checkFields();
+ },
+
+ onPassphraseGenerate: function () {
+ let passphrase = Weave.Utils.generatePassphrase();
+ Weave.Service.identity.syncKey = passphrase;
+ let el = document.getElementById("weavePassphrase");
+ el.value = Weave.Utils.hyphenatePassphrase(passphrase);
+ },
+
+ onPageShow: function() {
+ switch (this.wizard.pageIndex) {
+ case INTRO_PAGE:
+ this.wizard.getButton("next").hidden = true;
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("extra1").hidden = true;
+ break;
+ case NEW_ACCOUNT_PP_PAGE:
+ document.getElementById("saveSyncKeyButton").focus();
+ let el = document.getElementById("weavePassphrase");
+ if (!el.value)
+ this.onPassphraseGenerate();
+ this.checkFields();
+ break;
+ case NEW_ACCOUNT_START_PAGE:
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.canRewind = true;
+ this.checkFields();
+ break;
+ case EXISTING_ACCOUNT_CONNECT_PAGE:
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.canAdvance = false;
+ this.wizard.canRewind = true;
+ this.startEasySetup();
+ break;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ this.wizard.canRewind = true;
+ this.checkFields();
+ break;
+ case SETUP_SUCCESS_PAGE:
+ this.wizard.canRewind = false;
+ this.wizard.canAdvance = true;
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("next").hidden = true;
+ this.wizard.getButton("cancel").hidden = true;
+ this.wizard.getButton("finish").hidden = false;
+ this._handleSuccess();
+ break;
+ case OPTIONS_PAGE:
+ this.wizard.canRewind = false;
+ this.wizard.canAdvance = true;
+ if (!this._resettingSync) {
+ this.wizard.getButton("next").label =
+ this._stringBundle.GetStringFromName("button.syncOptionsDone.label");
+ this.wizard.getButton("next").removeAttribute("accesskey");
+ }
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("cancel").hidden = !this._resettingSync;
+ this.wizard.getButton("extra1").hidden = true;
+ document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
+ document.getElementById("syncOptions").collapsed = this._resettingSync;
+ document.getElementById("mergeOptions").collapsed = this._settingUpNew;
+ break;
+ case OPTIONS_CONFIRM_PAGE:
+ this.wizard.canRewind = true;
+ this.wizard.canAdvance = true;
+ this.wizard.getButton("back").label =
+ this._stringBundle.GetStringFromName("button.syncOptionsCancel.label");
+ this.wizard.getButton("back").removeAttribute("accesskey");
+ this.wizard.getButton("back").hidden = this._resettingSync;
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("finish").hidden = true;
+ break;
+ }
+ },
+
+ onWizardAdvance: function () {
+ // Check pageIndex so we don't prompt before the Sync setup wizard appears.
+ // This is a fallback in case the Master Password gets locked mid-wizard.
+ if (this.wizard.pageIndex >= 0 && !Weave.Utils.ensureMPUnlocked())
+ return false;
+
+ if (!this.wizard.pageIndex)
+ return true;
+
+ switch (this.wizard.pageIndex) {
+ case NEW_ACCOUNT_START_PAGE:
+ // If the user selects Next (e.g. by hitting enter) when we haven't
+ // executed the delayed checks yet, execute them immediately.
+ if (this._checkAccountTimer)
+ this.checkAccount();
+ if (this._checkServerTimer)
+ this.checkServer();
+ return this.wizard.canAdvance;
+ case NEW_ACCOUNT_CAPTCHA_PAGE:
+ let doc = this.captchaBrowser.contentDocument;
+ let getField = function getField(field) {
+ let node = doc.getElementById("recaptcha_" + field + "_field");
+ return node && node.value;
+ };
+
+ // Display throbber
+ let feedback = document.getElementById("captchaFeedback");
+ let image = feedback.firstChild;
+ let label = image.nextSibling;
+ image.setAttribute("status", "active");
+ label.value = this._stringBundle.GetStringFromName("verifying.label");
+ feedback.hidden = false;
+
+ let password = document.getElementById("weavePassword").value;
+ let email = Weave.Utils.normalizeAccount(
+ document.getElementById("weaveEmail").value);
+ let challenge = getField("challenge");
+ let response = getField("response");
+
+ let error = Weave.Service.createAccount(email, password,
+ challenge, response);
+
+ if (error == null) {
+ Weave.Service.identity.account = email;
+ Weave.Service.identity.basicPassword = password;
+ this._handleNoScript(false);
+ this.wizard.pageIndex = SETUP_SUCCESS_PAGE;
+ return false;
+ }
+
+ image.setAttribute("status", "error");
+ label.value = Weave.Utils.getErrorString(error);
+ return false;
+ case NEW_ACCOUNT_PP_PAGE:
+ // Time to load the captcha.
+ // First check for NoScript and whitelist the right sites.
+ this._handleNoScript(true);
+ this.captchaBrowser.loadURI(Weave.Service.miscAPI + "captcha_html");
+ break;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ Weave.Service.identity.account = Weave.Utils.normalizeAccount(
+ document.getElementById("existingAccountName").value);
+ Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value;
+ let pp = document.getElementById("existingPassphrase").value;
+ Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp);
+ if (Weave.Service.login())
+ this.wizard.pageIndex = SETUP_SUCCESS_PAGE;
+ return false;
+ case OPTIONS_PAGE:
+ let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
+ // No confirmation needed on new account setup or merge option
+ // with existing account.
+ if (this._settingUpNew || (!this._resettingSync && desc == 0))
+ return this.returnFromOptions();
+ return this._handleChoice();
+ case OPTIONS_CONFIRM_PAGE:
+ if (this._resettingSync) {
+ this.onWizardFinish();
+ window.close();
+ return false;
+ }
+ return this.returnFromOptions();
+ }
+ return true;
+ },
+
+ onWizardBack: function () {
+ switch (this.wizard.pageIndex) {
+ case NEW_ACCOUNT_START_PAGE:
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ this.wizard.pageIndex = INTRO_PAGE;
+ return false;
+ case EXISTING_ACCOUNT_CONNECT_PAGE:
+ this.abortEasySetup();
+ this.wizard.pageIndex = INTRO_PAGE;
+ return false;
+ case OPTIONS_CONFIRM_PAGE:
+ // Backing up from the confirmation page = resetting first sync to merge.
+ document.getElementById("mergeChoiceRadio").selectedIndex = 0;
+ return this.returnFromOptions();
+ }
+ return true;
+ },
+
+ onWizardFinish: function () {
+ this.setupInitialSync();
+
+ if (!this._resettingSync) {
+ function isChecked(element) {
+ return document.getElementById(element).hasAttribute("checked");
+ }
+
+ let prefs = ["engine.bookmarks", "engine.passwords", "engine.history",
+ "engine.tabs", "engine.prefs", "engine.addons"];
+ for (let i = 0; i < prefs.length; i++) {
+ Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i]));
+ }
+ this._handleNoScript(false);
+ if (Weave.Svc.Prefs.get("firstSync", "") == "notReady")
+ Weave.Svc.Prefs.reset("firstSync");
+
+ Weave.Service.persistLogin();
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+ if (this._settingUpNew)
+ gSyncUtils.openFirstClientFirstrun();
+ else
+ gSyncUtils.openAddedClientFirstrun();
+ }
+
+ if (!Weave.Service.isLoggedIn)
+ Weave.Service.login();
+
+ Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
+ },
+
+ onWizardCancel: function () {
+ if (this._resettingSync)
+ return;
+
+ if (this.wizard.pageIndex == SETUP_SUCCESS_PAGE) {
+ this.onWizardFinish();
+ return;
+ }
+ this.abortEasySetup();
+ this._handleNoScript(false);
+ Weave.Service.startOver();
+ },
+
+ onSyncOptions: function () {
+ this._beforeOptionsPage = this.wizard.pageIndex;
+ this.wizard.pageIndex = OPTIONS_PAGE;
+ },
+
+ returnFromOptions: function() {
+ this.wizard.getButton("next").label = this._nextButtonLabel;
+ this.wizard.getButton("next").setAttribute("accesskey",
+ this._nextButtonAccesskey);
+ this.wizard.getButton("back").label = this._backButtonLabel;
+ this.wizard.getButton("back").setAttribute("accesskey",
+ this._backButtonAccesskey);
+ this.wizard.getButton("cancel").hidden = false;
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.pageIndex = this._beforeOptionsPage;
+ return false;
+ },
+
+ startEasySetup: function () {
+ // Don't do anything if we have a client already (e.g. we went to
+ // Sync Options and just came back).
+ if (this._jpakeclient)
+ return;
+
+ // When onAbort is called, Weave may already be gone.
+ const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
+
+ let self = this;
+ this._jpakeclient = new Weave.JPAKEClient({
+ displayPIN: function displayPIN(pin) {
+ document.getElementById("easySetupPIN1").value = pin.slice(0, 4);
+ document.getElementById("easySetupPIN2").value = pin.slice(4, 8);
+ document.getElementById("easySetupPIN3").value = pin.slice(8);
+ },
+
+ onPairingStart: function onPairingStart() {},
+
+ onPaired: function onPaired() {},
+
+ onComplete: function onComplete(credentials) {
+ Weave.Service.identity.account = credentials.account;
+ Weave.Service.identity.basicPassword = credentials.password;
+ Weave.Service.identity.syncKey = credentials.synckey;
+ Weave.Service.serverURL = credentials.serverURL;
+ self.wizard.pageIndex = SETUP_SUCCESS_PAGE;
+ },
+
+ onAbort: function onAbort(error) {
+ delete self._jpakeclient;
+
+ // Ignore if wizard is aborted.
+ if (error == JPAKE_ERROR_USERABORT)
+ return;
+
+ // Automatically go to manual setup if we couldn't acquire a channel.
+ if (error == Weave.JPAKE_ERROR_CHANNEL) {
+ self.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
+ return;
+ }
+
+ // Restart on all other errors.
+ self.startEasySetup();
+ }
+ });
+ this._jpakeclient.receiveNoPIN();
+ },
+
+ abortEasySetup: function () {
+ document.getElementById("easySetupPIN1").value = "";
+ document.getElementById("easySetupPIN2").value = "";
+ document.getElementById("easySetupPIN3").value = "";
+ if (!this._jpakeclient)
+ return;
+
+ this._jpakeclient.abort();
+ delete this._jpakeclient;
+ },
+
+ manualSetup: function () {
+ this.abortEasySetup();
+ this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
+ },
+
+ // _handleNoScript is needed because it blocks the captcha. So we temporarily
+ // allow the necessary sites so that we can verify the user is in fact a human.
+ // This was done with the help of Giorgio (NoScript author). See bug 508112.
+ _handleNoScript: function (addExceptions) {
+ // if NoScript isn't installed, or is disabled, bail out.
+ if (!("@maone.net/noscript-service;1" in Cc))
+ return;
+
+ let ns = Cc["@maone.net/noscript-service;1"].getService().wrappedJSObject;
+ if (addExceptions) {
+ this._remoteSites.forEach(function(site) {
+ site = ns.getSite(site);
+ if (!ns.isJSEnabled(site)) {
+ this._disabledSites.push(site); // save status
+ ns.setJSEnabled(site, true); // allow site
+ }
+ }, this);
+ }
+ else {
+ this._disabledSites.forEach(function(site) {
+ ns.setJSEnabled(site, false);
+ });
+ this._disabledSites = [];
+ }
+ },
+
+ _updateControl: function(controlId) {
+ let control = document.getElementById(controlId);
+ if (control.selectedIndex == 0) {
+ control.editable = false;
+ Weave.Svc.Prefs.reset("serverURL");
+ } else {
+ // Prevent double selection upon using down key.
+ control.activeChild = null;
+ control.editable = true;
+ control.value = "";
+ }
+ // Force a style flush to ensure that the binding is attached.
+ control.clientTop;
+ control.focus();
+ return control;
+ },
+
+ onExistingServerCommand: function () {
+ this._updateControl("existingServer");
+ document.getElementById("existingServerFeedbackRow").hidden = true;
+ this.checkFields();
+ },
+
+ onExistingServerInput: function () {
+ // Check custom server validity when the user stops typing for 1 second.
+ if (this._existingServerTimer)
+ window.clearTimeout(this._existingServerTimer);
+ this._existingServerTimer = window.setTimeout(function () {
+ gSyncSetup.checkFields();
+ }, 1000);
+ },
+
+ onServerCommand: function () {
+ document.getElementById("TOSRow").hidden = !this._usingMainServers;
+ let control = this._updateControl("server");
+ if (control.selectedIndex != 0) {
+ // checkServer() will call checkAccount() and checkFields().
+ this.checkServer();
+ return;
+ }
+ this.checkAccount();
+ this.status.server = true;
+ document.getElementById("serverFeedbackRow").hidden = true;
+ this.checkFields();
+ },
+
+ onServerInput: function () {
+ // Check custom server validity when the user stops typing for 1 second.
+ if (this._checkServerTimer)
+ window.clearTimeout(this._checkServerTimer);
+ this._checkServerTimer = window.setTimeout(function () {
+ gSyncSetup.checkServer();
+ }, 1000);
+ },
+
+ checkServer: function () {
+ delete this._checkServerTimer;
+ let el = document.getElementById("server");
+ let valid = false;
+ let feedback = document.getElementById("serverFeedbackRow");
+
+ let str = "";
+ if (el.value) {
+ valid = this._validateServer(el, true);
+ let str = valid ? "" : "serverInvalid.label";
+ this._setFeedbackMessage(feedback, valid, str);
+ }
+ else {
+ this._setFeedbackMessage(feedback, true);
+ }
+
+ // Recheck account against the new server.
+ if (valid)
+ this.checkAccount();
+
+ this.status.server = valid;
+ this.checkFields();
+ },
+
+ // xxxmpc - checkRemote is a hack, we can't verify a minimal server is live
+ // without auth, so we won't validate in the existing-server case.
+ _validateServer: function (element, checkRemote) {
+ let valid = false;
+ let val = element.value;
+ if (!val)
+ return false;
+
+ let uri = Weave.Utils.makeURI(val);
+
+ if (!uri)
+ uri = Weave.Utils.makeURI("https://" + val);
+
+ if (uri && checkRemote) {
+ function isValid(uri) {
+ Weave.Service.serverURL = uri.spec;
+ let check = Weave.Service.checkAccount("a");
+ return (check == "available" || check == "notAvailable");
+ }
+
+ if (uri.schemeIs("http")) {
+ uri.scheme = "https";
+ if (isValid(uri))
+ valid = true;
+ else
+ // setting the scheme back to http
+ uri.scheme = "http";
+ }
+ if (!valid)
+ valid = isValid(uri);
+ }
+ else if (uri) {
+ valid = true;
+ Weave.Service.serverURL = uri.spec;
+ }
+
+ if (valid)
+ element.value = Weave.Service.serverURL;
+ else
+ Weave.Svc.Prefs.reset("serverURL");
+
+ return valid;
+ },
+
+ _handleSuccess: function() {
+ let self = this;
+ function fill(id, string) {
+ document.getElementById(id).textContent =
+ string ? self._stringBundle.GetStringFromName(string) : "";
+ }
+
+ fill("firstSyncAction", "");
+ fill("firstSyncActionWarning", "");
+ if (this._settingUpNew) {
+ fill("firstSyncAction", "newAccount.action.label");
+ fill("firstSyncActionChange", "newAccount.change.label");
+ return;
+ }
+ fill("firstSyncActionChange", "existingAccount.change.label");
+ let action = document.getElementById("mergeChoiceRadio").value;
+ let id = action == "resetClient" ? "firstSyncAction" : "firstSyncActionWarning";
+ fill(id, action + ".change.label");
+ },
+
+ _handleChoice: function () {
+ let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
+ document.getElementById("chosenActionDeck").selectedIndex = desc;
+ switch (desc) {
+ case 1:
+ if (this._case1Setup)
+ break;
+
+ let places_db = PlacesUtils.history
+ .QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ if (Weave.Service.engineManager.get("history").enabled) {
+ let daysOfHistory = 0;
+ let stm = places_db.createStatement(
+ "SELECT ROUND(( " +
+ "strftime('%s','now','localtime','utc') - " +
+ "( " +
+ "SELECT visit_date FROM moz_historyvisits " +
+ "ORDER BY visit_date ASC LIMIT 1 " +
+ ")/1000000 " +
+ ")/86400) AS daysOfHistory ");
+
+ if (stm.step())
+ daysOfHistory = stm.getInt32(0);
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("historyCount").value =
+ PluralForm.get(daysOfHistory,
+ this._stringBundle.GetStringFromName("historyDaysCount.label"))
+ .replace("%S", daysOfHistory)
+ .replace("#1", daysOfHistory);
+ } else {
+ document.getElementById("historyCount").hidden = true;
+ }
+
+ if (Weave.Service.engineManager.get("bookmarks").enabled) {
+ let bookmarks = 0;
+ let stm = places_db.createStatement(
+ "SELECT count(*) AS bookmarks " +
+ "FROM moz_bookmarks b " +
+ "LEFT JOIN moz_bookmarks t ON " +
+ "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag");
+ stm.params.tag = PlacesUtils.tagsFolderId;
+ if (stm.executeStep())
+ bookmarks = stm.row.bookmarks;
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("bookmarkCount").value =
+ PluralForm.get(bookmarks,
+ this._stringBundle.GetStringFromName("bookmarksCount.label"))
+ .replace("%S", bookmarks)
+ .replace("#1", bookmarks);
+ } else {
+ document.getElementById("bookmarkCount").hidden = true;
+ }
+
+ if (Weave.Service.engineManager.get("passwords").enabled) {
+ let logins = Services.logins.getAllLogins({});
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("passwordCount").value =
+ PluralForm.get(logins.length,
+ this._stringBundle.GetStringFromName("passwordsCount.label"))
+ .replace("%S", logins.length)
+ .replace("#1", logins.length);
+ } else {
+ document.getElementById("passwordCount").hidden = true;
+ }
+
+ let addonsEngine = Weave.Service.engineManager.get("addons");
+ if (addonsEngine.enabled) {
+ let ids = addonsEngine._store.getAllIDs();
+ let blessedcount = Object.keys(ids).filter(id => ids[id]).length;
+ // Bug 600141 does not apply as this does not have to support existing strings.
+ document.getElementById("addonCount").value =
+ PluralForm.get(blessedcount,
+ this._stringBundle.GetStringFromName("addonsCount.label"))
+ .replace("#1", blessedcount);
+ } else {
+ document.getElementById("addonCount").hidden = true;
+ }
+
+ if (!Weave.Service.engineManager.get("prefs").enabled) {
+ document.getElementById("prefsWipe").hidden = true;
+ }
+
+ this._case1Setup = true;
+ break;
+ case 2:
+ if (this._case2Setup)
+ break;
+ let count = 0;
+ function appendNode(label) {
+ let box = document.getElementById("clientList");
+ let node = document.createElement("label");
+ node.setAttribute("value", label);
+ node.setAttribute("class", "data indent");
+ box.appendChild(node);
+ }
+
+ for (let name of Weave.Service.clientsEngine.stats.names) {
+ // Don't list the current client
+ if (name == Weave.Service.clientsEngine.localName)
+ continue;
+
+ // Only show the first several client names
+ if (++count <= 5)
+ appendNode(name);
+ }
+ if (count > 5) {
+ // Support %S for historical reasons (see bug 600141)
+ let label =
+ PluralForm.get(count - 5,
+ this._stringBundle.GetStringFromName("additionalClientCount.label"))
+ .replace("%S", count - 5)
+ .replace("#1", count - 5);
+ appendNode(label);
+ }
+ this._case2Setup = true;
+ break;
+ }
+
+ return true;
+ },
+
+ // sets class and string on a feedback element
+ // if no property string is passed in, we clear label/style
+ _setFeedback: function (element, success, string) {
+ element.hidden = success || !string;
+ let classname = success ? "success" : "error";
+ let image = element.getElementsByAttribute("class", "statusIcon")[0];
+ image.setAttribute("status", classname);
+ let label = element.getElementsByAttribute("class", "status")[0];
+ label.value = string;
+ },
+
+ // shim
+ _setFeedbackMessage: function (element, success, string) {
+ let str = "";
+ if (string) {
+ try {
+ str = this._stringBundle.GetStringFromName(string);
+ } catch(e) {}
+
+ if (!str)
+ str = Weave.Utils.getErrorString(string);
+ }
+ this._setFeedback(element, success, str);
+ },
+
+ onStateChange: function(webProgress, request, stateFlags, status) {
+ // We're only looking for the end of the frame load
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0)
+ return;
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0)
+ return;
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0)
+ return;
+
+ // If we didn't find the captcha, assume it's not needed and move on
+ if (request.QueryInterface(Ci.nsIHttpChannel).responseStatus == 404)
+ this.onWizardAdvance();
+ },
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ onLocationChange: function () {}
+};
+
+// onWizardAdvance() and onPageShow() are run before init(), so we'll set
+// wizard & _stringBundle up as lazy getters.
+XPCOMUtils.defineLazyGetter(gSyncSetup, "wizard", function() {
+ return document.getElementById("accountSetup");
+});
+XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() {
+ return Services.strings.createBundle("chrome://communicator/locale/sync/syncSetup.properties");
+});
diff --git a/comm/suite/components/sync/content/syncSetup.xul b/comm/suite/components/sync/content/syncSetup.xul
new file mode 100644
index 0000000000..2df682bb82
--- /dev/null
+++ b/comm/suite/components/sync/content/syncSetup.xul
@@ -0,0 +1,482 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard id="accountSetup" title="&accountSetupTitle.label;"
+ windowtype="Weave:AccountSetup"
+ persist="screenX screenY"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onwizardnext="return gSyncSetup.onWizardAdvance();"
+ onwizardback="return gSyncSetup.onWizardBack();"
+ onwizardfinish="gSyncSetup.onWizardFinish();"
+ onwizardcancel="gSyncSetup.onWizardCancel();"
+ onload="gSyncSetup.init();">
+
+ <script src="chrome://communicator/content/sync/syncSetup.js"/>
+ <script src="chrome://communicator/content/sync/syncUtils.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+ <script src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="pickSetupType"
+ label="&syncBrand.fullName.label;"
+ onpageshow="gSyncSetup.onPageShow();">
+ <vbox align="center" flex="1">
+ <description id="pickSetupDesc">
+ &setup.pickSetupType.description;
+ </description>
+ <spacer flex="1"/>
+ <button id="newAccount"
+ class="accountChoiceButton"
+ label="&button.createNewAccount.label;"
+ oncommand="gSyncSetup.startNewAccountSetup();"
+ align="center"/>
+ <spacer flex="3"/>
+ </vbox>
+ <separator class="groove"/>
+ <vbox align="center" flex="1">
+ <spacer flex="3"/>
+ <label value="&setup.haveAccount.label;"/>
+ <spacer flex="1"/>
+ <button id="existingAccount"
+ class="accountChoiceButton"
+ label="&button.connect.label;"
+ oncommand="gSyncSetup.useExistingAccount();"/>
+ <spacer flex="3"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="newAccountStart"
+ label="&setup.newAccountDetailsPage.title.label;"
+ onextra1="gSyncSetup.onSyncOptions();"
+ onpageshow="gSyncSetup.onPageShow();">
+ <grid>
+ <columns>
+ <column/>
+ <column class="inputColumn" flex="1"/>
+ </columns>
+ <rows>
+ <row id="emailRow" align="center">
+ <label value="&setup.emailAddress.label;"
+ accesskey="&setup.emailAddress.accesskey;"
+ control="weaveEmail"/>
+ <textbox id="weaveEmail"
+ oninput="gSyncSetup.onEmailInput();"/>
+ </row>
+ <row id="emailFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row id="passwordRow" align="center">
+ <label value="&signIn.password.label;"
+ accesskey="&signIn.password.accesskey;"
+ control="weavePassword"/>
+ <textbox id="weavePassword"
+ type="password"
+ onchange="gSyncSetup.onPasswordChange();"/>
+ </row>
+ <row id="confirmRow" align="center">
+ <label value="&setup.confirmPassword.label;"
+ accesskey="&setup.confirmPassword.accesskey;"
+ control="weavePasswordConfirm"/>
+ <textbox id="weavePasswordConfirm"
+ type="password"
+ onchange="gSyncSetup.onPasswordChange();"/>
+ </row>
+ <row id="passwordFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row align="center">
+ <label control="server"
+ accesskey="&server.accesskey;"
+ value="&server.label;"/>
+ <menulist id="server"
+ oncommand="gSyncSetup.onServerCommand();"
+ oninput="gSyncSetup.onServerInput();">
+ <menupopup>
+ <menuitem label="&serverType.main.label;"/>
+ <menuitem label="&serverType.custom2.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="serverFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row id="TOSRow" align="center">
+ <spacer/>
+ <hbox align="center">
+ <checkbox id="tos"
+ accesskey="&setup.tosAgree1.accesskey;"
+ oncommand="this.focus(); gSyncSetup.checkFields();"/>
+ <description id="tosDesc"
+ flex="1"
+ onclick="document.getElementById('tos').focus();
+ document.getElementById('tos').click();">
+ &setup.tosAgree1.label;
+ <label class="text-link inline-link"
+ onclick="event.stopPropagation(); gSyncUtils.openToS();">
+ &setup.tosLink.label;
+ </label>
+ &setup.tosAgree2.label;
+ <label class="text-link inline-link"
+ onclick="event.stopPropagation();
+ gSyncUtils.openPrivacyPolicy();">
+ &setup.ppLink.label;
+ </label>
+ &setup.tosAgree3.label;
+ </description>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ </wizardpage>
+
+ <wizardpage id="newSyncKey"
+ label="&setup.newRecoveryKeyPage.title.label;"
+ onextra1="gSyncSetup.onSyncOptions();"
+ onpageshow="gSyncSetup.onPageShow();">
+ <description>
+ &setup.newRecoveryKeyPage.description.label;
+ </description>
+ <spacer/>
+
+ <groupbox>
+ <label value="&recoveryKeyEntry.label;"
+ accesskey="&recoveryKeyEntry.accesskey;"
+ control="weavePassphrase"/>
+ <textbox id="weavePassphrase"
+ readonly="true"
+ onfocus="this.select();"/>
+ </groupbox>
+
+ <groupbox align="center">
+ <description>&recoveryKeyBackup.description;</description>
+ <hbox>
+ <button id="printSyncKeyButton"
+ label="&button.syncKeyBackup.print.label;"
+ accesskey="&button.syncKeyBackup.print.accesskey;"
+ oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
+ <button id="saveSyncKeyButton"
+ label="&button.syncKeyBackup.save.label;"
+ accesskey="&button.syncKeyBackup.save.accesskey;"
+ oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/>
+ </hbox>
+ </groupbox>
+ </wizardpage>
+
+ <wizardpage id="captchaEntry"
+ label="&setup.captchaPage2.title.label;"
+ onextra1="gSyncSetup.onSyncOptions();">
+ <vbox flex="1" align="center">
+ <browser height="150"
+ width="450"
+ id="captcha"
+ type="content"
+ disablehistory="true"/>
+ <spacer flex="1"/>
+ <hbox id="captchaFeedback" hidden="true">
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ <spacer flex="3"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="addDevice"
+ label="&addDevice.title.label;"
+ onextra1="gSyncSetup.onSyncOptions();"
+ onpageshow="gSyncSetup.onPageShow();">
+ <description>
+ &addDevice.setup.description.label;
+ <label class="text-link"
+ value="&addDevice.showMeHow.label;"
+ href="https://services.mozilla.com/sync/help/easy-setup"/>
+ </description>
+ <description>&addDevice.setup.enterCode.label;</description>
+ <spacer flex="1"/>
+ <vbox align="center" flex="1">
+ <textbox id="easySetupPIN1"
+ class="pin"
+ value=""
+ size="4"
+ disabled="true"/>
+ <textbox id="easySetupPIN2"
+ class="pin"
+ value=""
+ size="4"
+ disabled="true"/>
+ <textbox id="easySetupPIN3"
+ class="pin"
+ value=""
+ size="4"
+ disabled="true"/>
+ </vbox>
+ <spacer flex="3"/>
+ <label class="text-link"
+ value="&addDevice.dontHaveDevice.label;"
+ onclick="gSyncSetup.manualSetup();"/>
+ </wizardpage>
+
+ <wizardpage id="existingAccount"
+ label="&setup.signInPage.title.label;"
+ onextra1="gSyncSetup.onSyncOptions();"
+ onpageshow="gSyncSetup.onPageShow();">
+ <grid>
+ <columns>
+ <column/>
+ <column class="inputColumn" flex="1"/>
+ </columns>
+ <rows>
+ <row id="existingAccountRow" align="center">
+ <label id="existingAccountLabel"
+ value="&signIn.account2.label;"
+ accesskey="&signIn.account2.accesskey;"
+ control="existingAccountName"/>
+ <textbox id="existingAccountName"
+ oninput="gSyncSetup.checkFields(event);"
+ onchange="gSyncSetup.checkFields(event);"/>
+ </row>
+ <row id="existingPasswordRow" align="center">
+ <label id="existingPasswordLabel"
+ value="&signIn.password.label;"
+ accesskey="&signIn.password.accesskey;"
+ control="existingPassword"/>
+ <textbox id="existingPassword"
+ type="password"
+ onkeyup="gSyncSetup.checkFields(event);"
+ onchange="gSyncSetup.checkFields(event);"/>
+ </row>
+ <row id="existingPasswordFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row align="center">
+ <spacer/>
+ <label class="text-link"
+ value="&resetPassword.label;"
+ onclick="gSyncUtils.resetPassword(); return false;"/>
+ </row>
+ <row align="center">
+ <label control="existingServer"
+ accesskey="&server.accesskey;"
+ value="&server.label;"/>
+ <menulist id="existingServer"
+ oncommand="gSyncSetup.onExistingServerCommand();"
+ oninput="gSyncSetup.onExistingServerInput();">
+ <menupopup>
+ <menuitem label="&serverType.main.label;"/>
+ <menuitem label="&serverType.custom2.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="existingServerFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <vbox>
+ <label class="status" value=" "/>
+ </vbox>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+
+ <groupbox>
+ <label id="existingPassphraseLabel"
+ value="&signIn.recoveryKey.label;"
+ accesskey="&signIn.recoveryKey.accesskey;"
+ control="existingPassphrase"/>
+ <textbox id="existingPassphrase"
+ oninput="gSyncSetup.checkFields();"/>
+ <hbox id="login-throbber" hidden="true">
+ <image/>
+ <label value="&verifying.label;"/>
+ </hbox>
+ <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true">
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </vbox>
+ </groupbox>
+ <vbox id="passphraseHelpBox">
+ <description>
+ &existingRecoveryKey.description;
+ <label class="text-link"
+ href="https://services.mozilla.com/sync/help/manual-setup">
+ &addDevice.showMeHow.label;
+ </label>
+ <spacer id="passphraseHelpSpacer"/>
+ <label class="text-link"
+ onclick="gSyncSetup.resetPassphrase(); return false;">
+ &resetSyncKey.label;
+ </label>
+ </description>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="syncOptionsPage"
+ label="&setup.optionsPage.title;"
+ onpageshow="gSyncSetup.onPageShow();">
+ <groupbox id="syncOptions">
+ <grid>
+ <columns>
+ <column/>
+ <column class="inputColumn" flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&syncComputerName.label;"
+ accesskey="&syncComputerName.accesskey;"
+ control="syncComputerName"/>
+ <textbox id="syncComputerName"
+ flex="1"
+ onchange="gSyncUtils.changeName(this);"/>
+ </row>
+ <row>
+ <label value="&syncMy.label;"/>
+ <vbox>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ id="engine.addons"
+ checked="true"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ id="engine.bookmarks"
+ checked="true"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ id="engine.history"
+ checked="true"/>
+ <checkbox label="&engine.passwords.label;"
+ accesskey="&engine.passwords.accesskey;"
+ id="engine.passwords"
+ checked="true"/>
+ <checkbox label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+ id="engine.prefs"
+ checked="true"/>
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ id="engine.tabs"
+ checked="true"/>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox id="mergeOptions">
+ <radiogroup id="mergeChoiceRadio" pack="start">
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows flex="1">
+ <row align="center">
+ <radio value="resetClient"
+ label="&choice2.merge.main.label;"
+ class="mergeChoiceButton"/>
+ <label value="&choice2.merge.recommended.label;" class="recommended"/>
+ </row>
+ <row align="center">
+ <radio value="wipeClient"
+ label="&choice2.client.main.label;"
+ class="mergeChoiceButton"/>
+ </row>
+ <row align="center">
+ <radio value="wipeRemote"
+ label="&choice2.server.main.label;"
+ class="mergeChoiceButton"/>
+ </row>
+ </rows>
+ </grid>
+ </radiogroup>
+ </groupbox>
+ </wizardpage>
+
+ <wizardpage id="syncOptionsConfirm"
+ label="&setup.optionsConfirmPage.title;"
+ onpageshow="gSyncSetup.onPageShow();">
+ <deck id="chosenActionDeck">
+ <vbox id="chosenActionMerge" class="confirm">
+ <description class="normal">
+ &confirm.merge.label;
+ </description>
+ </vbox>
+ <vbox id="chosenActionWipeClient" class="confirm">
+ <description class="normal">
+ &confirm.client2.label;
+ </description>
+ <separator class="thin"/>
+ <vbox id="dataList">
+ <label class="data indent" id="bookmarkCount"/>
+ <label class="data indent" id="historyCount"/>
+ <label class="data indent" id="passwordCount"/>
+ <label class="data indent" id="addonCount"/>
+ <label class="data indent" id="prefsWipe"
+ value="&engine.prefs.label;"/>
+ </vbox>
+ <separator class="thin"/>
+ <description class="normal">
+ &confirm.client.moreinfo.label;
+ </description>
+ </vbox>
+ <vbox id="chosenActionWipeServer" class="confirm">
+ <description class="normal">
+ &confirm.server2.label;
+ </description>
+ <separator class="thin"/>
+ <vbox id="clientList">
+ </vbox>
+ </vbox>
+ </deck>
+ </wizardpage>
+
+ <wizardpage id="successfulSetup"
+ label="&setup.successPage.title;"
+ onextra1="gSyncSetup.onSyncOptions();"
+ onpageshow="gSyncSetup.onPageShow();">
+ <vbox align="center">
+ <image id="successPageIcon"/>
+ </vbox>
+ <separator/>
+ <description class="normal">
+ <html:span id="firstSyncAction"/>
+ <html:strong id="firstSyncActionWarning"/>
+ <html:span id="firstSyncActionChange"/>
+ </description>
+ <description>
+ &continueUsing.label;
+ </description>
+ </wizardpage>
+</wizard>
diff --git a/comm/suite/components/sync/content/syncUI.js b/comm/suite/components/sync/content/syncUI.js
new file mode 100644
index 0000000000..668447c46d
--- /dev/null
+++ b/comm/suite/components/sync/content/syncUI.js
@@ -0,0 +1,454 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gSyncUI = {
+ _obs: ["weave:notification:added",
+ "weave:service:sync:start",
+ "weave:service:sync:delayed",
+ "weave:service:quota:remaining",
+ "weave:service:setup-complete",
+ "weave:service:login:start",
+ "weave:service:login:finish",
+ "weave:service:logout:finish",
+ "weave:service:start-over",
+ "weave:ui:login:error",
+ "weave:ui:sync:error",
+ "weave:ui:sync:finish",
+ "weave:ui:clear-error"],
+
+ _unloaded: false,
+
+ init: function SUI_init() {
+ // Update the Tools menu according to whether Sync is set up or not.
+ let taskPopup = document.getElementById("taskPopup");
+ if (taskPopup)
+ taskPopup.addEventListener("popupshowing", this.updateUI.bind(this));
+
+ // Proceed to set up the UI if Sync has already started up.
+ // Otherwise we'll do it when Sync is firing up.
+ if (Cc["@mozilla.org/weave/service;1"]
+ .getService().wrappedJSObject.ready) {
+ this.initUI();
+ return;
+ }
+
+ Services.obs.addObserver(this, "weave:service:ready", true);
+
+ // Remove the observer if the window is closed before the observer
+ // was triggered.
+ window.addEventListener("unload", function SUI_unload() {
+ gSyncUI._unloaded = true;
+ window.removeEventListener("unload", SUI_unload);
+ Services.obs.removeObserver(gSyncUI, "weave:service:ready");
+
+ if (Weave.Status.ready) {
+ gSyncUI._obs.forEach(function(topic) {
+ Services.obs.removeObserver(gSyncUI, topic);
+ });
+ }
+ });
+ },
+
+ initUI: function SUI_initUI() {
+ this._obs.forEach(function(topic) {
+ Services.obs.addObserver(this, topic, true);
+ }, this);
+
+ // Find the alltabs-popup
+ let popup = document.getElementById("alltabs-popup");
+ if (popup) {
+ popup.addEventListener(
+ "popupshowing", this.alltabsPopupShowing.bind(this), true);
+
+ if (Weave.Notifications.notifications.length)
+ this.initNotifications();
+ }
+ this.updateUI();
+ },
+
+ initNotifications: function SUI_initNotifications() {
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let notificationbox = document.createElementNS(XULNS, "notificationbox");
+ notificationbox.id = "sync-notifications";
+
+ let statusbar = document.getElementById("status-bar");
+ statusbar.parentNode.insertBefore(notificationbox, statusbar);
+
+ // Force a style flush to ensure that our binding is attached.
+ notificationbox.clientTop;
+
+ // notificationbox will listen to observers from now on.
+ Services.obs.removeObserver(this, "weave:notification:added");
+ },
+
+ _wasDelayed: false,
+
+ _needsSetup: function SUI__needsSetup() {
+ let firstSync = "";
+ try {
+ firstSync = Services.prefs.getCharPref("services.sync.firstSync");
+ } catch (e) { }
+ return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
+ firstSync == "notReady";
+ },
+
+ updateUI: function SUI_updateUI() {
+ let needsSetup = this._needsSetup();
+ document.getElementById("sync-setup-state").hidden = !needsSetup;
+ document.getElementById("sync-syncnow-state").hidden = needsSetup;
+
+ let syncButton = document.getElementById("sync-button");
+ if (syncButton) {
+ syncButton.removeAttribute("status");
+ this._updateLastSyncTime();
+ if (needsSetup)
+ syncButton.removeAttribute("tooltiptext");
+ }
+ },
+
+ alltabsPopupShowing: function(event) {
+ // Should we show the menu item?
+ //XXXphilikon We should remove the check for isLoggedIn here and have
+ // about:sync-tabs auto-login (bug 583344)
+ if (!Weave.Service.isLoggedIn || !Weave.Service.engineManager.get("tabs").enabled)
+ return;
+
+ let label = this._stringBundle.GetStringFromName("tabs.fromOtherComputers.label");
+
+ let popup = document.getElementById("alltabs-popup");
+ if (!popup)
+ return;
+
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("id", "sync-tabs-menuitem");
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("class", "alltabs-item");
+ menuitem.setAttribute("oncommand", "BrowserOpenSyncTabs();");
+
+ let sep = document.createElement("menuseparator");
+ sep.setAttribute("id", "sync-tabs-sep");
+
+ // Fake the tab object on the menu entries, so that we don't have to worry
+ // about removing them ourselves. They will just get cleaned up by popup
+ // binding. This also makes sure the statusbar updates with the URL.
+ menuitem.tab = { "linkedBrowser": { "currentURI": { "spec": label } } };
+ sep.tab = { "linkedBrowser": { "currentURI": { "spec": " " } } };
+
+ popup.insertBefore(sep, popup.firstChild);
+ popup.insertBefore(menuitem, sep);
+ },
+
+ // Functions called by observers
+ onActivityStart: function SUI_onActivityStart() {
+ let syncButton = document.getElementById("sync-button");
+ if (syncButton)
+ syncButton.setAttribute("status", "active");
+ },
+
+ onSyncDelay: function SUI_onSyncDelay() {
+ // basically, we want to just inform users that stuff is going to take a while
+ let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
+ let description = this._stringBundle.GetStringFromName("error.sync.no_node_found");
+ let buttons = [new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
+ this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
+ function() { gSyncUI.openServerStatus(); return true; }
+ )];
+ let notification = new Weave.Notification(
+ title, description, null, Weave.Notifications.PRIORITY_INFO, buttons);
+ Weave.Notifications.replaceTitle(notification);
+ this._wasDelayed = true;
+ },
+
+ onLoginFinish: function SUI_onLoginFinish() {
+ // Clear out any login failure notifications
+ let title = this._stringBundle.GetStringFromName("error.login.title");
+ this.clearError(title);
+ },
+
+ onLoginError: function SUI_onLoginError() {
+ // if login fails, any other notifications are essentially moot
+ Weave.Notifications.removeAll();
+
+ // if we haven't set up the client, don't show errors
+ if (this._needsSetup()) {
+ this.updateUI();
+ return;
+ }
+
+ let title = this._stringBundle.GetStringFromName("error.login.title");
+
+ let description;
+ if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
+ // Convert to days
+ let lastSync =
+ Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
+ description =
+ this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
+ } else {
+ let reason = Weave.Utils.getErrorString(Weave.Status.login);
+ description =
+ this._stringBundle.formatStringFromName("error.sync.description", [reason], 1);
+ }
+
+ let buttons = [];
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.login.prefs.label"),
+ this._stringBundle.GetStringFromName("error.login.prefs.accesskey"),
+ function() { gSyncUI.openPrefs(); return true; }
+ ));
+
+ let notification = new Weave.Notification(title, description, null,
+ Weave.Notifications.PRIORITY_WARNING, buttons);
+ Weave.Notifications.replaceTitle(notification);
+ this.updateUI();
+ },
+
+ onLogout: function SUI_onLogout() {
+ this.updateUI();
+ },
+
+ onStartOver: function SUI_onStartOver() {
+ this.clearError();
+ },
+
+ onQuotaNotice: function onQuotaNotice(subject, data) {
+ let title = this._stringBundle.GetStringFromName("warning.sync.quota.label");
+ let description = this._stringBundle.GetStringFromName("warning.sync.quota.description");
+ let buttons = [];
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"),
+ this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"),
+ function() { gSyncUI.openQuotaDialog(); return true; }
+ ));
+
+ let notification = new Weave.Notification(
+ title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons);
+ Weave.Notifications.replaceTitle(notification);
+ },
+
+ openServerStatus: function () {
+ let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
+ openUILinkIn(statusURL, "tab");
+ },
+
+ // Commands
+ doSync: function SUI_doSync() {
+ setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
+ },
+
+ handleToolbarButton: function SUI_handleToolbarButton() {
+ if (this._needsSetup())
+ this.openSetup();
+ else
+ this.doSync();
+ },
+
+ //XXXzpao should be part of syncCommon.js - which we might want to make a module...
+ // To be fixed in a followup (bug 583366)
+ openSetup: function SUI_openSetup() {
+ let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
+ if (win)
+ win.focus();
+ else {
+ window.openDialog("chrome://communicator/content/sync/syncSetup.xul",
+ "weaveSetup", "centerscreen,chrome,resizable=no");
+ }
+ },
+
+ openQuotaDialog: function SUI_openQuotaDialog() {
+ let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
+ if (win)
+ win.focus();
+ else
+ Services.ww.activeWindow.openDialog(
+ "chrome://communicator/content/sync/syncQuota.xul", "",
+ "centerscreen,chrome,dialog,modal");
+ },
+
+ openPrefs: function SUI_openPrefs() {
+ goPreferences("sync_pane");
+ },
+
+
+ // Helpers
+ _updateLastSyncTime: function SUI__updateLastSyncTime() {
+ let syncButton = document.getElementById("sync-button");
+ if (!syncButton)
+ return;
+
+ let lastSync;
+ try {
+ lastSync = Services.prefs.getCharPref("services.sync.lastSync");
+ }
+ catch (e) { };
+ if (!lastSync || this._needsSetup()) {
+ syncButton.removeAttribute("tooltiptext");
+ return;
+ }
+
+ // Show the day-of-week and time (HH:MM) of last sync
+ let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M");
+ let lastSyncLabel =
+ this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1);
+ syncButton.setAttribute("tooltiptext", lastSyncLabel);
+ },
+
+ clearError: function SUI_clearError(errorString) {
+ Weave.Notifications.removeAll(errorString);
+ this.updateUI();
+ },
+
+ onSyncFinish: function SUI_onSyncFinish() {
+ let title = this._stringBundle.GetStringFromName("error.sync.title");
+
+ // Clear out sync failures on a successful sync
+ this.clearError(title);
+
+ if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) {
+ title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
+ this.clearError(title);
+ this._wasDelayed = false;
+ }
+ },
+
+ onSyncError: function SUI_onSyncError() {
+ let title = this._stringBundle.GetStringFromName("error.sync.title");
+
+ if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
+ this.onLoginError();
+ return;
+ }
+
+ let description;
+ if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
+ // Convert to days
+ let lastSync =
+ Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
+ description =
+ this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
+ } else {
+ let error = Weave.Utils.getErrorString(Weave.Status.sync);
+ description =
+ this._stringBundle.formatStringFromName("error.sync.description", [error], 1);
+ }
+ let priority = Weave.Notifications.PRIORITY_WARNING;
+ let buttons = [];
+
+ // Check if the client is outdated in some way
+ let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE;
+ for (let [engine, reason] of Object.entries(Weave.Status.engines))
+ outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE;
+
+ if (outdated) {
+ description = this._stringBundle.GetStringFromName(
+ "error.sync.needUpdate.description");
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.sync.needUpdate.label"),
+ this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"),
+ function() { window.openUILinkIn("https://services.mozilla.com/update/", "tab"); return true; }
+ ));
+ }
+ else if (Weave.Status.sync == Weave.OVER_QUOTA) {
+ description = this._stringBundle.GetStringFromName(
+ "error.sync.quota.description");
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName(
+ "error.sync.viewQuotaButton.label"),
+ this._stringBundle.GetStringFromName(
+ "error.sync.viewQuotaButton.accesskey"),
+ function() { gSyncUI.openQuotaDialog(); return true; } )
+ );
+ }
+ else if (Weave.Status.enforceBackoff) {
+ priority = Weave.Notifications.PRIORITY_INFO;
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
+ this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
+ function() { gSyncUI.openServerStatus(); return true; }
+ ));
+ }
+ else {
+ priority = Weave.Notifications.PRIORITY_INFO;
+ buttons.push(new Weave.NotificationButton(
+ this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
+ this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
+ function() { gSyncUI.doSync(); return true; }
+ ));
+ }
+
+ let notification =
+ new Weave.Notification(title, description, null, priority, buttons);
+ Weave.Notifications.replaceTitle(notification);
+
+ if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) {
+ title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
+ Weave.Notifications.removeAll(title);
+ this._wasDelayed = false;
+ }
+
+ this.updateUI();
+ },
+
+ observe: function SUI_observe(subject, topic, data) {
+ if (this._unloaded)
+ throw "SyncUI observer called after unload: " + topic;
+
+ switch (topic) {
+ case "weave:service:sync:start":
+ this.onActivityStart();
+ break;
+ case "weave:ui:sync:finish":
+ this.onSyncFinish();
+ break;
+ case "weave:ui:sync:error":
+ this.onSyncError();
+ break;
+ case "weave:service:sync:delayed":
+ this.onSyncDelay();
+ break;
+ case "weave:service:quota:remaining":
+ this.onQuotaNotice();
+ break;
+ case "weave:service:setup-complete":
+ this.onLoginFinish();
+ break;
+ case "weave:service:login:start":
+ this.onActivityStart();
+ break;
+ case "weave:service:login:finish":
+ this.onLoginFinish();
+ break;
+ case "weave:ui:login:error":
+ this.onLoginError();
+ break;
+ case "weave:service:logout:finish":
+ this.onLogout();
+ break;
+ case "weave:service:start-over":
+ this.onStartOver();
+ break;
+ case "weave:service:ready":
+ this.initUI();
+ break;
+ case "weave:notification:added":
+ this.initNotifications();
+ break;
+ case "weave:ui:clear-error":
+ this.clearError();
+ break;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
+ //XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return Services.strings.createBundle("chrome://weave/locale/services/sync.properties");
+});
diff --git a/comm/suite/components/sync/content/syncUtils.js b/comm/suite/components/sync/content/syncUtils.js
new file mode 100644
index 0000000000..fe63654073
--- /dev/null
+++ b/comm/suite/components/sync/content/syncUtils.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Weave should always exist before before this file gets included.
+var gSyncUtils = {
+ _openLink: function (url) {
+ if (document.documentElement.id == "change-dialog")
+ Services.wm.getMostRecentWindow("navigator:browser")
+ .openUILinkIn(url, "tab");
+ else
+ openUILinkIn(url, "tab");
+ },
+
+ changeName: function changeName(input) {
+ // Make sure to update to a modified name, e.g., empty-string -> default
+ Weave.Service.clientsEngine.localName = input.value;
+ input.value = Weave.Service.clientsEngine.localName;
+ },
+
+ openChange: function openChange(type, duringSetup) {
+ // Just re-show the dialog if it's already open
+ let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type);
+ if (openedDialog != null) {
+ openedDialog.focus();
+ return;
+ }
+
+ // Open up the change dialog
+ let changeXUL = "chrome://communicator/content/sync/syncGenericChange.xul";
+ let changeOpt = "centerscreen,chrome,resizable=no";
+ Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt,
+ type, duringSetup);
+ },
+
+ changePassword: function () {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("ChangePassword");
+ },
+
+ resetPassphrase: function (duringSetup) {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("ResetPassphrase", duringSetup);
+ },
+
+ updatePassphrase: function () {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("UpdatePassphrase");
+ },
+
+ resetPassword: function () {
+ this._openLink(Weave.Service.pwResetURL);
+ },
+
+ openToS: function () {
+ this._openLink(Weave.Svc.Prefs.get("termsURL"));
+ },
+
+ openPrivacyPolicy: function () {
+ this._openLink(Weave.Svc.Prefs.get("privacyURL"));
+ },
+
+ // xxxmpc - fix domain before 1.3 final (bug 583652)
+ // xxxInvisibleSmiley - we should really have our own pages
+ // since these refer to Firefox in the page contents
+ _baseURL: "http://www.mozilla.com/firefox/sync/",
+
+ openFirstClientFirstrun: function () {
+ let url = this._baseURL + "firstrun.html";
+ this._openLink(url);
+ },
+
+ openAddedClientFirstrun: function () {
+ let url = this._baseURL + "secondrun.html";
+ this._openLink(url);
+ },
+
+ /**
+ * Prepare an invisible iframe with the passphrase backup document.
+ * Used by both the print and saving methods.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ * @param callback : Function called once the iframe has loaded.
+ */
+ _preparePPiframe: function(elid, callback) {
+ let pp = document.getElementById(elid).value;
+
+ // Create an invisible iframe whose contents we can print.
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "chrome://communicator/content/sync/syncKey.xhtml");
+ iframe.setAttribute("type", "content");
+ iframe.collapsed = true;
+ document.documentElement.appendChild(iframe);
+ iframe.addEventListener("load", function loadListener() {
+ iframe.removeEventListener("load", loadListener, true);
+
+ // Remove the license block.
+ let node = iframe.contentDocument.firstChild;
+ if (node && node.nodeType == Node.COMMENT_NODE)
+ node.remove();
+
+ // Insert the Sync Key into the page.
+ let el = iframe.contentDocument.getElementById("synckey");
+ el.firstChild.nodeValue = pp;
+
+ // Insert the TOS and Privacy Policy URLs into the page.
+ let termsURL = Weave.Svc.Prefs.get("termsURL");
+ el = iframe.contentDocument.getElementById("tosLink");
+ el.setAttribute("href", termsURL);
+ el.firstChild.nodeValue = termsURL;
+
+ let privacyURL = Weave.Svc.Prefs.get("privacyURL");
+ el = iframe.contentDocument.getElementById("ppLink");
+ el.setAttribute("href", privacyURL);
+ el.firstChild.nodeValue = privacyURL;
+
+ callback(iframe);
+ }, true);
+ },
+
+ /**
+ * Print passphrase backup document.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ */
+ passphrasePrint: function(elid) {
+ this._preparePPiframe(elid, function(iframe) {
+ let webBrowserPrint = iframe.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserPrint);
+ let printSettings = PrintUtils.getPrintSettings();
+
+ // Display no header/footer decoration except for the date.
+ printSettings.headerStrLeft
+ = printSettings.headerStrCenter
+ = printSettings.headerStrRight
+ = printSettings.footerStrLeft
+ = printSettings.footerStrCenter = "";
+ printSettings.footerStrRight = "&D";
+
+ try {
+ webBrowserPrint.print(printSettings, null);
+ } catch (ex) {
+ // print()'s return codes are expressed as exceptions. Ignore.
+ }
+ });
+ },
+
+ /**
+ * Save passphrase backup document to disk as HTML file.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ */
+ passphraseSave: function(elid) {
+ let dialogTitle = this._stringBundle.GetStringFromName("save.recoverykey.title");
+ let defaultSaveName = this._stringBundle.GetStringFromName("save.recoverykey.defaultfilename");
+ this._preparePPiframe(elid, function(iframe) {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == Ci.nsIFilePicker.returnOK ||
+ aResult == Ci.nsIFilePicker.returnReplace) {
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(filepicker.file, -1, parseInt("0600", 8), 0);
+
+ let serializer = new XMLSerializer();
+ let output = serializer.serializeToString(iframe.contentDocument);
+ output = output.replace(/<!DOCTYPE (.|\n)*?]>/,
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' +
+ '"DTD/xhtml1-strict.dtd">');
+ output = Weave.Utils.encodeUTF8(output);
+ stream.write(output, output.length);
+ }
+ };
+
+ fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.defaultString = defaultSaveName;
+ fp.open(fpCallback);
+ return false;
+ });
+ },
+
+ /**
+ * validatePassword
+ *
+ * @param el1 : the first textbox element in the form
+ * @param el2 : the second textbox element, if omitted it's an update form
+ *
+ * returns [valid, errorString]
+ */
+ validatePassword: function (el1, el2) {
+ let valid = false;
+ let val1 = el1.value;
+ let val2 = el2 ? el2.value : "";
+ let error = "";
+
+ if (!el2)
+ valid = val1.length >= Weave.MIN_PASS_LENGTH;
+ else if (val1 && val1 == Weave.Service.identity.username)
+ error = "change.password.pwSameAsUsername";
+ else if (val1 && val1 == Weave.Service.identity.account)
+ error = "change.password.pwSameAsEmail";
+ else if (val1 && val1 == Weave.Service.identity.basicPassword)
+ error = "change.password.pwSameAsPassword";
+ else if (val1 && val1 == Weave.Service.identity.syncKey)
+ error = "change.password.pwSameAsRecoveryKey";
+ else if (val1 && val2) {
+ if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
+ valid = true;
+ else if (val1.length < Weave.MIN_PASS_LENGTH)
+ error = "change.password.tooShort";
+ else if (val1 != val2)
+ error = "change.password.mismatch";
+ }
+ let errorString = error ? Weave.Utils.getErrorString(error) : "";
+ return [valid, errorString];
+ }
+};
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyGetter(gSyncUtils, "_stringBundle", function() {
+ return Services.strings.createBundle("chrome://communicator/locale/sync/syncSetup.properties");
+});
diff --git a/comm/suite/components/sync/jar.mn b/comm/suite/components/sync/jar.mn
new file mode 100644
index 0000000000..42138bae9a
--- /dev/null
+++ b/comm/suite/components/sync/jar.mn
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+comm.jar:
+ content/communicator/aboutSyncTabs.xul (content/aboutSyncTabs.xul)
+ content/communicator/aboutSyncTabs.js (content/aboutSyncTabs.js)
+ content/communicator/aboutSyncTabs.css (content/aboutSyncTabs.css)
+ content/communicator/aboutSyncTabs-bindings.xml (content/aboutSyncTabs-bindings.xml)
+ content/communicator/sync/syncAddDevice.xul (content/syncAddDevice.xul)
+ content/communicator/sync/syncAddDevice.js (content/syncAddDevice.js)
+ content/communicator/sync/syncSetup.xul (content/syncSetup.xul)
+ content/communicator/sync/syncSetup.js (content/syncSetup.js)
+ content/communicator/sync/syncGenericChange.xul (content/syncGenericChange.xul)
+ content/communicator/sync/syncGenericChange.js (content/syncGenericChange.js)
+ content/communicator/sync/syncKey.xhtml (content/syncKey.xhtml)
+ content/communicator/sync/syncNotification.xml (content/syncNotification.xml)
+ content/communicator/sync/syncQuota.xul (content/syncQuota.xul)
+ content/communicator/sync/syncQuota.js (content/syncQuota.js)
+ content/communicator/sync/syncUtils.js (content/syncUtils.js)
+ content/communicator/sync/syncUI.js (content/syncUI.js)
diff --git a/comm/suite/components/sync/moz.build b/comm/suite/components/sync/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/suite/components/sync/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/components/tests/browser/browser.ini b/comm/suite/components/tests/browser/browser.ini
new file mode 100644
index 0000000000..96c17932c0
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_339445.js]
+support-files = browser_339445_sample.html
+[browser_345898.js]
+[browser_346337.js]
+support-files = browser_346337_sample.html
+[browser_350525.js]
+[browser_354894.js]
+[browser_367052.js]
+[browser_393716.js]
+[browser_394759_basic.js]
+[browser_394759_behavior.js]
+[browser_408470.js]
+support-files = browser_408470_sample.html
+[browser_423132.js]
+support-files = browser_423132_sample.html
+[browser_447951.js]
+support-files = browser_447951_sample.html
+[browser_448741.js]
+[browser_454908.js]
+support-files = browser_454908_sample.html
+[browser_456342.js]
+support-files = browser_456342_sample.xhtml
+[browser_461634.js]
+[browser_463206.js]
+support-files = browser_463206_sample.html
+[browser_465215.js]
+[browser_465223.js]
+[browser_466937.js]
+support-files = browser_466937_sample.html
+[browser_477657.js]
+[browser_480893.js]
+[browser_483330.js]
+[browser_485482.js]
+support-files = browser_485482_sample.html
+[browser_490040.js]
+[browser_491168.js]
+[browser_491577.js]
+[browser_493467.js]
+[browser_500328.js]
+[browser_514751.js]
+[browser_522545.js]
+[browser_524745.js]
+[browser_526613.js]
+[browser_528776.js]
+[browser_581937.js]
+[browser_586068-cascaded_restore.js]
+[browser_597315.js]
+support-files =
+ browser_597315_index.html
+ browser_597315_a.html
+ browser_597315_b.html
+ browser_597315_c.html
+ browser_597315_c1.html
+ browser_597315_c2.html
+[browser_607016.js]
+[browser_615394-SSWindowState_events.js]
+[browser_625257.js]
+[browser_636279.js]
+[browser_637020.js]
+support-files = browser_637020_slow.sjs
+[browser_645428.js]
+[browser_665702-state_session.js]
+[browser_687710.js]
+[browser_687710_2.js]
+[browser_694378.js]
+[browser_bug431826.js]
+[browser_isempty.js]
+[browser_markPageAsFollowedLink.js]
+support-files = framedPage.html frameLeft.html frameRight.html
diff --git a/comm/suite/components/tests/browser/browser_339445.js b/comm/suite/components/tests/browser/browser_339445.js
new file mode 100644
index 0000000000..fb41aebb24
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_339445.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 339445 **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "suite/common/tests/browser/browser_339445_sample.html";
+
+ let tab = getBrowser().addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+ let doc = tab.linkedBrowser.contentDocument;
+ is(doc.getElementById("storageTestItem").textContent, "PENDING",
+ "sessionStorage value has been set");
+
+ let tab2 = ss.duplicateTab(window,tab);
+ tab2.linkedBrowser.addEventListener("load", function testTab2LBLoad(aEvent) {
+ this.removeEventListener("load", testTab2LBLoad, true);
+ let doc2 = tab2.linkedBrowser.contentDocument;
+ is(doc2.getElementById("storageTestItem").textContent, "SUCCESS",
+ "sessionStorage value has been duplicated");
+
+ // clean up
+ getBrowser().removeTab(tab2);
+ getBrowser().removeTab(tab);
+
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_339445_sample.html b/comm/suite/components/tests/browser/browser_339445_sample.html
new file mode 100644
index 0000000000..1fd7d5f032
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_339445_sample.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 339445</title>
+
+storageTestItem = <span id="storageTestItem">FAIL</span>
+
+<!--
+ storageTestItem's textContent will be one of the following:
+ * FAIL : sessionStorage wasn't available
+ * PENDING : the test value has been initialized on first load
+ * SUCCESS : the test value was correctly retrieved
+-->
+
+<script>
+ document.getElementById("storageTestItem").textContent =
+ sessionStorage["storageTestItem"] || "PENDING";
+ sessionStorage["storageTestItem"] = "SUCCESS";
+</script>
diff --git a/comm/suite/components/tests/browser/browser_345898.js b/comm/suite/components/tests/browser/browser_345898.js
new file mode 100644
index 0000000000..7f8c26f1a6
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_345898.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 345898 **/
+
+ function test(aLambda) {
+ try {
+ aLambda();
+ return false;
+ }
+ catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(test(() => ss.getWindowState({})),
+ "Invalid window for getWindowState throws");
+ ok(test(() => ss.setWindowState({}, "", false)),
+ "Invalid window for setWindowState throws");
+ ok(test(() => ss.getTabState({})),
+ "Invalid tab for getTabState throws");
+ ok(test(() => ss.setTabState({}, "{}")),
+ "Invalid tab state for setTabState throws");
+ ok(test(() => ss.setTabState({}, '{ "entries": [] }')),
+ "Invalid tab for setTabState throws");
+ ok(test(() => ss.duplicateTab({}, {})),
+ "Invalid tab for duplicateTab throws");
+ ok(test(() => ss.duplicateTab({}, getBrowser().selectedTab)),
+ "Invalid window for duplicateTab throws");
+ ok(test(() => ss.getClosedTabData({})),
+ "Invalid window for getClosedTabData throws");
+ ok(test(() => ss.undoCloseTab({}, 0)),
+ "Invalid window for undoCloseTab throws");
+ ok(test(() => ss.undoCloseTab(window, -1)),
+ "Invalid index for undoCloseTab throws");
+ ok(test(() => ss.getWindowValue({}, "")),
+ "Invalid window for getWindowValue throws");
+ ok(test(() => ss.getWindowValue({}, "")),
+ "Invalid window for getWindowValue throws");
+ ok(test(() => ss.getWindowValue({}, "", "")),
+ "Invalid window for setWindowValue throws");
+}
diff --git a/comm/suite/components/tests/browser/browser_346337.js b/comm/suite/components/tests/browser/browser_346337.js
new file mode 100644
index 0000000000..f97e36cc24
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_346337.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 346337 **/
+
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("346337_test1.file");
+ let filePath1 = file.path;
+ file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("346337_test2.file");
+ let filePath2 = file.path;
+
+ let fieldList = {
+ "//input[@name='input']": Date.now().toString(),
+ "//input[@name='spaced 1']": Math.random().toString(),
+ "//input[3]": "three",
+ "//input[@type='checkbox']": true,
+ "//input[@name='uncheck']": false,
+ "//input[@type='radio'][1]": false,
+ "//input[@type='radio'][2]": true,
+ "//input[@type='radio'][3]": false,
+ "//select": 2,
+ "//select[@multiple]": [1, 3],
+ "//textarea[1]": "",
+ "//textarea[2]": "Some text... " + Math.random(),
+ "//textarea[3]": "Some more text\n" + new Date(),
+ "//input[@type='file'][1]": [filePath1],
+ "//input[@type='file'][2]": [filePath1, filePath2]
+ };
+
+ function getElementByXPath(aTab, aQuery) {
+ let doc = aTab.linkedBrowser.contentDocument;
+ let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+ return doc.evaluate(aQuery, doc, null, xptype, null).singleNodeValue;
+ }
+
+ function setFormValue(aTab, aQuery, aValue) {
+ let node = getElementByXPath(aTab, aQuery);
+ if (typeof aValue == "string")
+ node.value = aValue;
+ else if (typeof aValue == "boolean")
+ node.checked = aValue;
+ else if (typeof aValue == "number")
+ node.selectedIndex = aValue;
+ else if (ChromeUtils.getClassName(node) === "HTMLInputElement" && node.type == "file")
+ node.mozSetFileNameArray(aValue, aValue.length);
+ else
+ Array.from(node.options).forEach((aOpt, aIx) =>
+ aOpt.selected = aValue.includes(aIx));
+ }
+
+ function compareFormValue(aTab, aQuery, aValue) {
+ let node = getElementByXPath(aTab, aQuery);
+ if (!node)
+ return false;
+ if (ChromeUtils.getClassName(node) === "HTMLInputElement") {
+ if (node.type == "file") {
+ let fileNames = node.mozGetFileNameArray();
+ return fileNames.length == aValue.length &&
+ Array.from(fileNames).every(aFile => aValue.includes(aFile));
+ }
+ return aValue == (node.type == "checkbox" || node.type == "radio" ?
+ node.checked : node.value);
+ }
+ if (ChromeUtils.getClassName(node) === "HTMLTextAreaElement")
+ return aValue == node.value;
+ if (!node.multiple)
+ return aValue == node.selectedIndex;
+ return Array.from(node.options).every((aOpt, aIx) =>
+ aValue.includes(aIx) == aOpt.selected);
+ }
+
+ // test setup
+ let tabbrowser = getBrowser();
+ waitForExplicitFinish();
+
+ // make sure we don't save form data at all (except for tab duplication)
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ let rootDir = getRootDirectory(gTestPath);
+ let testURL = rootDir + "browser_346337_sample.html";
+ let tab = tabbrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function loadListener1(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", loadListener1, true);
+ for (let xpath in fieldList)
+ setFormValue(tab, xpath, fieldList[xpath]);
+
+ let tab2 = ss.duplicateTab(window,tab);
+ tab2.linkedBrowser.addEventListener("pageshow", function pageshowListener2(aEvent) {
+ tab2.linkedBrowser.removeEventListener("pageshow", pageshowListener2, true);
+ for (let xpath in fieldList)
+ ok(compareFormValue(tab2, xpath, fieldList[xpath]),
+ "The value for \"" + xpath + "\" was correctly restored");
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function pageshowListener3(aEvent) {
+ browser.removeEventListener("load", pageshowListener3, true);
+ let tab3 = tabbrowser.undoCloseTab(0);
+ tab3.linkedBrowser.addEventListener("pageshow", function pageshowListener4(aEvent) {
+ tab3.linkedBrowser.removeEventListener("pageshow", pageshowListener4, true);
+ for (let xpath in fieldList)
+ if (fieldList[xpath])
+ ok(!compareFormValue(tab3, xpath, fieldList[xpath]),
+ "The value for \"" + xpath + "\" was correctly discarded");
+
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.privacy_level"))
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ // undoCloseTab can reuse a single blank tab, so we have to
+ // make sure not to close the window when closing our last tab
+ if (tabbrowser.tabContainer.childNodes.length == 1)
+ tabbrowser.addTab();
+ tabbrowser.removeTab(tab3);
+ finish();
+ }, true);
+ }, true);
+ // clean up
+ tabbrowser.removeTab(tab2);
+ tabbrowser.removeTab(tab);
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_346337_sample.html b/comm/suite/components/tests/browser/browser_346337_sample.html
new file mode 100644
index 0000000000..b0c305775e
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_346337_sample.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 346337</title>
+
+<h3>Text Fields</h3>
+<input type="text" name="input">
+<input type="text" name="spaced 1">
+<input>
+
+<h3>Checkboxes and Radio buttons</h3>
+<input type="checkbox" name="check"> Check 1
+<input type="checkbox" name="uncheck" checked> Check 2
+<p>
+<input type="radio" name="group" value="1"> Radio 1
+<input type="radio" name="group" value="some"> Radio 2
+<input type="radio" name="group" checked> Radio 3
+
+<h3>Selects</h3>
+<select name="any">
+ <option value="1"> Select 1
+ <option value="some"> Select 2
+ <option>Select 3
+</select>
+<select multiple="multiple">
+ <option value=1> Multi-select 1
+ <option value=2> Multi-select 2
+ <option value=3> Multi-select 3
+ <option value=4> Multi-select 4
+</select>
+
+<h3>Text Areas</h3>
+<textarea name="testarea"></textarea>
+<textarea name="sized one" rows="5" cols="25"></textarea>
+<textarea></textarea>
+
+<h3>File Selector</h3>
+<input type="file">
+<input type="file" multiple>
diff --git a/comm/suite/components/tests/browser/browser_350525.js b/comm/suite/components/tests/browser/browser_350525.js
new file mode 100644
index 0000000000..c5fb0e11e7
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_350525.js
@@ -0,0 +1,100 @@
+function test() {
+ /** Test for Bug 350525 **/
+
+ function test(aLambda) {
+ try {
+ return aLambda() || true;
+ }
+ catch (ex) { }
+ return false;
+ }
+
+ waitForExplicitFinish();
+
+ ////////////////////////////
+ // setWindowValue, et al. //
+ ////////////////////////////
+ let key = "Unique name: " + Date.now();
+ let value = "Unique value: " + Math.random();
+
+ // test adding
+ ok(test(() => ss.setWindowValue(window, key, value)), "set a window value");
+
+ // test retrieving
+ is(ss.getWindowValue(window, key), value, "stored window value matches original");
+
+ // test deleting
+ ok(test(() => ss.deleteWindowValue(window, key)), "delete the window value");
+
+ // value should not exist post-delete
+ is(ss.getWindowValue(window, key), "", "window value was deleted");
+
+ // test deleting a non-existent value
+ ok(test(() => ss.deleteWindowValue(window, key)), "delete non-existent window value");
+
+ /////////////////////////
+ // setTabValue, et al. //
+ /////////////////////////
+ key = "Unique name: " + Math.random();
+ value = "Unique value: " + Date.now();
+ let tab = getBrowser().addTab();
+ tab.linkedBrowser.stop();
+
+ // test adding
+ ok(test(() => ss.setTabValue(tab, key, value)), "store a tab value");
+
+ // test retrieving
+ is(ss.getTabValue(tab, key), value, "stored tab value match original");
+
+ // test deleting
+ ok(test(() => ss.deleteTabValue(tab, key)), "delete the tab value");
+ // value should not exist post-delete
+ is(ss.getTabValue(tab, key), "", "tab value was deleted");
+
+ // test deleting a non-existent value
+ ok(test(() => ss.deleteTabValue(tab, key)), "delete non-existent tab value");
+
+ // clean up
+ getBrowser().removeTab(tab);
+
+ /////////////////////////////////////
+ // getClosedTabCount, undoCloseTab //
+ /////////////////////////////////////
+
+ // get closed tab count
+ let count = ss.getClosedTabCount(window);
+ let max_tabs_undo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+ ok(0 <= count && count <= max_tabs_undo,
+ "getClosedTabCount returns zero or at most max_tabs_undo");
+
+ // create a new tab
+ let testURL = "about:";
+ tab = getBrowser().addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ this.removeEventListener("load", testTabLBLoad, true);
+ // make sure that the next closed tab will increase getClosedTabCount
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+
+ // remove tab
+ getBrowser().removeTab(tab);
+
+ // getClosedTabCount
+ var newcount = ss.getClosedTabCount(window);
+ ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
+
+ // undoCloseTab
+ tab = test(() => ss.undoCloseTab(window, 0));
+ ok(tab, "undoCloseTab doesn't throw")
+
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad2(aEvent) {
+ this.removeEventListener("load", testTabLBLoad2, true);
+ is(this.currentURI.spec, testURL, "correct tab was reopened");
+
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.max_tabs_undo"))
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ getBrowser().removeTab(tab);
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_354894.js b/comm/suite/components/tests/browser/browser_354894.js
new file mode 100644
index 0000000000..8fbc5330d0
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_354894.js
@@ -0,0 +1,459 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Checks that restoring the last browser window in session is actually
+ * working:
+ * 1.1) Open a new browser window
+ * 1.2) Add some tabs
+ * 1.3) Close that window
+ * 1.4) Opening another window
+ * --> State is restored
+ *
+ * 2.1) Open a new browser window
+ * 2.2) Add some tabs
+ * 2.4) Open some popups
+ * 2.5) Add another tab to one popup (so that it gets stored) and close it again
+ * 2.5) Close the browser window
+ * 2.6) Open another browser window
+ * --> State of the closed browser window, but not of the popup, is restored
+ *
+ * 3.1) Open a popup
+ * 3.2) Add another tab to the popup (so that it gets stored) and close it again
+ * 3.3) Open a window
+ * --> Nothing at all should be restored
+ *
+ * 4.1) Open two browser windows and close them again
+ * 4.2) undoCloseWindow() one
+ * 4.3) Open another browser window
+ * --> Nothing at all should be restored
+ *
+ * Checks the new notifications are correctly posted and processed, that is
+ * for each successful -requested a -granted is received, but omitted if
+ * -requested was cnceled
+ * Said notifications are:
+ * - browser-lastwindow-close-requested
+ * - browser-lastwindow-close-granted
+ * Tests are:
+ * 5) Cancel closing when first observe a -requested
+ * --> Window is kept open
+ * 6) Count the number of notifications
+ * --> count(-requested) == count(-granted) + 1
+ * --> (The first -requested was canceled, so off-by-one)
+ * 7) (Mac only) Mac version of Test 5 additionally preparing Test 6
+ *
+ * @see https://bugzilla.mozilla.org/show_bug.cgi?id=354894
+ * @note It is implicitly tested that restoring the last window works when
+ * non-browser windows are around. The "Run Tests" window as well as the main
+ * browser window (wherein the test code gets executed) won't be considered
+ * browser windows. To achiveve this said main browser window has it's windowtype
+ * attribute modified so that it's not considered a browser window any longer.
+ * This is crucial, because otherwise there would be two browser windows around,
+ * said main test window and the one opened by the tests, and hence the new
+ * logic wouldn't be executed at all.
+ * @note Mac only tests the new notifications, as restoring the last window is
+ * not enabled on that platform (platform shim; the application is kept running
+ * although there are no windows left)
+ * @note There is a difference when closing a browser window with
+ * BrowserTryToCloseWindow() as opposed to close(). The former will make
+ * nsSessionStore restore a window next time it gets a chance and will post
+ * notifications. The latter won't.
+ */
+
+function browserWindowsCount(expected, msg) {
+ if (typeof expected == "number")
+ expected = [expected, expected];
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ is(count, expected[0], msg + " (nsIWindowMediator)");
+ let state = Cc["@mozilla.org/suite/sessionstore;1"]
+ .getService(Ci.nsISessionStore)
+ .getBrowserState();
+ is(JSON.parse(state).windows.length, expected[1], msg + " (getBrowserState)");
+}
+
+function test() {
+ browserWindowsCount(1, "Only one browser window should be open initially");
+
+ if (AppConstants.platform == "macosx") {
+ todo(false, "Test disabled on MacOSX. (Bug 520787)");
+ return;
+ }
+
+ waitForExplicitFinish();
+ // This test takes some time to run, and it could timeout randomly.
+ // So we require a longer timeout. See bug 528219.
+ requestLongerTimeout(2);
+
+ // Some urls that might be opened in tabs and/or popups
+ // Do not use about:blank:
+ // That one is reserved for special purposes in the tests
+ const TEST_URLS = ["about:mozilla", "about:buildconfig"];
+
+ // Number of -request notifications to except
+ // remember to adjust when adding new tests
+ const NOTIFICATIONS_EXPECTED = 4;
+
+ // Window features of popup windows
+ const POPUP_FEATURES = "toolbar=no,resizable=no,status=no";
+
+ // Window features of browser windows
+ const CHROME_FEATURES = "chrome,all,dialog=no";
+
+ // Store the old window type for cleanup
+ var oldWinType = "";
+ // Store the old tabs.warnOnClose pref so that we may reset it during
+ // cleanup
+ var oldWarnTabsOnClose = Services.prefs.getBoolPref("browser.tabs.warnOnClose");
+
+ // Observe these, and also use to count the number of hits
+ var observing = {
+ "browser-lastwindow-close-requested": 0,
+ "browser-lastwindow-close-granted": 0
+ };
+
+ /**
+ * Helper: Will observe and handle the notifications for us
+ */
+ var observer = {
+ hitCount: 0,
+
+ observe: function(aCancel, aTopic, aData) {
+ // count so that we later may compare
+ observing[aTopic]++;
+
+ // handle some tests
+ if (++this.hitCount == 1) {
+ // Test 6
+ aCancel.QueryInterface(Ci.nsISupportsPRBool).data = true;
+ }
+ }
+ };
+
+ /**
+ * Helper: Sets prefs as the testsuite requires
+ * @note Will be reset in cleanTestSuite just before finishing the tests
+ */
+ function setPrefs() {
+ Services.prefs.setIntPref("browser.startup.page", 3);
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+ }
+
+ /**
+ * Helper: Sets up this testsuite
+ */
+ function setupTestsuite(testFn) {
+ // Register our observers
+ for (let o in observing)
+ Services.obs.addObserver(observer, o);
+
+ // Make the main test window not count as a browser window any longer
+ oldWinType = document.documentElement.getAttribute("windowtype");
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+ }
+
+ /**
+ * Helper: Cleans up behind the testsuite
+ */
+ function cleanupTestsuite(callback) {
+ // Finally remove observers again
+ for (let o in observing)
+ Services.obs.removeObserver(observer, o, false);
+
+ // Reset the prefs we touched
+ for (let pref of [
+ "browser.startup.page"
+ ]) {
+ if (Services.prefs.prefHasUserValue(pref))
+ Services.prefs.clearUserPref(pref);
+ }
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", oldWarnTabsOnClose);
+
+ // Reset the window type
+ document.documentElement.setAttribute("windowtype", oldWinType);
+ }
+
+ /**
+ * Helper: sets the prefs and a new window with our test tabs
+ */
+ function setupTestAndRun(testFn) {
+ // Prepare the prefs
+ setPrefs();
+
+ // Prepare a window; open it and add more tabs
+ let newWin = openDialog(location, "_blank", CHROME_FEATURES, "about:config");
+ newWin.addEventListener("load", function loadListener1(aEvent) {
+ newWin.removeEventListener("load", loadListener1);
+ newWin.getBrowser().addEventListener("pageshow", function pageshowListener2(aEvent) {
+ newWin.getBrowser().removeEventListener("pageshow", pageshowListener2, true);
+ for (let url of TEST_URLS) {
+ newWin.getBrowser().addTab(url);
+ }
+
+ executeSoon(() => testFn(newWin));
+ }, true);
+ });
+ }
+
+ /**
+ * Test 1: Normal in-session restore
+ * @note: Non-Mac only
+ */
+ function testOpenCloseNormal(nextFn) {
+ setupTestAndRun(function(newWin) {
+ // Close the window
+ // window.close doesn't push any close events,
+ // so use BrowserTryToCloseWindow
+ newWin.BrowserTryToCloseWindow();
+
+ // The first request to close is denied by our observer (Test 6)
+ ok(!newWin.closed, "First close request was denied");
+ if (!newWin.closed) {
+ newWin.BrowserTryToCloseWindow();
+ ok(newWin.closed, "Second close request was granted");
+ }
+
+ // Open a new window
+ // The previously closed window should be restored
+ newWin = openDialog(location, "_blank", CHROME_FEATURES, "about:blank");
+ newWin.addEventListener("load", function loadListener3() {
+ newWin.removeEventListener("load", loadListener3);
+ executeSoon(function() {
+ is(newWin.getBrowser().browsers.length, TEST_URLS.length + 1,
+ "Restored window in-session with otherpopup windows around");
+
+ // Cleanup
+ newWin.close();
+
+ // Next please
+ executeSoon(nextFn);
+ });
+ }, true);
+ });
+ }
+
+ /**
+ * Test 2: Open some popup windows to check those aren't restored, but
+ * the browser window is
+ * @note: Non-Mac only
+ */
+ function testOpenCloseWindowAndPopup(nextFn) {
+ setupTestAndRun(function(newWin) {
+ // open some popups
+ let popup = openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[0]);
+ let popup2 = openDialog(location, "popup2", POPUP_FEATURES, TEST_URLS[1]);
+ popup2.addEventListener("load", function loadListener4() {
+ popup2.removeEventListener("load", loadListener4);
+ popup2.getBrowser().addEventListener("pageshow", function pageshowListener5() {
+ popup2.getBrowser().removeEventListener("pageshow", pageshowListener5, true);
+ popup2.getBrowser().addTab(TEST_URLS[0]);
+ // close the window
+ newWin.BrowserTryToCloseWindow();
+
+ // Close the popup window
+ // The test is successful when not this popup window is restored
+ // but instead newWin
+ popup2.close();
+
+ // open a new window the previously closed window should be restored to
+ newWin = openDialog(location, "_blank", CHROME_FEATURES, "about:blank");
+ newWin.addEventListener("load", function loadListener6() {
+ newWin.removeEventListener("load", loadListener6);
+ executeSoon(function() {
+ is(newWin.getBrowser().browsers.length, TEST_URLS.length + 1,
+ "Restored window and associated tabs in session");
+
+ // Cleanup
+ newWin.close();
+ popup.close();
+
+ // Next please
+ executeSoon(nextFn);
+ });
+ }, true);
+ }, true);
+ });
+ });
+ }
+
+ /**
+ * Test 3: Open some popup window to check it isn't restored.
+ * Instead nothing at all should be restored
+ * @note: Non-Mac only
+ */
+ function testOpenCloseOnlyPopup(nextFn) {
+ // prepare the prefs
+ setPrefs();
+
+ // This will cause nsSessionStore to restore a window the next time it
+ // gets a chance.
+ let popup = openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
+ popup.addEventListener("load", function loadListener7() {
+ popup.removeEventListener("load", loadListener7, true);
+ is(popup.getBrowser().browsers.length, 1,
+ "Did not restore the popup window (1)");
+ popup.BrowserTryToCloseWindow();
+
+ // Real tests
+ popup = openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
+ popup.addEventListener("load", function loadListener8() {
+ popup.removeEventListener("load", loadListener8);
+ popup.getBrowser().addEventListener("pageshow", function pageshowListener9() {
+ popup.getBrowser().removeEventListener("pageshow", pageshowListener9, true);
+ popup.getBrowser().addTab(TEST_URLS[0]);
+
+ is(popup.getBrowser().browsers.length, 2,
+ "Did not restore to the popup window (2)");
+
+ // Close the popup window
+ // The test is successful when not this popup window is restored
+ // but instead a new window is opened without restoring anything
+ popup.close();
+
+ let newWin = openDialog(location, "_blank", CHROME_FEATURES, "about:blank");
+ newWin.addEventListener("load", function loadListener10() {
+ newWin.removeEventListener("load", loadListener10, true);
+ executeSoon(function() {
+ isnot(newWin.getBrowser().browsers.length, 2,
+ "Did not restore the popup window");
+ is(TEST_URLS.indexOf(newWin.getBrowser().browsers[0].currentURI.spec), -1,
+ "Did not restore the popup window (2)");
+
+ // Cleanup
+ newWin.close();
+
+ // Next please
+ executeSoon(nextFn);
+ });
+ }, true);
+ }, true);
+ });
+ }, true);
+ }
+
+ /**
+ * Test 4: Open some windows and do undoCloseWindow. This should prevent any
+ * restoring later in the test
+ * @note: Non-Mac only
+ */
+ function testOpenCloseRestoreFromPopup(nextFn) {
+ setupTestAndRun(function(newWin) {
+ setupTestAndRun(function(newWin2) {
+ newWin.BrowserTryToCloseWindow();
+ newWin2.BrowserTryToCloseWindow();
+
+ browserWindowsCount([0, 1], "browser windows while running testOpenCloseRestoreFromPopup");
+
+ newWin = undoCloseWindow(0);
+
+ newWin2 = openDialog(location, "_blank", CHROME_FEATURES, "about:blank");
+ newWin2.addEventListener("load", function loadListener11() {
+ newWin2.removeEventListener("load", loadListener11, true);
+ executeSoon(function() {
+ is(newWin2.getBrowser().browsers.length, 1,
+ "Did not restore, as undoCloseWindow() was last called");
+ is(TEST_URLS.indexOf(newWin2.getBrowser().browsers[0].currentURI.spec), -1,
+ "Did not restore, as undoCloseWindow() was last called (2)");
+
+ browserWindowsCount([2, 3], "browser windows while running testOpenCloseRestoreFromPopup");
+
+ // Cleanup
+ newWin.close();
+ newWin2.close();
+
+ browserWindowsCount([0, 1], "browser windows while running testOpenCloseRestoreFromPopup");
+
+ // Next please
+ executeSoon(nextFn);
+ });
+ }, true);
+ });
+ });
+ }
+
+ /**
+ * Test 5: Check whether the right number of notifications was received during
+ * the tests
+ */
+ function testNotificationCount(nextFn) {
+ is(observing["browser-lastwindow-close-requested"], NOTIFICATIONS_EXPECTED,
+ "browser-lastwindow-close-requested notifications observed");
+
+ // -request must be one more as we cancel the first one we hit,
+ // and hence won't produce a corresponding -grant
+ // @see observer.observe
+ is(observing["browser-lastwindow-close-requested"],
+ observing["browser-lastwindow-close-granted"] + 1,
+ "Notification count for -request and -grant matches");
+
+ executeSoon(nextFn);
+ }
+
+ /**
+ * Test 6: Test if closing can be denied on Mac
+ * Futhermore prepares the testNotificationCount test (Test 6)
+ * @note: Mac only
+ */
+ function testMacNotifications(nextFn, iteration) {
+ iteration = iteration || 1;
+ setupTestAndRun(function(newWin) {
+ // close the window
+ // window.close doesn't push any close events,
+ // so use BrowserTryToCloseWindow
+ newWin.BrowserTryToCloseWindow();
+ if (iteration == 1) {
+ ok(!newWin.closed, "First close attempt denied");
+ if (!newWin.closed) {
+ newWin.BrowserTryToCloseWindow();
+ ok(newWin.closed, "Second close attempt granted");
+ }
+ }
+
+ if (iteration < NOTIFICATIONS_EXPECTED - 1) {
+ executeSoon(() => testMacNotifications(nextFn, ++iteration));
+ }
+ else {
+ executeSoon(nextFn);
+ }
+ });
+ }
+
+ // Execution starts here
+
+ setupTestsuite();
+ if (AppConstants.platform == "macosx") {
+ // Mac tests
+ testMacNotifications(function () {
+ testNotificationCount(function () {
+ cleanupTestsuite();
+ browserWindowsCount(1, "Only one browser window should be open eventually");
+ finish();
+ });
+ });
+ }
+ else {
+ // Non-Mac Tests
+ testOpenCloseNormal(function () {
+ browserWindowsCount([0, 1], "browser windows after testOpenCloseNormal");
+ testOpenCloseWindowAndPopup(function () {
+ browserWindowsCount([0, 1], "browser windows after testOpenCloseWindowAndPopup");
+ testOpenCloseOnlyPopup(function () {
+ browserWindowsCount([0, 1], "browser windows after testOpenCloseOnlyPopup");
+ testOpenCloseRestoreFromPopup(function () {
+ browserWindowsCount([0, 1], "browser windows after testOpenCloseRestoreFromPopup");
+ testNotificationCount(function () {
+ cleanupTestsuite();
+ browserWindowsCount(1, "browser windows after testNotificationCount");
+ finish();
+ });
+ });
+ });
+ });
+ });
+ }
+}
diff --git a/comm/suite/components/tests/browser/browser_367052.js b/comm/suite/components/tests/browser/browser_367052.js
new file mode 100644
index 0000000000..54ffaf0253
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_367052.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 367052 **/
+
+ waitForExplicitFinish();
+
+ // make sure that the next closed tab will increase getClosedTabCount
+ let max_tabs_undo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+ let closedTabCount = ss.getClosedTabCount(window);
+
+ // restore a blank tab
+ let tab = getBrowser().addTab("about:");
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ this.removeEventListener("load", testTabLBLoad, true);
+
+ let history = tab.linkedBrowser.webNavigation.sessionHistory;
+ ok(history.count >= 1, "the new tab does have at least one history entry");
+
+ ss.setTabState(tab, '{ "entries": [] }');
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad2(aEvent) {
+ this.removeEventListener("load", testTabLBLoad2, true);
+ ok(history.count == 0, "the tab was restored without any history whatsoever");
+
+ getBrowser().removeTab(tab);
+ ok(ss.getClosedTabCount(window) == closedTabCount,
+ "The closed blank tab wasn't added to Recently Closed Tabs");
+
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.max_tabs_undo"))
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_393716.js b/comm/suite/components/tests/browser/browser_393716.js
new file mode 100644
index 0000000000..ce1d33e167
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_393716.js
@@ -0,0 +1,74 @@
+function test() {
+ /** Test for Bug 393716 **/
+
+ waitForExplicitFinish();
+
+ /////////////////
+ // getTabState //
+ /////////////////
+ let key = "Unique key: " + Date.now();
+ let value = "Unique value: " + Math.random();
+ let testURL = "about:config";
+
+ // create a new tab
+ let tab = getBrowser().addTab(testURL);
+ ss.setTabValue(tab, key, value);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ this.removeEventListener("load", testTabLBLoad, true);
+ // get the tab's state
+ let state = ss.getTabState(tab);
+ ok(state, "get the tab's state");
+
+ // verify the tab state's integrity
+ state = eval("(" + state + ")");
+ ok(state instanceof Object && state.entries instanceof Array && state.entries.length > 0,
+ "state object seems valid");
+ ok(state.entries.length == 1 && state.entries[0].url == testURL,
+ "Got the expected state object (test URL)");
+ ok(state.extData && state.extData[key] == value,
+ "Got the expected state object (test manually set tab value)");
+
+ // clean up
+ getBrowser().removeTab(tab);
+ }, true);
+
+ //////////////////////////////////
+ // setTabState and duplicateTab //
+ //////////////////////////////////
+ let key2 = "key2";
+ let value2 = "Value " + Math.random();
+ let value3 = "Another value: " + Date.now();
+ let state = { entries: [{ url: testURL }], extData: { key2: value2 } };
+
+ // create a new tab
+ let tab2 = getBrowser().addTab();
+ // set the tab's state
+ ss.setTabState(tab2, JSON.stringify(state));
+ tab2.linkedBrowser.addEventListener("load", function testTab2LBLoad(aEvent) {
+ this.removeEventListener("load", testTab2LBLoad, true);
+ // verify the correctness of the restored tab
+ ok(ss.getTabValue(tab2, key2) == value2 && this.currentURI.spec == testURL,
+ "the tab's state was correctly restored");
+
+ // add text data
+ let textbox = this.contentDocument.getElementById("textbox");
+ textbox.value = value3;
+
+ // duplicate the tab
+ let duplicateTab = ss.duplicateTab(window, tab2);
+ getBrowser().removeTab(tab2);
+
+ duplicateTab.linkedBrowser.addEventListener("load", function testTab2DupLBLoad(aEvent) {
+ this.removeEventListener("load", testTab2DupLBLoad, true);
+ // verify the correctness of the duplicated tab
+ ok(ss.getTabValue(duplicateTab, key2) == value2 && this.currentURI.spec == testURL,
+ "correctly duplicated the tab's state");
+ let textbox = this.contentDocument.getElementById("textbox");
+ is(textbox.value, value3, "also duplicated text data");
+
+ // clean up
+ getBrowser().removeTab(duplicateTab);
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_394759_basic.js b/comm/suite/components/tests/browser/browser_394759_basic.js
new file mode 100644
index 0000000000..a2137cc60c
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_394759_basic.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** Test for Bug 394759, ported in Bug 510890 **/
+
+function test() {
+ waitForExplicitFinish();
+
+ let testURL = "about:config";
+ let uniqueKey = "bug 394759";
+ let uniqueValue = "unik" + Date.now();
+ let uniqueText = "pi != " + Math.random();
+
+ // Be consistent: let the page actually display, as we are "interacting" with it.
+ Services.prefs.setBoolPref("general.warnOnAboutConfig", false);
+
+ // make sure that the next closed window will increase getClosedWindowCount
+ let max_windows_undo = Services.prefs.getIntPref("browser.sessionstore.max_windows_undo");
+ Services.prefs.setIntPref("browser.sessionstore.max_windows_undo", max_windows_undo + 1);
+ let closedWindowCount = ss.getClosedWindowCount();
+
+ provideWindow(function onTestURLLoaded(newWin) {
+ newWin.getBrowser().addTab().linkedBrowser.stop();
+
+ // mark the window with some unique data to be restored later on
+ ss.setWindowValue(newWin, uniqueKey, uniqueValue);
+ let textbox = newWin.content.document.getElementById("textbox");
+ textbox.value = uniqueText;
+
+ newWin.close();
+
+ is(ss.getClosedWindowCount(), closedWindowCount + 1,
+ "The closed window was added to Recently Closed Windows");
+ let data = JSON.parse(ss.getClosedWindowData())[0];
+ ok(data.title == testURL && JSON.stringify(data).includes(uniqueText),
+ "The closed window data was stored correctly");
+
+ // reopen the closed window and ensure its integrity
+ let newWin2 = ss.undoCloseWindow(0);
+
+ ok(newWin2 instanceof ChromeWindow,
+ "undoCloseWindow actually returned a window");
+ is(ss.getClosedWindowCount(), closedWindowCount,
+ "The reopened window was removed from Recently Closed Windows");
+
+ // SSTabRestored will fire more than once, so we need to make sure we count them
+ let restoredTabs = 0;
+ let expectedTabs = data.tabs.length;
+ newWin2.addEventListener("SSTabRestored", function sstabrestoredListener(aEvent) {
+ ++restoredTabs;
+ info("Restored tab " + restoredTabs + "/" + expectedTabs);
+ if (restoredTabs < expectedTabs) {
+ return;
+ }
+
+ newWin2.removeEventListener("SSTabRestored", sstabrestoredListener, true);
+
+ is(newWin2.getBrowser().tabs.length, 2,
+ "The window correctly restored 2 tabs");
+ is(newWin2.getBrowser().currentURI.spec, testURL,
+ "The window correctly restored the URL");
+
+ let textbox = newWin2.content.document.getElementById("textbox");
+ is(textbox.value, uniqueText,
+ "The window correctly restored the form");
+ is(ss.getWindowValue(newWin2, uniqueKey), uniqueValue,
+ "The window correctly restored the data associated with it");
+
+ // clean up
+ newWin2.close();
+ Services.prefs.clearUserPref("browser.sessionstore.max_windows_undo");
+ Services.prefs.clearUserPref("general.warnOnAboutConfig");
+ finish();
+ }, true);
+ }, testURL);
+}
diff --git a/comm/suite/components/tests/browser/browser_394759_behavior.js b/comm/suite/components/tests/browser/browser_394759_behavior.js
new file mode 100644
index 0000000000..79c70cd937
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_394759_behavior.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** Test for Bug 394759, ported in Bug 510890 **/
+
+function test() {
+ // This test takes quite some time, and timeouts frequently, so we require
+ // more time to run.
+ // See Bug 518970.
+ requestLongerTimeout(2);
+
+ waitForExplicitFinish();
+
+ // helper function that does the actual testing
+ function openWindowRec(windowsToOpen, expectedResults, recCallback) {
+ // do actual checking
+ if (!windowsToOpen.length) {
+ let closedWindowData = JSON.parse(ss.getClosedWindowData());
+ let numPopups = closedWindowData.filter(function(el, i, arr) {
+ return el.isPopup;
+ }).length;
+ let numNormal = ss.getClosedWindowCount() - numPopups;
+
+ let oResults = AppConstants.platform == "macosx" ? expectedResults.mac
+ : expectedResults.other;
+ is(numPopups, oResults.popup,
+ "There were " + oResults.popup + " popup windows to repoen");
+ is(numNormal, oResults.normal,
+ "There were " + oResults.normal + " normal windows to repoen");
+
+ // cleanup & return
+ executeSoon(recCallback);
+ return;
+ }
+
+ // hack to force window to be considered a popup (toolbar=no didn't work)
+ let winData = windowsToOpen.shift();
+ let settings = "chrome,dialog=no," +
+ (winData.isPopup ? "all=no" : "all");
+ let url = "http://example.com/?window=" + windowsToOpen.length;
+
+ provideWindow(function onTestURLLoaded(win) {
+ win.close();
+ openWindowRec(windowsToOpen, expectedResults, recCallback);
+ }, url, settings);
+ }
+
+ let windowsToOpen = [{isPopup: false},
+ {isPopup: false},
+ {isPopup: true},
+ {isPopup: true},
+ {isPopup: true}];
+ let expectedResults = {mac: {popup: 3, normal: 0},
+ other: {popup: 3, normal: 1}};
+ let windowsToOpen2 = [{isPopup: false},
+ {isPopup: false},
+ {isPopup: false},
+ {isPopup: false},
+ {isPopup: false}];
+ let expectedResults2 = {mac: {popup: 0, normal: 3},
+ other: {popup: 0, normal: 3}};
+ openWindowRec(windowsToOpen, expectedResults, function() {
+ openWindowRec(windowsToOpen2, expectedResults2, finish);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_408470.js b/comm/suite/components/tests/browser/browser_408470.js
new file mode 100644
index 0000000000..099aa7cbf4
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_408470.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 408470 **/
+
+ waitForExplicitFinish();
+
+ let pendingCount = 1;
+ let rootDir = getRootDirectory(gTestPath);
+ let testURL = rootDir + "browser_408470_sample.html";
+ let tab = getBrowser().addTab(testURL);
+ let window = tab.ownerDocument.defaultView;
+
+ tab.linkedBrowser.addEventListener("load", function loadListener1(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", loadListener1, true);
+ // enable all stylesheets and verify that they're correctly persisted
+ Array.from(tab.linkedBrowser.contentDocument.styleSheets).forEach(function(aSS, aIx) {
+ pendingCount++;
+ let ssTitle = aSS.title;
+ stylesheetSwitchAll(tab.linkedBrowser.contentWindow, ssTitle);
+
+ let newTab = ss.duplicateTab(window,tab);
+ newTab.linkedBrowser.addEventListener("load", function loadListener2(aEvent) {
+ newTab.linkedBrowser.removeEventListener("load", loadListener2, true);
+ let states = Array.from(newTab.linkedBrowser.contentDocument.styleSheets,
+ aSS => !aSS.disabled);
+ let correct = states.indexOf(true) == aIx && !states.includes(true, aIx + 1);
+
+ if (/^fail_/.test(ssTitle))
+ ok(!correct, "didn't restore stylesheet " + ssTitle);
+ else
+ ok(correct, "restored stylesheet " + ssTitle);
+
+ getBrowser().removeTab(newTab);
+ if (--pendingCount == 0)
+ finish();
+ }, true);
+ });
+
+ // disable all styles and verify that this is correctly persisted
+ tab.linkedBrowser.markupDocumentViewer.authorStyleDisabled = true;
+ let newTab = ss.duplicateTab(window,tab);
+ newTab.linkedBrowser.addEventListener("load", function loadListener3(aEvent) {
+ newTab.linkedBrowser.removeEventListener("load", loadListener3, true);
+ is(newTab.linkedBrowser.markupDocumentViewer.authorStyleDisabled, true,
+ "disabled all stylesheets");
+
+ getBrowser().removeTab(newTab);
+ if (--pendingCount == 0)
+ finish();
+ }, true);
+
+ getBrowser().removeTab(tab);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_408470_sample.html b/comm/suite/components/tests/browser/browser_408470_sample.html
new file mode 100644
index 0000000000..44122b9453
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_408470_sample.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<title>Test for bug 408470</title>
+
+<link href="404.css" title="default" rel="stylesheet">
+<link href="404.css" title="alternate" rel="alternate stylesheet">
+<link href="404.css" title="altERnate" rel=" styLEsheet altERnate ">
+<link href="404.css" title="media_empty" rel="alternate stylesheet" media="">
+<link href="404.css" title="media_all" rel="alternate stylesheet" media="all">
+<link href="404.css" title="media_ALL" rel="alternate stylesheet" media=" ALL ">
+<link href="404.css" title="media_screen" rel="alternate stylesheet" media="screen">
+<link href="404.css" title="media_print_screen" rel="alternate stylesheet" media="print,screen">
+<link href="404.css" title="fail_media_print" rel="alternate stylesheet" media="print">
+<link href="404.css" title="fail_media_projection" rel="stylesheet" media="projection">
+<link href="404.css" title="fail_media_invalid" rel="alternate stylesheet" media="hallo">
+
+</head>
+<body></body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_423132.js b/comm/suite/components/tests/browser/browser_423132.js
new file mode 100644
index 0000000000..87108f6c6f
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_423132.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ // test that cookies are stored and restored correctly by sessionstore,
+ // bug 423132, ported by bug 524371.
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+
+ let cs = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ cs.removeAll();
+
+ // make sure that sessionstore.js can be forced to be created by setting
+ // the interval pref to 0
+ Services.prefs.setIntPref("browser.sessionstore.interval", 0);
+
+ const testURL = "http://mochi.test:8888/browser/" +
+ "suite/common/tests/browser/browser_423132_sample.html";
+
+ // open a new window
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no", "about:blank");
+
+ // make sure sessionstore saves the cookie data, then close the window
+ newWin.addEventListener("load", function testNewWinLoad(aEvent) {
+ newWin.removeEventListener("load", testNewWinLoad);
+
+ newWin.getBrowser().selectedBrowser.loadURI(testURL, null, null);
+
+ newWin.getBrowser().addEventListener("pageshow", function testNewWinPageShow(aEvent) {
+ newWin.getBrowser().removeEventListener("pageshow", testNewWinPageShow, true);
+
+ // get the sessionstore state for the window
+ let state = ss.getWindowState(newWin);
+
+ // verify our cookie got set during pageload
+ let e = cs.enumerator;
+ let cookie;
+ let i = 0;
+ while (e.hasMoreElements()) {
+ cookie = e.getNext().QueryInterface(Ci.nsICookie);
+ i++;
+ }
+ is(i, 1, "expected one cookie");
+
+ // remove the cookie
+ cs.removeAll();
+
+ // restore the window state
+ ss.setWindowState(newWin, state, true);
+
+ // at this point, the cookie should be restored...
+ e = cs.enumerator;
+ let cookie2;
+ while (e.hasMoreElements()) {
+ cookie2 = e.getNext().QueryInterface(Ci.nsICookie);
+ if (cookie.name == cookie2.name)
+ break;
+ }
+ is(cookie.name, cookie2.name, "cookie name successfully restored");
+ is(cookie.value, cookie2.value, "cookie value successfully restored");
+ is(cookie.path, cookie2.path, "cookie path successfully restored");
+
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.interval"))
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ cs.removeAll();
+ newWin.close();
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ finish();
+ }, true);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_423132_sample.html b/comm/suite/components/tests/browser/browser_423132_sample.html
new file mode 100644
index 0000000000..bac1866cbc
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_423132_sample.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script>
+ // generate an enormous random number...
+ var r = Math.floor(Math.random() * Math.pow(2, 62)).toString();
+
+ // ... and use it to set a randomly named cookie
+ document.cookie = r + "=value; path=/ohai";
+ </script>
+<body>
+</body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_447951.js b/comm/suite/components/tests/browser/browser_447951.js
new file mode 100644
index 0000000000..259d49a0fa
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_447951.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 447951 **/
+
+ waitForExplicitFinish();
+ const baseURL = "http://mochi.test:8888/browser/" +
+ "suite/common/tests/browser/browser_447951_sample.html#";
+
+ let tab = getBrowser().addTab();
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+
+ let tabState = { entries: [] };
+ let max_entries = Services.prefs.getIntPref("browser.sessionhistory.max_entries");
+ for (let i = 0; i < max_entries; i++)
+ tabState.entries.push({ url: baseURL + i });
+
+ ss.setTabState(tab, JSON.stringify(tabState));
+ tab.addEventListener("SSTabRestored", function testTabSSTabRestored(aEvent) {
+ tab.removeEventListener("SSTabRestored", testTabSSTabRestored);
+ tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.entries.length, max_entries, "session history filled to the limit");
+ is(tabState.entries[0].url, baseURL + 0, "... but not more");
+
+ // visit yet another anchor (appending it to session history)
+ let doc = tab.linkedBrowser.contentDocument;
+ let event = doc.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, doc.defaultView, 1,
+ 0, 0, 0, 0, false, false, false, false, 0, null);
+ doc.querySelector("a").dispatchEvent(event);
+
+ executeSoon(function() {
+ tabState = JSON.parse(ss.getTabState(tab));
+ is(tab.linkedBrowser.currentURI.spec, baseURL + "end",
+ "the new anchor was loaded");
+ is(tabState.entries[tabState.entries.length - 1].url, baseURL + "end",
+ "... and ignored");
+ is(tabState.entries[0].url, baseURL + 1,
+ "... and the first item was removed");
+
+ // clean up
+ getBrowser().removeTab(tab);
+ finish();
+ });
+ });
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_447951_sample.html b/comm/suite/components/tests/browser/browser_447951_sample.html
new file mode 100644
index 0000000000..b9ad7bf1f1
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_447951_sample.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<title>Testcase for bug 447951</title>
+
+<a href="#end">click me</a>
diff --git a/comm/suite/components/tests/browser/browser_448741.js b/comm/suite/components/tests/browser/browser_448741.js
new file mode 100644
index 0000000000..85aee816f2
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_448741.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 448741 **/
+
+ waitForExplicitFinish();
+
+ let uniqueName = "bug 448741";
+ let uniqueValue = "as good as unique: " + Date.now();
+
+ // set a unique value on a new, blank tab
+ var tab = getBrowser().addTab();
+ tab.linkedBrowser.stop();
+ ss.setTabValue(tab, uniqueName, uniqueValue);
+ let valueWasCleaned = false;
+
+ // prevent our value from being written to disk
+ function cleaningObserver(aSubject, aTopic, aData) {
+ ok(aTopic == "sessionstore-state-write", "observed correct topic?");
+ ok(aSubject instanceof Ci.nsISupportsString, "subject is a string?");
+ ok(aSubject.data.includes(uniqueValue), "data contains our value?");
+
+ // find the data for the newly added tab and delete it
+ let state = JSON.parse(aSubject.data);
+ state.windows.forEach(function (winData) {
+ winData.tabs.forEach(function (tabData) {
+ if (tabData.extData && uniqueName in tabData.extData &&
+ tabData.extData[uniqueName] == uniqueValue) {
+ delete tabData.extData[uniqueName];
+ valueWasCleaned = true;
+ }
+ });
+ });
+
+ ok(valueWasCleaned, "found and removed the specific tab value");
+ aSubject.data = JSON.stringify(state);
+ Services.obs.removeObserver(cleaningObserver, aTopic, false);
+ }
+
+ // make sure that all later observers don't see that value any longer
+ function checkingObserver(aSubject, aTopic, aData) {
+ ok(valueWasCleaned && aSubject instanceof Ci.nsISupportsString,
+ "ready to check the cleaned state?");
+ ok(!aSubject.data.includes(uniqueValue), "data no longer contains our value?");
+
+ // clean up
+ getBrowser().removeTab(tab);
+ Services.obs.removeObserver(checkingObserver, aTopic, false);
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.interval"))
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ finish();
+ }
+
+ // last added observers are invoked first
+ Services.obs.addObserver(checkingObserver, "sessionstore-state-write");
+ Services.obs.addObserver(cleaningObserver, "sessionstore-state-write");
+
+ // trigger an immediate save operation
+ Services.prefs.setIntPref("browser.sessionstore.interval", 0);
+}
diff --git a/comm/suite/components/tests/browser/browser_454908.js b/comm/suite/components/tests/browser/browser_454908.js
new file mode 100644
index 0000000000..e5ce5f932e
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_454908.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 454908 **/
+
+ waitForExplicitFinish();
+
+ let fieldValues = {
+ username: "User " + Math.random(),
+ passwd: "pwd" + Date.now()
+ };
+
+ // make sure we do save form data
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 0);
+
+ let rootDir = getRootDirectory(gTestPath);
+ let testURL = rootDir + "browser_454908_sample.html";
+ let tab = getBrowser().addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+ let doc = tab.linkedBrowser.contentDocument;
+ for (let id in fieldValues)
+ doc.getElementById(id).value = fieldValues[id];
+
+ getBrowser().removeTab(tab);
+
+ tab = getBrowser().undoCloseTab();
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad2(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad2, true);
+ let doc = tab.linkedBrowser.contentDocument;
+ for (let id in fieldValues) {
+ let node = doc.getElementById(id);
+ if (node.type == "password")
+ is(node.value, "", "password wasn't saved/restored");
+ else
+ is(node.value, fieldValues[id], "username was saved/restored");
+ }
+
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.privacy_level"))
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ // undoCloseTab can reuse a single blank tab, so we have to
+ // make sure not to close the window when closing our last tab
+ if (gBrowser.tabContainer.childNodes.length == 1)
+ gBrowser.addTab();
+ gBrowser.removeTab(tab);
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_454908_sample.html b/comm/suite/components/tests/browser/browser_454908_sample.html
new file mode 100644
index 0000000000..02f40bf20b
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_454908_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 454908</title>
+
+<h3>Dummy Login</h3>
+<form>
+<p>Username: <input type="text" id="username">
+<p>Password: <input type="password" id="passwd">
+</form>
diff --git a/comm/suite/components/tests/browser/browser_456342.js b/comm/suite/components/tests/browser/browser_456342.js
new file mode 100644
index 0000000000..86bcb0ef06
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_456342.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 456342 **/
+
+ waitForExplicitFinish();
+
+ // make sure we do save form data
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 0);
+
+ let rootDir = getRootDirectory(gTestPath);
+ let testURL = rootDir + "browser_456342_sample.xhtml";
+ let tab = getBrowser().addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ this.removeEventListener("load", testTabLBLoad, true);
+
+ let expectedValue = "try to save me";
+ // Since bug 537289 we only save non-default values, so we need to set each
+ // form field's value after load.
+ let formEls = aEvent.originalTarget.forms[0].elements;
+ for (let i = 0; i < formEls.length; i++)
+ formEls[i].value = expectedValue;
+
+ getBrowser().removeTab(tab);
+
+ let undoItems = JSON.parse(ss.getClosedTabData(window));
+ let savedFormData = undoItems[0].state.entries[0].formdata;
+
+ let countGood = 0, countBad = 0;
+ for (let value of Object.values(savedFormData)) {
+ if (value == expectedValue)
+ countGood++;
+ else
+ countBad++;
+ }
+
+ is(countGood, 4, "Saved text for non-standard input fields");
+ is(countBad, 0, "Didn't save text for ignored field types");
+
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.privacy_level"))
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ finish();
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_456342_sample.xhtml b/comm/suite/components/tests/browser/browser_456342_sample.xhtml
new file mode 100644
index 0000000000..f0b0005b77
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_456342_sample.xhtml
@@ -0,0 +1,28 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head><title>Test for bug 456342</title></head>
+
+<body>
+<form>
+<h3>Non-standard &lt;input&gt;s</h3>
+<p>Search <input type="search" id="searchTerm"/></p>
+<p>Image Search: <input type="image search" /></p>
+<p>Autocomplete: <input type="autocomplete" name="fill-in"/></p>
+<p>Mistyped: <input type="txet" name="mistyped"/></p>
+
+<h3>Ignored types</h3>
+<input type="hidden" name="hideme"/>
+<input type="HIDDEN" name="hideme2"/>
+<input type="submit" name="submit"/>
+<input type="reset" name="reset"/>
+<input type="image" name="image"/>
+<input type="button" name="button"/>
+<input type="password" name="password"/>
+<input type="PassWord" name="password2"/>
+<input type="PASSWORD" name="password3"/>
+</form>
+
+</body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_461634.js b/comm/suite/components/tests/browser/browser_461634.js
new file mode 100644
index 0000000000..28207e7e6b
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_461634.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 461634, ported by Bug 524345 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let test_state = { windows: [{ "tabs": [{ "entries": [] }], _closedTabs: [
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
+ ] }] };
+ let remember_count = 2;
+
+ function countByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ }
+ catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // open a window and add the above closed tab list
+ let newWin = openDialog(location, "", "chrome,all,dialog=no");
+ newWin.addEventListener("load", function loadListener(aEvent) {
+ newWin.removeEventListener("load", loadListener);
+
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo",
+ test_state.windows[0]._closedTabs.length);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+
+ let closedTabs = JSON.parse(ss.getClosedTabData(newWin));
+ is(closedTabs.length, test_state.windows[0]._closedTabs.length,
+ "Closed tab list has the expected length");
+ is(countByTitle(closedTabs, FORGET),
+ test_state.windows[0]._closedTabs.length - remember_count,
+ "The correct amout of tabs are to be forgotten");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "Everything is set up.");
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(testForError(() => ss.forgetClosedTab({}, 0)),
+ "Invalid window for forgetClosedTab throws");
+ ok(testForError(() => ss.forgetClosedTab(newWin, -1)),
+ "Invalid tab for forgetClosedTab throws");
+ ok(testForError(() => ss.forgetClosedTab(newWin, test_state.windows[0]._closedTabs.length + 1)),
+ "Invalid tab for forgetClosedTab throws");
+
+ // Remove third tab, then first tab
+ ss.forgetClosedTab(newWin, 2);
+ ss.forgetClosedTab(newWin, null);
+
+ closedTabs = JSON.parse(ss.getClosedTabData(newWin));
+ is(closedTabs.length, remember_count,
+ "The correct amout of tabs was removed");
+ is(countByTitle(closedTabs, FORGET), 0,
+ "All tabs specifically forgotten were indeed removed");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "... and tabs not specifically forgetten weren't.");
+
+ // clean up
+ newWin.close();
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ finish();
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_463206.js b/comm/suite/components/tests/browser/browser_463206.js
new file mode 100644
index 0000000000..c044787546
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_463206.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 463206 **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "suite/common/tests/browser/browser_463206_sample.html";
+
+ var frameCount = 0;
+ let tab = getBrowser().addTab(testURL);
+ let window = tab.ownerDocument.defaultView;
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 5)
+ return;
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+ function typeText(aTextField, aValue) {
+ aTextField.value = aValue;
+
+ let event = aTextField.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, aTextField.ownerDocument.defaultView, 0);
+ aTextField.dispatchEvent(event);
+ }
+
+ let doc = tab.linkedBrowser.contentDocument;
+ typeText(doc.getElementById("out1"), Date.now());
+ typeText(doc.getElementsByName("1|#out2")[0], Math.random());
+ typeText(doc.defaultView.frames[0].frames[1].document.getElementById("in1"), new Date());
+
+ frameCount = 0;
+ let tab2 = ss.duplicateTab(window,tab);
+ tab2.linkedBrowser.addEventListener("load", function testTab2LBLoad(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 5)
+ return;
+ tab2.linkedBrowser.removeEventListener("load", testTab2LBLoad, true);
+
+ let doc = tab2.linkedBrowser.contentDocument;
+ let win = tab2.linkedBrowser.contentWindow;
+ isnot(doc.getElementById("out1").value,
+ win.frames[1].document.getElementById("out1").value,
+ "text isn't reused for frames");
+ isnot(doc.getElementsByName("1|#out2")[0].value, "",
+ "text containing | and # is correctly restored");
+ is(win.frames[1].document.getElementById("out2").value, "",
+ "id prefixes can't be faked");
+ // Disabled for now, Bug 588077
+ // isnot(win.frames[0].frames[1].document.getElementById("in1").value, "",
+ // "id prefixes aren't mixed up");
+ is(win.frames[1].frames[0].document.getElementById("in1").value, "",
+ "id prefixes aren't mixed up");
+
+ // clean up
+ getBrowser().removeTab(tab2);
+ getBrowser().removeTab(tab);
+
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_463206_sample.html b/comm/suite/components/tests/browser/browser_463206_sample.html
new file mode 100644
index 0000000000..48a841ee69
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_463206_sample.html
@@ -0,0 +1,10 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 463206</title>
+
+<iframe src="data:text/html,<iframe></iframe><iframe%20src='data:text/html,<input%2520id=%2522in1%2522>'></iframe>"></iframe>
+<iframe src="data:text/html,<input%20id='out1'><input%20id='out2'><iframe%20src='data:text/html,<input%2520id=%2522in1%2522>'>"></iframe>
+
+<input id="out1">
+<input name="1|#out2">
diff --git a/comm/suite/components/tests/browser/browser_465215.js b/comm/suite/components/tests/browser/browser_465215.js
new file mode 100644
index 0000000000..f34bd780b0
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_465215.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 465215 **/
+
+ waitForExplicitFinish();
+
+ let uniqueName = "bug 465215";
+ let uniqueValue1 = "as good as unique: " + Date.now();
+ let uniqueValue2 = "as good as unique: " + Math.random();
+
+ // set a unique value on a new, blank tab
+ let tab1 = gBrowser.addTab();
+ tab1.linkedBrowser.addEventListener("load", function testTab1LBLoad() {
+ tab1.linkedBrowser.removeEventListener("load", testTab1LBLoad, true);
+ ss.setTabValue(tab1, uniqueName, uniqueValue1);
+
+ // duplicate the tab with that value
+ let tab2 = ss.duplicateTab(window, tab1);
+ is(ss.getTabValue(tab2, uniqueName), uniqueValue1, "tab value was duplicated");
+
+ ss.setTabValue(tab2, uniqueName, uniqueValue2);
+ isnot(ss.getTabValue(tab1, uniqueName), uniqueValue2, "tab values aren't sync'd");
+
+ // overwrite the tab with the value which should remove it
+ ss.setTabState(tab1, JSON.stringify({ entries: [] }));
+ tab1.linkedBrowser.addEventListener("load", function testTab1LBLoad2() {
+ tab1.linkedBrowser.removeEventListener("load", testTab1LBLoad2, true);
+ is(ss.getTabValue(tab1, uniqueName), "", "tab value was cleared");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_465223.js b/comm/suite/components/tests/browser/browser_465223.js
new file mode 100644
index 0000000000..89e1e69042
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_465223.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 465223 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+
+ let uniqueKey1 = "bug 465223.1";
+ let uniqueKey2 = "bug 465223.2";
+ let uniqueValue1 = "unik" + Date.now();
+ let uniqueValue2 = "pi != " + Math.random();
+
+ // open a window and set a value on it
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ newWin.addEventListener("load", function loadListener(aEvent) {
+ newWin.removeEventListener("load", loadListener);
+
+ ss.setWindowValue(newWin, uniqueKey1, uniqueValue1);
+
+ let newState = { windows: [{ tabs:[{ entries: [] }], extData: {} }] };
+ newState.windows[0].extData[uniqueKey2] = uniqueValue2;
+ ss.setWindowState(newWin, JSON.stringify(newState), false);
+
+ is(newWin.gBrowser.tabContainer.childNodes.length, 2,
+ "original tab wasn't overwritten");
+ is(ss.getWindowValue(newWin, uniqueKey1), uniqueValue1,
+ "window value wasn't overwritten when the tabs weren't");
+ is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue2,
+ "new window value was correctly added");
+
+ newState.windows[0].extData[uniqueKey2] = uniqueValue1;
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ is(newWin.gBrowser.tabContainer.childNodes.length, 1,
+ "original tabs were overwritten");
+ is(ss.getWindowValue(newWin, uniqueKey1), "",
+ "window value was cleared");
+ is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue1,
+ "window value was correctly overwritten");
+
+ // clean up
+ newWin.close();
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ finish();
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_466937.js b/comm/suite/components/tests/browser/browser_466937.js
new file mode 100644
index 0000000000..8d34f65aeb
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_466937.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 466937 **/
+
+ waitForExplicitFinish();
+
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("466937_test.file");
+ let testPath = file.path;
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "suite/common/tests/browser/browser_466937_sample.html";
+
+ let tab = getBrowser().addTab(testURL);
+ let window = tab.ownerDocument.defaultView;
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+ let doc = tab.linkedBrowser.contentDocument;
+ doc.getElementById("reverse_thief").value = "/home/user/secret2";
+ doc.getElementById("bystander").value = testPath;
+
+ let tab2 = ss.duplicateTab(window,tab);
+ tab2.linkedBrowser.addEventListener("load", function testTab2LBLoad(aEvent) {
+ tab2.linkedBrowser.removeEventListener("load", testTab2LBLoad, true);
+ doc = tab2.linkedBrowser.contentDocument;
+ is(doc.getElementById("thief").value, "",
+ "file path wasn't set to text field value");
+ is(doc.getElementById("reverse_thief").value, "",
+ "text field value wasn't set to full file path");
+ is(doc.getElementById("bystander").value, testPath,
+ "normal case: file path was correctly preserved");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_466937_sample.html b/comm/suite/components/tests/browser/browser_466937_sample.html
new file mode 100644
index 0000000000..f876719987
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_466937_sample.html
@@ -0,0 +1,21 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 466937</title>
+
+<input id="thief" value="/home/user/secret">
+<input type="file" id="reverse_thief">
+<input type="file" id="bystander">
+
+<script>
+ window.addEventListener("DOMContentLoaded", function windowDOMContentLoaded() {
+ window.removeEventListener("DOMContentLoaded", windowDOMContentLoaded);
+ if (!document.location.hash) {
+ document.location.hash = "#ready";
+ }
+ else {
+ document.getElementById("thief").type = "file";
+ document.getElementById("reverse_thief").type = "text";
+ }
+ });
+</script>
diff --git a/comm/suite/components/tests/browser/browser_477657.js b/comm/suite/components/tests/browser/browser_477657.js
new file mode 100644
index 0000000000..23683a8b12
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_477657.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 477657 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ // Test fails randomly on OS X (bug 482975)
+ if ("nsILocalFileMac" in Ci)
+ return;
+
+ waitForExplicitFinish();
+
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ newWin.addEventListener("load", function loadListener(aEvent) {
+ newWin.removeEventListener("load", loadListener);
+
+ let newState = { windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [{
+ state: { entries: [{ url: "about:" }]},
+ title: "About:"
+ }],
+ sizemode: "maximized"
+ }] };
+
+ let uniqueKey = "bug 477657";
+ let uniqueValue = "unik" + Date.now();
+
+ ss.setWindowValue(newWin, uniqueKey, uniqueValue);
+ is(ss.getWindowValue(newWin, uniqueKey), uniqueValue,
+ "window value was set before the window was overwritten");
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ // use setTimeout(..., 0) to mirror sss_restoreWindowFeatures
+ setTimeout(function() {
+ is(ss.getWindowValue(newWin, uniqueKey), "",
+ "window value was implicitly cleared");
+
+ is(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window was maximized");
+
+ is(JSON.parse(ss.getClosedTabData(newWin)).length, 1,
+ "the closed tab was added before the window was overwritten");
+ delete newState.windows[0]._closedTabs;
+ delete newState.windows[0].sizemode;
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ setTimeout(function() {
+ is(JSON.parse(ss.getClosedTabData(newWin)).length, 0,
+ "closed tabs were implicitly cleared");
+
+ is(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window remains maximized");
+ newState.windows[0].sizemode = "normal";
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ setTimeout(function() {
+ isnot(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window was explicitly unmaximized");
+
+ newWin.close();
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ finish();
+ }, 0);
+ }, 0);
+ }, 0);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_480893.js b/comm/suite/components/tests/browser/browser_480893.js
new file mode 100644
index 0000000000..41de93c8f1
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_480893.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 480893 **/
+
+ waitForExplicitFinish();
+
+ // Test that starting a new session loads a blank page if Firefox is
+ // configured to display a blank page at startup (browser.startup.page = 0)
+ Services.prefs.setIntPref("browser.startup.page", 0);
+ let tab = getBrowser().addTab("about:sessionrestore");
+ getBrowser().selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function testBrowserLoad(aEvent) {
+ browser.removeEventListener("load", testBrowserLoad, true);
+ let doc = browser.contentDocument;
+
+ // click on the "Start New Session" button after about:sessionrestore is loaded
+ doc.getElementById("errorCancel").click();
+ browser.addEventListener("load", function testBrowserLoad2(aEvent) {
+ browser.removeEventListener("load", testBrowserLoad2, true);
+ let doc = browser.contentDocument;
+
+ is(doc.URL, "about:blank", "loaded page is about:blank");
+
+ // Test that starting a new session loads the homepage (set to http://mochi.test:8888)
+ // if Firefox is configured to display a homepage at startup (browser.startup.page = 1)
+ let homepage = "http://mochi.test:8888/";
+ Services.prefs.setCharPref("browser.startup.homepage", homepage);
+ Services.prefs.setIntPref("browser.startup.page", 1);
+ getBrowser().loadURI("about:sessionrestore");
+ browser.addEventListener("load", function testBrowserLoad3(aEvent) {
+ browser.removeEventListener("load", testBrowserLoad3, true);
+ let doc = browser.contentDocument;
+
+ // click on the "Start New Session" button after about:sessionrestore is loaded
+ doc.getElementById("errorCancel").click();
+ browser.addEventListener("load", function testBrowserLoad4(aEvent) {
+ browser.removeEventListener("load", testBrowserLoad4, true);
+ let doc = browser.contentDocument;
+
+ is(doc.URL, homepage, "loaded page is the homepage");
+
+ // close tab, restore default values and finish the test
+ getBrowser().removeTab(tab);
+ // we need this if-statement because if there is no user set value,
+ // clearUserPref throws a uncatched exception and finish is not called
+ if (Services.prefs.prefHasUserValue("browser.startup.page"))
+ Services.prefs.clearUserPref("browser.startup.page");
+ Services.prefs.clearUserPref("browser.startup.homepage");
+ finish();
+ }, true);
+ }, true);
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_483330.js b/comm/suite/components/tests/browser/browser_483330.js
new file mode 100644
index 0000000000..3e650488b7
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_483330.js
@@ -0,0 +1,37 @@
+function test() {
+ /** Test for Bug 483330 **/
+
+ waitForExplicitFinish();
+
+ let tab = getBrowser().addTab();
+ getBrowser().selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function loadListener(e) {
+ browser.removeEventListener("load", loadListener, true);
+
+ // Scroll the content document
+ browser.contentWindow.scrollTo(1100, 1200);
+ is(browser.contentWindow.scrollX, 1100, "scrolled horizontally");
+ is(browser.contentWindow.scrollY, 1200, "scrolled vertically");
+
+ getBrowser().removeTab(tab);
+
+ let newTab = ss.undoCloseTab(window, 0);
+ newTab.addEventListener("SSTabRestored", function tabRestored(e) {
+ newTab.removeEventListener("SSTabRestored", tabRestored, true);
+
+ let newBrowser = newTab.linkedBrowser;
+
+ // check that the scroll position was restored
+ is(newBrowser.contentWindow.scrollX, 1100, "still scrolled horizontally");
+ is(newBrowser.contentWindow.scrollY, 1200, "still scrolled vertically");
+
+ getBrowser().removeTab(newTab);
+
+ finish();
+ }, true);
+ }, true);
+
+ browser.loadURI("data:text/html,<body style='width: 100000px; height: 100000px;'><p>top</p></body>");
+}
diff --git a/comm/suite/components/tests/browser/browser_485482.js b/comm/suite/components/tests/browser/browser_485482.js
new file mode 100644
index 0000000000..6e4573f609
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_485482.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 485482, ported by Bug 487922 **/
+
+ waitForExplicitFinish();
+
+ let uniqueValue = Math.random();
+
+ let rootDir = getRootDirectory(gTestPath);
+ let testURL = rootDir + "browser_485482_sample.html";
+ let tab = getBrowser().addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true);
+ let doc = tab.linkedBrowser.contentDocument;
+ doc.querySelector("input[type=text]").value = uniqueValue;
+ doc.querySelector("input[type=checkbox]").checked = true;
+
+ let tab2 = ss.duplicateTab(window, tab);
+ tab2.linkedBrowser.addEventListener("load", function testTab2LBLoad(aEvent) {
+ tab2.linkedBrowser.removeEventListener("load", testTab2LBLoad, true);
+ doc = tab2.linkedBrowser.contentDocument;
+ is(doc.querySelector("input[type=text]").value, uniqueValue,
+ "generated XPath expression was valid");
+ ok(doc.querySelector("input[type=checkbox]").checked,
+ "generated XPath expression was valid");
+
+ // clean up
+ getBrowser().removeTab(tab2);
+ getBrowser().removeTab(tab);
+ finish();
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_485482_sample.html b/comm/suite/components/tests/browser/browser_485482_sample.html
new file mode 100644
index 0000000000..c2097b5930
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_485482_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Test for bug 485482</title>
+
+<bad=name>
+ <input type="text">
+</bad=name>
+
+<worse=name>
+ <l0c@l+na~e"'§>
+ <input type="checkbox" name="check"> Check
+ </l0c@l+na~e"'§>
+</worse=name>
diff --git a/comm/suite/components/tests/browser/browser_490040.js b/comm/suite/components/tests/browser/browser_490040.js
new file mode 100644
index 0000000000..91687058f6
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_490040.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 490040, ported by Bug 511640 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+
+ function testWithState(aState) {
+ // Ensure we can store the window if needed.
+ let curClosedWindowCount = ss.getClosedWindowCount();
+ Services.prefs.setIntPref("browser.sessionstore.max_windows_undo",
+ curClosedWindowCount + 1);
+
+ var origWin;
+ function windowObserver(aSubject, aTopic, aData) {
+ let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ if (origWin && theWin != origWin)
+ return;
+
+ switch (aTopic) {
+ case "domwindowopened":
+ origWin = theWin;
+ theWin.addEventListener("load", function testTheWinLoad() {
+ theWin.removeEventListener("load", testTheWinLoad);
+ executeSoon(function () {
+ // Close the window as soon as the first tab loads, or
+ // immediately if there are no tabs.
+ if (aState.windowState.windows[0].tabs[0].entries.length) {
+ theWin.gBrowser.addEventListener("load",
+ function testTheWinLoad2() {
+ theWin.gBrowser.removeEventListener("load", testTheWinLoad2,
+ true);
+ theWin.close();
+ }, true);
+ } else {
+ executeSoon(function () {
+ theWin.close();
+ });
+ }
+ ss.setWindowState(theWin, JSON.stringify(aState.windowState),
+ true);
+ });
+ });
+ break;
+
+ case "domwindowclosed":
+ Services.ww.unregisterNotification(windowObserver);
+ // Use executeSoon to ensure this happens after SS observer.
+ executeSoon(function () {
+ is(ss.getClosedWindowCount(),
+ curClosedWindowCount + (aState.shouldBeAdded ? 1 : 0),
+ "That window should " + (aState.shouldBeAdded ? "" : "not ") +
+ "be restorable");
+ executeSoon(runNextTest);
+ });
+ break;
+ }
+ }
+ Services.ww.registerNotification(windowObserver);
+ Services.ww.openWindow(null,
+ location,
+ "_blank",
+ "chrome,all,dialog=no",
+ null);
+ }
+
+ // Only windows with open tabs are restorable. Windows where a lone tab is
+ // detached may have _closedTabs, but is left with just an empty tab.
+ let states = [
+ {
+ shouldBeAdded: true,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [{ url: "http://example.com", title: "example.com" }] }],
+ selected: 1,
+ _closedTabs: []
+ }]
+ }
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: []
+ }]
+ }
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [{ state: { entries: [{ url: "http://example.com", index: 1 }] } }]
+ }]
+ }
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [],
+ extData: { keyname: "pi != " + Math.random() }
+ }]
+ }
+ }
+ ];
+
+ function runNextTest() {
+ if (states.length) {
+ let state = states.shift();
+ testWithState(state);
+ }
+ else {
+ if (Services.prefs.prefHasUserValue("browser.sessionstore.max_windows_undo"))
+ Services.prefs.clearUserPref("browser.sessionstore.max_windows_undo");
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ finish();
+ }
+ }
+ runNextTest();
+}
+
diff --git a/comm/suite/components/tests/browser/browser_491168.js b/comm/suite/components/tests/browser/browser_491168.js
new file mode 100644
index 0000000000..82ed998e99
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_491168.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ // make sure we use sessionstore for undoClosetab
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+
+ /** Test for Bug 491168, ported by Bug 524369 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+
+ const REFERRER1 = "http://example.org/?" + Date.now();
+ const REFERRER2 = "http://example.org/?" + Math.random();
+
+ let tab = getBrowser().addTab();
+ getBrowser().selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function testBrowserLoad() {
+ browser.removeEventListener("load", testBrowserLoad, true);
+
+ let tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.entries[0].referrer, REFERRER1,
+ "Referrer retrieved via getTabState matches referrer set via loadURI.");
+
+ tabState.entries[0].referrer = REFERRER2;
+ ss.setTabState(tab, JSON.stringify(tabState));
+
+ tab.addEventListener("SSTabRestored", function testBrowserTabRestored() {
+ tab.removeEventListener("SSTabRestored", testBrowserTabRestored, true);
+ is(window.content.document.referrer, REFERRER2, "document.referrer matches referrer set via setTabState.");
+
+ getBrowser().removeTab(tab);
+ let newTab = ss.undoCloseTab(window, 0);
+ newTab.addEventListener("SSTabRestored", function testBrowserNewTabRest() {
+ newTab.removeEventListener("SSTabRestored", testBrowserNewTabRest, true);
+
+ is(window.content.document.referrer, REFERRER2, "document.referrer is still correct after closing and reopening the tab.");
+ getBrowser().removeTab(newTab);
+
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ // clean up
+ if (Services.prefs.prefHasUserValue("browser.tabs.max_tabs_undo"))
+ Services.prefs.clearUserPref("browser.tabs.max_tabs_undo");
+ finish();
+ }, true);
+ }, true);
+ },true);
+
+ let referrerURI = Services.io.newURI(REFERRER1);
+ browser.loadURI("http://example.org", referrerURI, null);
+}
diff --git a/comm/suite/components/tests/browser/browser_491577.js b/comm/suite/components/tests/browser/browser_491577.js
new file mode 100644
index 0000000000..f65590abaf
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_491577.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 491577 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let test_state = {
+ windows: [ { tabs: [{ entries: [{ url: "http://example.com/", triggeringPrincipal_base64 }] }], selected: 1 } ],
+ _closedWindows: [
+ // _closedWindows[0]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", triggeringPrincipal_base64, title: "title" }] },
+ { entries: [{ url: "http://mozilla.org/", triggeringPrincipal_base64, title: "title" }] }
+ ],
+ selected: 2,
+ title: FORGET,
+ _closedTabs: []
+ },
+ // _closedWindows[1]
+ {
+ tabs: [
+ { entries: [{ url: "http://mozilla.org/", triggeringPrincipal_base64, title: "title" }] },
+ { entries: [{ url: "http://example.com/", triggeringPrincipal_base64, title: "title" }] },
+ { entries: [{ url: "http://mozilla.org/", triggeringPrincipal_base64, title: "title" }] },
+ ],
+ selected: 3,
+ title: REMEMBER,
+ _closedTabs: []
+ },
+ // _closedWindows[2]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", triggeringPrincipal_base64, title: "title" }] }
+ ],
+ selected: 1,
+ title: FORGET,
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ { url: "http://mozilla.org/", triggeringPrincipal_base64, title: "title" },
+ { url: "http://mozilla.org/again", triggeringPrincipal_base64, title: "title" }
+ ]
+ },
+ pos: 1,
+ title: "title"
+ },
+ {
+ state: {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64, title: "title" }
+ ]
+ },
+ title: "title"
+ }
+ ]
+ }
+ ]
+ };
+ let remember_count = 1;
+
+ function countByTitle(aClosedWindowList, aTitle) {
+ return aClosedWindowList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ } catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // open a window and add the above closed window list
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
+ test_state._closedWindows.length);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+
+ let closedWindows = JSON.parse(ss.getClosedWindowData());
+ is(closedWindows.length, test_state._closedWindows.length,
+ "Closed window list has the expected length");
+ is(countByTitle(closedWindows, FORGET),
+ test_state._closedWindows.length - remember_count,
+ "The correct amount of windows are to be forgotten");
+ is(countByTitle(closedWindows, REMEMBER), remember_count,
+ "Everything is set up.");
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(testForError(() => ss.forgetClosedWindow(-1)),
+ "Invalid window for forgetClosedWindow throws");
+ ok(testForError(() => ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
+ "Invalid window for forgetClosedWindow throws");
+
+ // Remove third window, then first window
+ ss.forgetClosedWindow(2);
+ ss.forgetClosedWindow(null);
+
+ closedWindows = JSON.parse(ss.getClosedWindowData());
+ is(closedWindows.length, remember_count,
+ "The correct amount of windows were removed");
+ is(countByTitle(closedWindows, FORGET), 0,
+ "All windows specifically forgotten were indeed removed");
+ is(countByTitle(closedWindows, REMEMBER), remember_count,
+ "... and windows not specifically forgetten weren't.");
+
+ // clean up
+ gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_493467.js b/comm/suite/components/tests/browser/browser_493467.js
new file mode 100644
index 0000000000..1b8f5f78d8
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_493467.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 493467, ported by Bug 524365 **/
+
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ let tab = getBrowser().addTab();
+ tab.linkedBrowser.stop();
+ let tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.disallow || "", "", "Everything is allowed per default");
+
+ // collect all permissions that can be set on a docShell (i.e. all
+ // attributes starting with "allow" such as "allowJavascript") and
+ // disallow them all, as SessionStore only remembers disallowed ones
+ let permissions = [];
+ let docShell = tab.linkedBrowser.docShell;
+ for (let attribute in docShell) {
+ if (/^allow([A-Z].*)/.test(attribute)) {
+ permissions.push(RegExp.$1);
+ docShell[attribute] = false;
+ }
+ }
+
+ // make sure that all available permissions have been remembered
+ tabState = JSON.parse(ss.getTabState(tab));
+ let disallow = tabState.disallow.split(",");
+ permissions.forEach(function(aName) {
+ ok(disallow.includes(aName), "Saved state of allow" + aName);
+ });
+ // IF A TEST FAILS, please add the missing permission's name (without the
+ // leading "allow") to nsSessionStore.js's CAPABILITIES array. Thanks.
+
+ getBrowser().removeTab(tab);
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+}
diff --git a/comm/suite/components/tests/browser/browser_500328.js b/comm/suite/components/tests/browser/browser_500328.js
new file mode 100644
index 0000000000..2286a5f6c3
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_500328.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function checkState(tab) {
+ // Go back and then forward, and make sure that the state objects received
+ // from the popState event are as we expect them to be.
+ //
+ // We also add a node to the document's body when after going back and make
+ // sure it's still there after we go forward -- this is to test that the two
+ // history entries correspond to the same document.
+
+ let popStateCount = 0;
+
+ tab.linkedBrowser.addEventListener('popstate', function checkStateTabPopState(aEvent) {
+ let contentWindow = tab.linkedBrowser.contentWindow;
+ if (popStateCount == 0) {
+ popStateCount++;
+
+ is(tab.linkedBrowser.contentWindow.testState, 'foo',
+ 'testState after going back');
+
+ ok(aEvent.state, "Event should have a state property.");
+ is(JSON.stringify(tab.linkedBrowser.contentWindow.history.state), JSON.stringify({obj1:1}),
+ "first popstate object.");
+
+ // Add a node with id "new-elem" to the document.
+ let doc = contentWindow.document;
+ ok(!doc.getElementById("new-elem"),
+ "doc shouldn't contain new-elem before we add it.");
+ let elem = doc.createElement("div");
+ elem.id = "new-elem";
+ doc.body.appendChild(elem);
+
+ contentWindow.history.forward();
+ }
+ else if (popStateCount == 1) {
+ popStateCount++;
+ is(aEvent.state.obj3.toString(), '/^a$/', "second popstate object.");
+
+ // Make sure that the new-elem node is present in the document. If it's
+ // not, then this history entry has a different doc identifier than the
+ // previous entry, which is bad.
+ let doc = contentWindow.document;
+ let newElem = doc.getElementById("new-elem");
+ ok(newElem, "doc should contain new-elem.");
+ newElem.remove();
+ ok(!doc.getElementById("new-elem"), "new-elem should be removed.");
+
+ // Clean up after ourselves and finish the test.
+ tab.linkedBrowser.removeEventListener("popstate", checkStateTabPopState,
+ true);
+ getBrowser().removeTab(tab);
+ finish();
+ }
+ }, true);
+
+ // Set some state in the page's window. When we go back(), the page should
+ // be retrieved from bfcache, and this state should still be there.
+ tab.linkedBrowser.contentWindow.testState = 'foo';
+
+ // Now go back. This should trigger the popstate event handler above.
+ tab.linkedBrowser.contentWindow.history.back();
+}
+
+function test() {
+ // Tests session restore functionality of history.pushState and
+ // history.replaceState(). (Bug 500328)
+
+ waitForExplicitFinish();
+
+ // We open a new blank window, let it load, and then load in
+ // http://example.com. We need to load the blank window first, otherwise the
+ // docshell gets confused and doesn't have a current history entry.
+ let tab = getBrowser().addTab("about:blank");
+ let tabBrowser = tab.linkedBrowser;
+
+ tabBrowser.addEventListener("load", function testTabBrowserLoad(aEvent) {
+ tabBrowser.removeEventListener("load", testTabBrowserLoad, true);
+
+ tabBrowser.loadURI("http://example.com", null, null);
+
+ tabBrowser.addEventListener("load", function testTabBrowserLoad2(aEvent) {
+ tabBrowser.removeEventListener("load", testTabBrowserLoad2, true);
+
+ // After these push/replaceState calls, the window should have three
+ // history entries:
+ // testURL (state object: null) <-- oldest
+ // testURL (state object: {obj1:1})
+ // testURL?page2 (state object: {obj3:/^a$/}) <-- newest
+ let contentWindow = tab.linkedBrowser.contentWindow;
+ let history = contentWindow.history;
+ history.pushState({obj1:1}, "title-obj1");
+ history.pushState({obj2:2}, "title-obj2", "?page2");
+ history.replaceState({obj3:/^a$/}, "title-obj3");
+
+ let state = ss.getTabState(tab);
+ getBrowser().removeTab(tab);
+
+ // Restore the state into a new tab. Things don't work well when we
+ // restore into the old tab, but that's not a real use case anyway.
+ let tab2 = getBrowser().addTab("about:blank");
+ ss.setTabState(tab2, state, true);
+
+ // Run checkState() once the tab finishes loading its restored state.
+ tab2.linkedBrowser.addEventListener("load", function testTBTab2LBLoad() {
+ tab2.linkedBrowser.removeEventListener("load", testTBTab2LBLoad, true);
+ SimpleTest.executeSoon(function() {
+ checkState(tab2);
+ });
+ }, true);
+
+ }, true);
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_514751.js b/comm/suite/components/tests/browser/browser_514751.js
new file mode 100644
index 0000000000..2290814aa9
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_514751.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 509315 (Wallpaper) **/
+
+ waitForExplicitFinish();
+
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [
+ { url: "http://www.mozilla.org/projects/minefield/", title: "Minefield Start Page" },
+ {}
+ ]
+ }]
+ }]
+ };
+
+ let windowObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+
+ switch(aTopic) {
+ case "domwindowopened":
+ theWin.addEventListener("load", function testTheWinLoad() {
+ theWin.removeEventListener("load", testTheWinLoad);
+ executeSoon(function() {
+ var gotError = false;
+ try {
+ ss.setWindowState(theWin, JSON.stringify(state), true);
+ } catch (e) {
+ if (/NS_ERROR_MALFORMED_URI/.test(e))
+ gotError = true;
+ }
+ ok(!gotError, "Didn't get a malformed URI error.");
+ executeSoon(function() {
+ theWin.close();
+ });
+ });
+ });
+ break;
+
+ case "domwindowclosed":
+ Services.ww.unregisterNotification(this);
+ finish();
+ break;
+ }
+ }
+ }
+ Services.ww.registerNotification(windowObserver);
+ Services.ww.openWindow(null,
+ location,
+ "_blank",
+ "chrome,all,dialog=no",
+ null);
+
+}
diff --git a/comm/suite/components/tests/browser/browser_522545.js b/comm/suite/components/tests/browser/browser_522545.js
new file mode 100644
index 0000000000..9088c88c81
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_522545.js
@@ -0,0 +1,280 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 522545 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, then crashes or quits.
+ function test_newTabFocused() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [], userTypedValue: "example.com", userTypedClear: 0 }
+ ],
+ selected: 2
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = getBrowser().selectedBrowser;
+ is(browser.currentURI.spec, "about:blank",
+ "No history entries still sets currentURI to about:blank");
+ is(browser.userTypedValue, "example.com",
+ "userTypedValue was correctly restored");
+ is(browser.userTypedClear, 0,
+ "userTypeClear restored as expected");
+ is(gURLBar.value, "example.com",
+ "Address bar's value correctly restored");
+ // Change tabs to make sure address bar value gets updated
+ getBrowser().selectedTab = getBrowser().tabContainer.getItemAtIndex(0);
+ is(gURLBar.value, "about:mozilla",
+ "Address bar's value correctly updated");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, switches back to the first tab, then crashes or quits.
+ function test_newTabNotFocused() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [], userTypedValue: "example.org", userTypedClear: 0 }
+ ],
+ selected: 1
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = getBrowser().getBrowserAtIndex(1);
+ is(browser.currentURI.spec, "about:blank",
+ "No history entries still sets currentURI to about:blank");
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was correctly restored");
+ is(browser.userTypedClear, 0,
+ "userTypeClear restored as expected");
+ is(gURLBar.value, "about:mozilla",
+ "Address bar's value correctly restored");
+ // Change tabs to make sure address bar value gets updated
+ getBrowser().selectedTab = getBrowser().tabContainer.getItemAtIndex(1);
+ is(gURLBar.value, "example.org",
+ "Address bar's value correctly updated");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, then types something in the
+ // address bar, then crashes or quits.
+ function test_existingSHEnd_noClear() {
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [{ url: "about:mozilla" }, { url: "about:config" }],
+ index: 2,
+ userTypedValue: "example.com",
+ userTypedClear: 0
+ }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = getBrowser().selectedBrowser;
+ is(browser.currentURI.spec, "about:config",
+ "browser.currentURI set to current entry in SH");
+ is(browser.userTypedValue, "example.com",
+ "userTypedValue was correctly restored");
+ is(browser.userTypedClear, 0,
+ "userTypeClear restored as expected");
+ is(gURLBar.value, "example.com",
+ "Address bar's value correctly restored to userTypedValue");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, presses back at some point, then
+ // types something in the address bar, then crashes or quits.
+ function test_existingSHMiddle_noClear() {
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [{ url: "about:mozilla" }, { url: "about:config" }],
+ index: 1,
+ userTypedValue: "example.org",
+ userTypedClear: 0
+ }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = getBrowser().selectedBrowser;
+ is(browser.currentURI.spec, "about:mozilla",
+ "browser.currentURI set to current entry in SH");
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was correctly restored");
+ is(browser.userTypedClear, 0,
+ "userTypeClear restored as expected");
+ is(gURLBar.value, "example.org",
+ "Address bar's value correctly restored to userTypedValue");
+ runNextTest();
+ });
+ }
+
+ // This test simulates lots of tabs opening at once and then quitting/crashing.
+ function test_getBrowserState_lotsOfTabsOpening() {
+ getBrowser().stop();
+
+ let uris = [];
+ for (let i = 0; i < 25; i++)
+ uris.push("http://example.com/" + i);
+
+ // We're waiting for the first location change, which should indicate
+ // one of the tabs has loaded and the others haven't. So one should
+ // be in a non-userTypedValue case, while others should still have
+ // userTypedValue and userTypedClear set.
+ getBrowser().addTabsProgressListener({
+ onLocationChange: function (aBrowser) {
+ if (uris.includes(aBrowser.currentURI.spec)) {
+ getBrowser().removeTabsProgressListener(this);
+ firstLocationChange();
+ }
+ }
+ });
+
+ function firstLocationChange() {
+ let state = JSON.parse(ss.getBrowserState());
+ let hasUTV = state.windows[0].tabs.some(function(aTab) {
+ return aTab.userTypedValue && aTab.userTypedClear && !aTab.entries.length;
+ });
+
+ ok(hasUTV, "At least one tab has a userTypedValue with userTypedClear with no loaded URL");
+
+ getBrowser().addEventListener("load", firstLoad, true);
+ }
+
+ function firstLoad() {
+ getBrowser().removeEventListener("load", firstLoad, true);
+
+ let state = JSON.parse(ss.getBrowserState());
+ let hasSH = state.windows[0].tabs.some(function(aTab) {
+ return !("userTypedValue" in aTab) && aTab.entries[0].url;
+ });
+
+ ok(hasSH, "At least one tab has its entry in SH");
+
+ runNextTest();
+ }
+
+ getBrowser().loadTabs(uris);
+ }
+
+ // This simulates setting a userTypedValue and ensures that just typing in the
+ // URL bar doesn't set userTypedClear as well.
+ function test_getBrowserState_userTypedValue() {
+ let state = {
+ windows: [{
+ tabs: [{ entries: [] }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = getBrowser().selectedBrowser;
+ // Make sure this tab isn't loading and state is clear before we test.
+ is(browser.userTypedValue, null, "userTypedValue is empty to start");
+ is(browser.userTypedClear, 0, "userTypedClear is 0 to start");
+
+ gURLBar.value = "example.org";
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, false);
+ gURLBar.dispatchEvent(event);
+
+ executeSoon(function() {
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was set when changing gURLBar.value");
+ is(browser.userTypedClear, 0,
+ "userTypedClear was not changed when changing gURLBar.value");
+
+ // Now make sure ss gets these values too
+ let newState = JSON.parse(ss.getBrowserState());
+ is(newState.windows[0].tabs[0].userTypedValue, "example.org",
+ "sessionstore got correct userTypedValue");
+ is(newState.windows[0].tabs[0].userTypedClear, 0,
+ "sessionstore got correct userTypedClear");
+ runNextTest();
+ });
+ });
+ }
+
+ // test_getBrowserState_lotsOfTabsOpening tested userTypedClear in a few cases,
+ // but not necessarily any that had legitimate URIs in the state of loading
+ // (eg, "http://example.com"), so this test will cover that case.
+ function test_userTypedClearLoadURI() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [], userTypedValue: "http://example.com", userTypedClear: 2 }
+ ]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "http://example.com/",
+ "userTypedClear=2 caused userTypedValue to be loaded");
+ is(browser.userTypedValue, null,
+ "userTypedValue was null after loading a URI");
+ is(browser.userTypedClear, 0,
+ "userTypeClear reset to 0");
+ is(gURLBar.value, "http://example.com/",
+ "Address bar's value set after loading URI");
+ runNextTest();
+ });
+ }
+
+
+ let tests = [test_newTabFocused, test_newTabNotFocused,
+ test_existingSHEnd_noClear, test_existingSHMiddle_noClear,
+ test_getBrowserState_lotsOfTabsOpening,
+ test_getBrowserState_userTypedValue, test_userTypedClearLoadURI];
+ let originalState = ss.getBrowserState();
+ let state = {
+ windows: [{
+ tabs: [{ entries: [{ url: "about:blank" }] }]
+ }]
+ };
+ function runNextTest() {
+ if (tests.length) {
+ waitForBrowserState(state, tests.shift());
+ } else {
+ ss.setBrowserState(originalState);
+ executeSoon(function () {
+ is(browserWindowsCount(), 1, "Only one browser window should be open eventually");
+ finish();
+ });
+ }
+ }
+
+ // Run the tests!
+ runNextTest();
+}
diff --git a/comm/suite/components/tests/browser/browser_524745.js b/comm/suite/components/tests/browser/browser_524745.js
new file mode 100644
index 0000000000..c14b868779
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_524745.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function browserWindowsCount() {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ return count;
+}
+
+function test() {
+ /** Test for Bug 524745, ported by bug 558638 **/
+ is(browserWindowsCount(), 1, "Only one browser window should be open initially");
+
+ let uniqKey = "bug524745";
+ let uniqVal = Date.now();
+
+ waitForExplicitFinish();
+
+ let window_B = openDialog(location, "_blank", "chrome,all,dialog=no");
+ window_B.addEventListener("load", function testWindowBLoad(aEvent) {
+ window_B.removeEventListener("load", testWindowBLoad);
+
+ waitForFocus(function() {
+ // Add identifying information to window_B
+ ss.setWindowValue(window_B, uniqKey, uniqVal);
+ let state = JSON.parse(ss.getBrowserState());
+ let selectedWindow = state.windows[state.selectedWindow - 1];
+ is(selectedWindow.extData && selectedWindow.extData[uniqKey], uniqVal,
+ "selectedWindow is window_B");
+
+ // Now minimize window_B. The selected window shouldn't have the secret data
+ window_B.minimize();
+ waitForFocus(function() {
+ state = JSON.parse(ss.getBrowserState());
+ selectedWindow = state.windows[state.selectedWindow - 1];
+ ok(!selectedWindow.extData || !selectedWindow.extData[uniqKey],
+ "selectedWindow is not window_B after minimizing it");
+
+ // Now minimize the last open window (assumes no other tests left windows open)
+ window.minimize();
+ state = JSON.parse(ss.getBrowserState());
+ is(state.selectedWindow, 0,
+ "selectedWindow should be 0 when all windows are minimized");
+
+ // Cleanup
+ window.restore();
+ window_B.close();
+ is(browserWindowsCount(), 1,
+ "Only one browser window should be open eventually");
+ finish();
+ });
+ }, window_B);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_526613.js b/comm/suite/components/tests/browser/browser_526613.js
new file mode 100644
index 0000000000..d7a664403f
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_526613.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 526613, porting done in Bug 548211 **/
+
+ waitForExplicitFinish();
+
+ function browserWindowsCount(expected) {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ is(count, expected,
+ "number of open browser windows according to nsIWindowMediator");
+ let state = ss.getBrowserState();
+ info(state);
+ is(JSON.parse(state).windows.length, expected,
+ "number of open browser windows according to getBrowserState");
+ }
+
+ browserWindowsCount(1);
+
+ // backup old state
+ let oldState = ss.getBrowserState();
+ // create a new state for testing
+ let testState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 },
+ { tabs: [{ entries: [{ url: "about:mozilla" }] }], selected: 1 },
+ ],
+ // make sure the first window is focused, otherwise when restoring the
+ // old state, the first window is closed and the test harness gets unloaded
+ selectedWindow: 1
+ };
+
+ let pass = 1;
+ function observer(aSubject, aTopic, aData) {
+ is(aTopic, "sessionstore-browser-state-restored",
+ "The sessionstore-browser-state-restored notification was observed");
+
+ if (pass++ == 1) {
+ browserWindowsCount(2);
+
+ // let the first window be focused (see above)
+ function pollMostRecentWindow() {
+ if (Services.wm.getMostRecentWindow("navigator:browser") == window) {
+ ss.setBrowserState(oldState);
+ } else {
+ info("waiting for the current window to become active");
+ setTimeout(pollMostRecentWindow, 0);
+ window.focus(); //XXX Why is this needed?
+ }
+ }
+ pollMostRecentWindow();
+ }
+ else {
+ browserWindowsCount(1);
+ ok(!window.closed, "Restoring the old state should have left this window open");
+ Services.obs.removeObserver(observer, "sessionstore-browser-state-restored");
+ finish();
+ }
+ }
+ Services.obs.addObserver(observer, "sessionstore-browser-state-restored");
+
+ // set browser to test state
+ ss.setBrowserState(JSON.stringify(testState));
+}
diff --git a/comm/suite/components/tests/browser/browser_528776.js b/comm/suite/components/tests/browser/browser_528776.js
new file mode 100644
index 0000000000..3f316ace06
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_528776.js
@@ -0,0 +1,29 @@
+function browserWindowsCount(expected) {
+ var count = 0;
+ var e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ is(count, expected,
+ "number of open browser windows according to nsIWindowMediator");
+ is(JSON.parse(ss.getBrowserState()).windows.length, expected,
+ "number of open browser windows according to getBrowserState");
+}
+
+function test() {
+ /** Test for Bug 528776, ported by Bug 548228 **/
+
+ waitForExplicitFinish();
+
+ browserWindowsCount(1);
+
+ var win = openDialog(location, "", "chrome,all,dialog=no");
+ win.addEventListener("load", function loadListener() {
+ win.removeEventListener("load", loadListener);
+ browserWindowsCount(2);
+ win.close();
+ browserWindowsCount(1);
+ finish();
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_581937.js b/comm/suite/components/tests/browser/browser_581937.js
new file mode 100644
index 0000000000..5f807715c5
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_581937.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ // Tests that an about:blank tab with no history will not be saved into
+ // session store and thus, it will not show up in Recently Closed Tabs.
+
+var tab;
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0);
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+
+ is(ss.getClosedTabCount(window), 0, "should be no closed tabs");
+
+ getBrowser().tabContainer.addEventListener("TabOpen", onTabOpen, true);
+
+ tab = getBrowser().addTab();
+}
+
+function onTabOpen(aEvent) {
+ getBrowser().tabContainer.removeEventListener("TabOpen", onTabOpen, true);
+
+ // Let other listeners react to the TabOpen event before removing the tab.
+ executeSoon(function() {
+ is(getBrowser().browsers[1].currentURI.spec, "about:blank",
+ "we will be removing an about:blank tab");
+
+ getBrowser().removeTab(tab);
+
+ is(ss.getClosedTabCount(window), 0, "should still be no closed tabs");
+
+ Services.prefs.clearUserPref("browser.tabs.max_tabs_undo");
+ executeSoon(finish);
+ });
+}
diff --git a/comm/suite/components/tests/browser/browser_586068-cascaded_restore.js b/comm/suite/components/tests/browser/browser_586068-cascaded_restore.js
new file mode 100644
index 0000000000..6389884048
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_586068-cascaded_restore.js
@@ -0,0 +1,730 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var stateBackup = ss.getBrowserState();
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+
+function test() {
+ /** Test for Bug 586068 - Cascade page loads when restoring **/
+ waitForExplicitFinish();
+ // This test does a lot of window opening / closing and waiting for loads.
+ // In order to prevent timeouts, we'll extend the default that mochitest uses.
+ requestLongerTimeout(4);
+ runNextTest();
+}
+
+// test_reloadCascade, test_reloadReload are generated tests that are run out
+// of cycle (since they depend on current state). They're listed in [tests] here
+// so that it is obvious when they run in respect to the other tests.
+var tests = [test_cascade, test_select, test_multiWindowState,
+ test_setWindowStateNoOverwrite, test_setWindowStateOverwrite,
+ test_setBrowserStateInterrupted, test_reload,
+ /* test_reloadReload, */ test_reloadCascadeSetup,
+ /* test_reloadCascade */];
+function runNextTest() {
+ // Reset the pref
+ try {
+ Services.prefs.clearUserPref("browser.sessionstore.max_concurrent_tabs");
+ } catch (e) {}
+
+ // set an empty state & run the next test, or finish
+ if (tests.length) {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ var currentWindow = windowsEnum.getNext();
+ if (currentWindow != window) {
+ currentWindow.close();
+ }
+ }
+
+ ss.setBrowserState(JSON.stringify({ windows: [{ tabs: [{ url: 'about:blank' }] }] }));
+ let currentTest = tests.shift();
+ info("running " + currentTest.name);
+ executeSoon(currentTest);
+ }
+ else {
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ }
+}
+
+
+function test_cascade() {
+ // Set the pref to 1 so we know exactly how many tabs should be restoring at any given time
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 1);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ dump("\n\nload: " + aBrowser.currentURI.spec + "\n" + JSON.stringify(countTabs()) + "\n\n");
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_cascade_progressCallback();
+ }
+ }
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
+ ] }] };
+
+ let loadCount = 0;
+ // Since our progress listener is fired before the one in sessionstore, our
+ // expected counts look a little weird. This is because we inspect the state
+ // before sessionstore has marked the tab as finished restoring and before it
+ // starts restoring the next tab
+ let expectedCounts = [
+ [5, 1, 0],
+ [4, 1, 1],
+ [3, 1, 2],
+ [2, 1, 3],
+ [1, 1, 4],
+ [0, 1, 5]
+ ];
+
+ function test_cascade_progressCallback() {
+ loadCount++;
+ let counts = countTabs();
+ let expected = expectedCounts[loadCount - 1];
+
+ is(counts[0], expected[0], "test_cascade: load " + loadCount + " - # tabs that need to be restored");
+ is(counts[1], expected[1], "test_cascade: load " + loadCount + " - # tabs that are restoring");
+ is(counts[2], expected[2], "test_cascade: load " + loadCount + " - # tabs that has been restored");
+
+ if (loadCount < state.windows[0].tabs.length)
+ return;
+
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ runNextTest();
+ }
+
+ // This progress listener will get attached before the listener in session store.
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+
+function test_select() {
+ // Set the pref to 0 so we know exactly how many tabs should be restoring at
+ // any given time. This guarantees that a finishing load won't start another.
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 0);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_select_progressCallback(aBrowser);
+ }
+ }
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
+ ], selected: 1 }] };
+
+ let loadCount = 0;
+ // expectedCounts looks a little wierd for the test case, but it works. See
+ // comment in test_cascade for an explanation
+ let expectedCounts = [
+ [5, 1, 0],
+ [4, 1, 1],
+ [3, 1, 2],
+ [2, 1, 3],
+ [1, 1, 4],
+ [0, 1, 5]
+ ];
+ let tabOrder = [0, 5, 1, 4, 3, 2];
+
+ function test_select_progressCallback(aBrowser) {
+ loadCount++;
+
+ let counts = countTabs();
+ let expected = expectedCounts[loadCount - 1];
+
+ is(counts[0], expected[0], "test_select: load " + loadCount + " - # tabs that need to be restored");
+ is(counts[1], expected[1], "test_select: load " + loadCount + " - # tabs that are restoring");
+ is(counts[2], expected[2], "test_select: load " + loadCount + " - # tabs that has been restored");
+
+ if (loadCount < state.windows[0].tabs.length) {
+ // double check that this tab was the right one
+ let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
+ let tab;
+ for (let i = 0; i < window.getBrowser().tabs.length; i++) {
+ if (!tab && window.getBrowser().tabs[i].linkedBrowser == aBrowser)
+ tab = window.getBrowser().tabs[i];
+ }
+ is(ss.getTabValue(tab, "uniq"), expectedData, "test_select: load " + loadCount + " - correct tab was restored");
+
+ // select the next tab
+ window.getBrowser().selectTabAtIndex(tabOrder[loadCount]);
+ return;
+ }
+
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ runNextTest();
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+
+function test_multiWindowState() {
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // We only care about load events when the tab still has
+ // __SS_restoreState == TAB_STATE_RESTORING on it.
+ // Since our listener is attached before the sessionstore one, this works out.
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_multiWindowState_progressCallback(aBrowser);
+ }
+ }
+
+ // The first window will be put into the already open window and the second
+ // window will be opened with _openWindowWithState, which is the source of the problem.
+ let state = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
+ ],
+ selected: 4
+ }
+ ] };
+ let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
+
+ let loadCount = 0;
+ function test_multiWindowState_progressCallback(aBrowser) {
+ loadCount++;
+
+ if (loadCount < numTabs)
+ return;
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "test_multiWindowState: all tabs were restored");
+ let count = countTabs();
+ is(count[0], 0,
+ "test_multiWindowState: there are no tabs left needing restore");
+
+ // Remove the progress listener from this window, it will be removed from
+ // theWin when that window is closed (in setBrowserState).
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ runNextTest();
+ }
+
+ // We also want to catch the 2nd window, so we need to observe domwindowopened
+ function windowObserver(aSubject, aTopic, aData) {
+ let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ if (aTopic == "domwindowopened") {
+ theWin.addEventListener("load", function theWinLoad() {
+ theWin.removeEventListener("load", theWinLoad);
+
+ Services.ww.unregisterNotification(windowObserver);
+ theWin.getBrowser().addTabsProgressListener(progressListener);
+ });
+ }
+ }
+ Services.ww.registerNotification(windowObserver);
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+
+function test_setWindowStateNoOverwrite() {
+ // Set the pref to 1 so we know exactly how many tabs should be restoring at any given time
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 1);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // We only care about load events when the tab still has
+ // __SS_restoreState == TAB_STATE_RESTORING on it.
+ // Since our listener is attached before the sessionstore one, this works out.
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_setWindowStateNoOverwrite_progressCallback(aBrowser);
+ }
+ }
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com#1" }] },
+ { entries: [{ url: "http://example.com#2" }] },
+ { entries: [{ url: "http://example.com#3" }] },
+ { entries: [{ url: "http://example.com#4" }] },
+ { entries: [{ url: "http://example.com#5" }] },
+ ] }] };
+ let state2 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }] },
+ { entries: [{ url: "http://example.org#2" }] },
+ { entries: [{ url: "http://example.org#3" }] },
+ { entries: [{ url: "http://example.org#4" }] },
+ { entries: [{ url: "http://example.org#5" }] }
+ ] }] };
+
+ let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ function test_setWindowStateNoOverwrite_progressCallback(aBrowser) {
+ loadCount++;
+
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (loadCount == 2)
+ ss.setWindowState(window, JSON.stringify(state2), false);
+
+ if (loadCount < numTabs)
+ return;
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
+ // window.__SS_tabsToRestore isn't decremented until after the progress
+ // listener is called. Since we get in here before that, we still expect
+ // the count to be 1.
+ is(window.__SS_tabsToRestore, 1,
+ "test_setWindowStateNoOverwrite: window doesn't think there are more tabs to restore");
+ let count = countTabs();
+ is(count[0], 0,
+ "test_setWindowStateNoOverwrite: there are no tabs left needing restore");
+
+ // Remove the progress listener from this window, it will be removed from
+ // theWin when that window is closed (in setBrowserState).
+ window.getBrowser().removeTabsProgressListener(progressListener);
+
+ runNextTest();
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setWindowState(window, JSON.stringify(state1), true);
+}
+
+
+function test_setWindowStateOverwrite() {
+ // Set the pref to 1 so we know exactly how many tabs should be restoring at any given time
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 1);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // We only care about load events when the tab still has
+ // __SS_restoreState == TAB_STATE_RESTORING on it.
+ // Since our listener is attached before the sessionstore one, this works out.
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_setWindowStateOverwrite_progressCallback(aBrowser);
+ }
+ }
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com#1" }] },
+ { entries: [{ url: "http://example.com#2" }] },
+ { entries: [{ url: "http://example.com#3" }] },
+ { entries: [{ url: "http://example.com#4" }] },
+ { entries: [{ url: "http://example.com#5" }] },
+ ] }] };
+ let state2 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }] },
+ { entries: [{ url: "http://example.org#2" }] },
+ { entries: [{ url: "http://example.org#3" }] },
+ { entries: [{ url: "http://example.org#4" }] },
+ { entries: [{ url: "http://example.org#5" }] }
+ ] }] };
+
+ let numTabs = 2 + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ function test_setWindowStateOverwrite_progressCallback(aBrowser) {
+ loadCount++;
+
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (loadCount == 2)
+ ss.setWindowState(window, JSON.stringify(state2), true);
+
+ if (loadCount < numTabs)
+ return;
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "test_setWindowStateOverwrite: all tabs were restored");
+ // window.__SS_tabsToRestore isn't decremented until after the progress
+ // listener is called. Since we get in here before that, we still expect
+ // the count to be 1.
+ is(window.__SS_tabsToRestore, 1,
+ "test_setWindowStateOverwrite: window doesn't think there are more tabs to restore");
+ let count = countTabs();
+ is(count[0], 0,
+ "test_setWindowStateOverwrite: there are no tabs left needing restore");
+
+ // Remove the progress listener from this window, it will be removed from
+ // theWin when that window is closed (in setBrowserState).
+ window.getBrowser().removeTabsProgressListener(progressListener);
+
+ runNextTest();
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setWindowState(window, JSON.stringify(state1), true);
+}
+
+
+function test_setBrowserStateInterrupted() {
+ // Set the pref to 1 so we know exactly how many tabs should be restoring at any given time
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 1);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // We only care about load events when the tab still has
+ // __SS_restoreState == TAB_STATE_RESTORING on it.
+ // Since our listener is attached before the sessionstore one, this works out.
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_setBrowserStateInterrupted_progressCallback(aBrowser);
+ }
+ }
+
+ // The first state will be loaded using setBrowserState, followed by the 2nd
+ // state also being loaded using setBrowserState, interrupting the first restore.
+ let state1 = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ ],
+ selected: 3
+ }
+ ] };
+ let state2 = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
+ ],
+ selected: 3
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
+ ],
+ selected: 1
+ }
+ ] };
+
+ // interruptedAfter will be set after the selected tab from each window have loaded.
+ let interruptedAfter = 0;
+ let loadedWindow1 = false;
+ let loadedWindow2 = false;
+ let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
+
+ let loadCount = 0;
+ function test_setBrowserStateInterrupted_progressCallback(aBrowser) {
+ loadCount++;
+
+ if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
+ loadedWindow1 = true;
+ if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
+ loadedWindow2 = true;
+
+ if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
+ interruptedAfter = loadCount;
+ ss.setBrowserState(JSON.stringify(state2));
+ return;
+ }
+
+ if (loadCount < numTabs + interruptedAfter)
+ return;
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs + interruptedAfter,
+ "test_setBrowserStateInterrupted: all tabs were restored");
+ let count = countTabs();
+ is(count[0], 0,
+ "test_setBrowserStateInterrupted: there are no tabs left needing restore");
+
+ // Remove the progress listener from this window, it will be removed from
+ // theWin when that window is closed (in setBrowserState).
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ Services.ww.unregisterNotification(windowObserver);
+ runNextTest();
+ }
+
+ // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
+ function windowObserver(aSubject, aTopic, aData) {
+ let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ if (aTopic == "domwindowopened") {
+ theWin.addEventListener("load", function wObserverTheWinLoad() {
+ theWin.removeEventListener("load", wObserverTheWinLoad);
+
+ Services.ww.unregisterNotification(windowObserver);
+ theWin.getBrowser().addTabsProgressListener(progressListener);
+ });
+ }
+ }
+ Services.ww.registerNotification(windowObserver);
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state1));
+}
+
+
+function test_reload() {
+ // Set the pref to 0 so we know exactly how many tabs should be restoring at
+ // any given time. This guarantees that a finishing load won't start another.
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 0);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_reload_progressCallback(aBrowser);
+ }
+ }
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } }
+ ], selected: 1 }] };
+
+ let loadCount = 0;
+ function test_reload_progressCallback(aBrowser) {
+ loadCount++;
+
+ is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url,
+ "test_reload: load " + loadCount + " - browser loaded correct url");
+
+ if (loadCount <= state.windows[0].tabs.length) {
+ // double check that this tab was the right one
+ let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq;
+ let tab;
+ for (let i = 0; i < window.getBrowser().tabs.length; i++) {
+ if (!tab && window.getBrowser().tabs[i].linkedBrowser == aBrowser)
+ tab = window.getBrowser().tabs[i];
+ }
+ is(ss.getTabValue(tab, "uniq"), expectedData,
+ "test_reload: load " + loadCount + " - correct tab was restored");
+
+ if (loadCount == state.windows[0].tabs.length) {
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ executeSoon(function() {
+ _test_reloadAfter("test_reloadReload", state, runNextTest);
+ });
+ }
+ else {
+ // reload the next tab
+ window.getBrowser().reloadTab(window.getBrowser().tabs[loadCount]);
+ }
+ }
+
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+
+// This doesn't actually test anything, just does a cascaded restore with default
+// settings. This really just sets up to test that reloads work.
+function test_reloadCascadeSetup() {
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_cascadeReloadSetup_progressCallback();
+ }
+ }
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } }
+ ] }] };
+
+ let loadCount = 0;
+ function test_cascadeReloadSetup_progressCallback() {
+ loadCount++;
+ if (loadCount < state.windows[0].tabs.length)
+ return;
+
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ executeSoon(function() {
+ _test_reloadAfter("test_reloadCascade", state, runNextTest);
+ });
+ }
+
+ // This progress listener will get attached before the listener in session store.
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+
+// This is a generic function that will attempt to reload each test. We do this
+// a couple times, so make it utilitarian.
+// This test expects that aState contains a single window and that each tab has
+// a unique extData value eg. { "uniq": value }.
+function _test_reloadAfter(aTestName, aState, aCallback) {
+ info("starting " + aTestName);
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ test_reloadAfter_progressCallback(aBrowser);
+ }
+ }
+
+ // Simulate a left mouse button click with no modifiers, which is what
+ // Command-R, or clicking reload does.
+ let fakeEvent = {
+ button: 0,
+ metaKey: false,
+ altKey: false,
+ ctrlKey: false,
+ shiftKey: false,
+ }
+
+ let loadCount = 0;
+ function test_reloadAfter_progressCallback(aBrowser) {
+ loadCount++;
+
+ if (loadCount <= aState.windows[0].tabs.length) {
+ // double check that this tab was the right one
+ let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq;
+ let tab;
+ for (let i = 0; i < window.getBrowser().tabs.length; i++) {
+ if (!tab && window.getBrowser().tabs[i].linkedBrowser == aBrowser)
+ tab = window.getBrowser().tabs[i];
+ }
+ is(ss.getTabValue(tab, "uniq"), expectedData,
+ aTestName + ": load " + loadCount + " - correct tab was reloaded");
+
+ if (loadCount == aState.windows[0].tabs.length) {
+ window.getBrowser().removeTabsProgressListener(progressListener);
+ aCallback();
+ }
+ else {
+ // reload the next tab
+ window.getBrowser().selectTabAtIndex(loadCount);
+ BrowserReload(fakeEvent);
+ }
+ }
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ BrowserReload(fakeEvent);
+}
+
+
+function countTabs() {
+ let needsRestore = 0,
+ isRestoring = 0,
+ wasRestored = 0;
+
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.closed)
+ continue;
+
+ for (let i = 0; i < window.getBrowser().tabs.length; i++) {
+ let browser = window.getBrowser().tabs[i].linkedBrowser;
+ if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+ isRestoring++;
+ else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ needsRestore++;
+ else
+ wasRestored++;
+ }
+ }
+ return [needsRestore, isRestoring, wasRestored];
+}
+
+function r() {
+ return "" + Date.now() + Math.random();
+}
+
diff --git a/comm/suite/components/tests/browser/browser_597315.js b/comm/suite/components/tests/browser/browser_597315.js
new file mode 100644
index 0000000000..516ff5ae88
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var stateBackup = ss.getBrowserState();
+
+function test() {
+ /** Test for Bug 597315 - Frameset history does not work properly when restoring a tab **/
+ waitForExplicitFinish();
+
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+
+ let testURL = getRootDirectory(gTestPath) + "browser_597315_index.html";
+ let tab = getBrowser().addTab(testURL);
+ getBrowser().selectedTab = tab;
+
+ waitForLoadsInBrowser(tab.linkedBrowser, 4, function() {
+ let browser_b = tab.linkedBrowser.contentDocument.getElementsByTagName("frame")[1];
+ let document_b = browser_b.contentDocument;
+ let links = document_b.getElementsByTagName("a");
+
+ // We're going to click on the first link, so listen for another load event
+ waitForLoadsInBrowser(tab.linkedBrowser, 1, function() {
+ waitForLoadsInBrowser(tab.linkedBrowser, 1, function() {
+
+ getBrowser().removeTab(tab);
+ // wait for 4 loads again...
+ let newTab = ss.undoCloseTab(window, 0);
+
+ waitForLoadsInBrowser(newTab.linkedBrowser, 4, function() {
+ getBrowser().goBack();
+ waitForLoadsInBrowser(newTab.linkedBrowser, 1, function() {
+
+ let expectedURLEnds = ["a.html", "b.html", "c1.html"];
+ let frames = newTab.linkedBrowser.contentDocument.getElementsByTagName("frame");
+ for (let i = 0; i < frames.length; i++) {
+ is(frames[i].contentDocument.location,
+ getRootDirectory(gTestPath) + "browser_597315_" + expectedURLEnds[i],
+ "frame " + i + " has the right url");
+ }
+ Services.prefs.clearUserPref("browser.tabs.max_tabs_undo");
+ getBrowser().removeTab(newTab);
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ });
+ });
+ });
+ EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
+ });
+ EventUtils.sendMouseEvent({type:"click"}, links[0], browser_b.contentWindow);
+ });
+}
+
+// helper function
+function waitForLoadsInBrowser(aBrowser, aLoadCount, aCallback) {
+ let loadCount = 0;
+ aBrowser.addEventListener("load", function aBrowserLoad(aEvent) {
+ if (++loadCount < aLoadCount)
+ return;
+
+ aBrowser.removeEventListener("load", aBrowserLoad, true);
+ aCallback();
+ }, true);
+}
diff --git a/comm/suite/components/tests/browser/browser_597315_a.html b/comm/suite/components/tests/browser/browser_597315_a.html
new file mode 100755
index 0000000000..8e7b35d7a1
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_a.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm A!
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_597315_b.html b/comm/suite/components/tests/browser/browser_597315_b.html
new file mode 100755
index 0000000000..f8dbfb2a27
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_b.html
@@ -0,0 +1,10 @@
+<html>
+ <body>
+ I'm B!<br/>
+ <a target="c" href="browser_597315_c1.html">click me first</a><br/>
+ <a target="c" href="browser_597315_c2.html">then click me</a><br/>
+ Close this tab.<br/>
+ Restore this tab.<br/>
+ Click back.<br/>
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_597315_c.html b/comm/suite/components/tests/browser/browser_597315_c.html
new file mode 100755
index 0000000000..0efd7d9026
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_c.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C!
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_597315_c1.html b/comm/suite/components/tests/browser/browser_597315_c1.html
new file mode 100755
index 0000000000..b55c1d45a9
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_c1.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C1!
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_597315_c2.html b/comm/suite/components/tests/browser/browser_597315_c2.html
new file mode 100755
index 0000000000..aec504141b
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_c2.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C2!
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/browser_597315_index.html b/comm/suite/components/tests/browser/browser_597315_index.html
new file mode 100644
index 0000000000..1465ddf044
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_597315_index.html
@@ -0,0 +1,10 @@
+<html>
+ <frameset cols="20%,80%">
+ <frameset rows="30%,70%">
+ <frame src="browser_597315_a.html"/>
+ <frame src="browser_597315_b.html"/>
+ </frameset>
+ <frame src="browser_597315_c.html" name="c"/>
+ </frameset>
+</html>
+
diff --git a/comm/suite/components/tests/browser/browser_607016.js b/comm/suite/components/tests/browser/browser_607016.js
new file mode 100644
index 0000000000..9de13dd05a
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_607016.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+
+var stateBackup = ss.getBrowserState();
+
+function cleanup() {
+ // Reset the pref
+ try {
+ Services.prefs.clearUserPref("browser.sessionstore.max_concurrent_tabs");
+ } catch (e) {}
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+}
+
+function test() {
+ /** Bug 607016 - If a tab is never restored, attributes (eg. hidden) aren't updated correctly **/
+ waitForExplicitFinish();
+
+ // Set the pref to 0 so we know exactly how many tabs should be restoring at
+ // any given time. This guarantees that a finishing load won't start another.
+ Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 0);
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+
+ // We have our own progress listener for this test, which we'll attach before our state is set
+ let progressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ progressCallback(aBrowser);
+ }
+ }
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } }, // overwriting
+ //{ entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } }, // hiding
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }, // adding
+ { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } }, // deleting
+ { entries: [{ url: "http://example.org#6" }] } // creating
+ ], selected: 1 }] };
+
+ function progressCallback(aBrowser) {
+ // We'll remove the progress listener after the first one because we aren't
+ // loading any other tabs
+ window.getBrowser().removeTabsProgressListener(progressListener);
+
+ let curState = JSON.parse(ss.getBrowserState());
+ for (let i = 0; i < curState.windows[0].tabs.length; i++) {
+ if (state.windows[0].tabs[i].extData) {
+ is(curState.windows[0].tabs[i].extData["uniq"],
+ state.windows[0].tabs[i].extData["uniq"],
+ "sanity check that tab has correct extData");
+ }
+ else
+ ok(!("extData" in curState.windows[0].tabs[i]),
+ "sanity check that tab doesn't have extData");
+ }
+
+ // Now we'll set a new unique value on 1 of the tabs
+ let newUniq = r();
+ ss.setTabValue(getBrowser().tabs[1], "uniq", newUniq);
+ getBrowser().removeTab(getBrowser().tabs[1]);
+ let closedTabData = (JSON.parse(ss.getClosedTabData(window)))[0];
+ is(closedTabData.state.extData.uniq, newUniq,
+ "(overwriting) new data is stored in extData");
+
+ // hide the next tab before closing it
+ //getBrowser().hideTab(getBrowser().tabs[1]);
+ //getBrowser().removeTab(getBrowser().tabs[1]);
+ //closedTabData = (JSON.parse(ss.getClosedTabData(window)))[0];
+ //ok(closedTabData.state.hidden, "(hiding) tab data has hidden == true");
+
+ // set data that's not in a conflicting key
+ let stillUniq = r();
+ ss.setTabValue(getBrowser().tabs[1], "stillUniq", stillUniq);
+ getBrowser().removeTab(getBrowser().tabs[1]);
+ closedTabData = (JSON.parse(ss.getClosedTabData(window)))[0];
+ is(closedTabData.state.extData.stillUniq, stillUniq,
+ "(adding) new data is stored in extData");
+
+ // remove the uniq value and make sure it's not there in the closed data
+ ss.deleteTabValue(getBrowser().tabs[1], "uniq");
+ getBrowser().removeTab(getBrowser().tabs[1]);
+ closedTabData = (JSON.parse(ss.getClosedTabData(window)))[0];
+ // Since Panorama might have put data in, first check if there is extData.
+ // If there is explicitly check that "uniq" isn't in it. Otherwise, we're ok
+ if ("extData" in closedTabData.state) {
+ ok(!("uniq" in closedTabData.state.extData),
+ "(deleting) uniq not in existing extData");
+ }
+ else {
+ ok(true, "(deleting) no data is stored in extData");
+ }
+
+ // set unique data on the tab that never had any set, make sure that's saved
+ let newUniq2 = r();
+ ss.setTabValue(getBrowser().tabs[1], "uniq", newUniq2);
+ getBrowser().removeTab(getBrowser().tabs[1]);
+ closedTabData = (JSON.parse(ss.getClosedTabData(window)))[0];
+ is(closedTabData.state.extData.uniq, newUniq2,
+ "(creating) new data is stored in extData where there was none");
+
+ cleanup();
+ }
+
+ window.getBrowser().addTabsProgressListener(progressListener);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+// Helper function to create a random value
+function r() {
+ return "" + Date.now() + Math.random();
+}
+
diff --git a/comm/suite/components/tests/browser/browser_615394-SSWindowState_events.js b/comm/suite/components/tests/browser/browser_615394-SSWindowState_events.js
new file mode 100644
index 0000000000..e71641d9ca
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_615394-SSWindowState_events.js
@@ -0,0 +1,362 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const stateBackup = ss.getBrowserState();
+const testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:blank" }] },
+ { entries: [{ url: "about:logo" }] }
+ ]
+ }]
+};
+const lameMultiWindowState = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ ],
+ selected: 3
+ }
+ ] };
+
+
+function getOuterWindowID(aWindow) {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+}
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+ // Preemptively extend the timeout to prevent [orange]
+ requestLongerTimeout(2);
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+ runNextTest();
+}
+
+
+var tests = [
+ test_setTabState,
+ test_duplicateTab,
+ test_undoCloseTab,
+ test_setWindowState,
+ test_setBrowserState,
+ test_undoCloseWindow
+];
+function runNextTest() {
+ // set an empty state & run the next test, or finish
+ if (tests.length) {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ var currentWindow = windowsEnum.getNext();
+ if (currentWindow != window) {
+ currentWindow.close();
+ }
+ }
+
+ let currentTest = tests.shift();
+ info("prepping for " + currentTest.name);
+ waitForBrowserState(testState, currentTest);
+ }
+ else {
+ Services.prefs.clearUserPref("browser.tabs.max_tabs_undo");
+ ss.setBrowserState(stateBackup);
+ finish();
+ }
+}
+
+/** ACTUAL TESTS **/
+
+function test_setTabState() {
+ let tab = getBrowser().tabs[1];
+ let newTabState = JSON.stringify({ entries: [{ url: "http://example.org" }], extData: { foo: "bar" } });
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(tab, "foo"), "bar");
+ ss.setTabValue(tab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(tab, "baz"), "qux");
+ is(tab.linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+ ss.setTabState(tab, newTabState);
+}
+
+
+function test_duplicateTab() {
+ let tab = getBrowser().tabs[1];
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+ let newTab;
+
+ // We'll look to make sure this value is on the duplicated tab
+ ss.setTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ // duplicateTab is "synchronous" in tab creation. Since restoreHistory is called
+ // via setTimeout, newTab will be assigned before the SSWindowStateReady event
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(newTab, "foo"), "bar");
+ ss.setTabValue(newTab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(newTab, "baz"), "qux");
+ is(newTab.linkedBrowser.currentURI.spec, "about:logo");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+
+ newTab = ss.duplicateTab(window, tab);
+}
+
+
+function test_undoCloseTab() {
+ let tab = getBrowser().tabs[1],
+ busyEventCount = 0,
+ readyEventCount = 0,
+ reopenedTab;
+
+ ss.setTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ // undoCloseTab is "synchronous" in tab creation. Since restoreHistory is called
+ // via setTimeout, reopenedTab will be assigned before the SSWindowStateReady event
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(reopenedTab, "foo"), "bar");
+ ss.setTabValue(reopenedTab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(reopenedTab, "baz"), "qux");
+ is(reopenedTab.linkedBrowser.currentURI.spec, "about:logo");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+
+ getBrowser().removeTab(tab);
+ reopenedTab = ss.undoCloseTab(window, 0);
+}
+
+
+function test_setWindowState() {
+ let testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }], extData: { "foo": "bar" } },
+ { entries: [{ url: "http://example.org" }], extData: { "baz": "qux" } }
+ ]
+ }]
+ };
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(gBrowser.tabs[0], "foo"), "bar");
+ is(ss.getTabValue(gBrowser.tabs[1], "baz"), "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 2)
+ return;
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(getBrowser().tabs[0].linkedBrowser.currentURI.spec, "about:mozilla");
+ is(getBrowser().tabs[1].linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+
+ ss.setWindowState(window, JSON.stringify(testState), true);
+}
+
+
+function test_setBrowserState() {
+ // We'll track events per window so we are sure that they are each happening once
+ // pre window.
+ let windowEvents = {};
+ windowEvents[getOuterWindowID(window)] = { busyEventCount: 0, readyEventCount: 0 };
+
+ // waitForBrowserState does it's own observing for windows, but doesn't attach
+ // the listeners we want here, so do it ourselves.
+ let newWindow;
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ newWindow.addEventListener("load", function newWindowLoad() {
+ newWindow.removeEventListener("load", newWindowLoad);
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ windowEvents[getOuterWindowID(newWindow)] = { busyEventCount: 0, readyEventCount: 0 };
+
+ newWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ newWindow.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ });
+ }
+ }
+
+ function onSSWindowStateBusy(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].readyEventCount++;
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ Services.ww.registerNotification(windowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function() {
+ let checkedWindows = 0;
+ for (let [id, winEvents] of Object.entries(windowEvents)) {
+ is(winEvents.busyEventCount, 1,
+ "[test_setBrowserState] window" + id + " busy event count correct");
+ is(winEvents.readyEventCount, 1,
+ "[test_setBrowserState] window" + id + " ready event count correct");
+ checkedWindows++;
+ }
+ is(checkedWindows, 2,
+ "[test_setBrowserState] checked 2 windows");
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ newWindow.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ newWindow.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ runNextTest();
+ });
+}
+
+
+function test_undoCloseWindow() {
+ let newWindow, reopenedWindow;
+
+ function firstWindowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ Services.ww.unregisterNotification(firstWindowObserver);
+ }
+ }
+ Services.ww.registerNotification(firstWindowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function() {
+ // Close the window which isn't window
+ newWindow.close();
+ reopenedWindow = ss.undoCloseWindow(0);
+ reopenedWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ reopenedWindow.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+
+ reopenedWindow.addEventListener("load", function reopenWindowLoad() {
+ reopenedWindow.removeEventListener("load", reopenWindowLoad);
+
+ reopenedWindow.getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+ });
+ });
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+ // These will listen to the reopened closed window...
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 4)
+ return;
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+
+ reopenedWindow.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ reopenedWindow.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ reopenedWindow.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ reopenedWindow.close();
+
+ runNextTest();
+ }
+}
diff --git a/comm/suite/components/tests/browser/browser_625257.js b/comm/suite/components/tests/browser/browser_625257.js
new file mode 100644
index 0000000000..b8dff1d233
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_625257.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This tests that a tab which is closed while loading is not lost.
+// Specifically, that session store does not rely on an invalid cache when
+// constructing data for a tab which is loading.
+
+// The newly created tab which we load a URL into and try closing/undoing.
+var tab;
+
+// This test steps through the following parts:
+// 1. Tab has been created is loading URI_TO_LOAD.
+// 2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and
+// tab is scheduled to be removed.
+// 3. After the tab has been closed, undoCloseTab() has been called and the tab
+// should fully load.
+const URI_TO_LOAD = "about:logo";
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setIntPref("browser.tabs.max_tabs_undo", 0);
+ getBrowser().addTabsProgressListener(tabsListener);
+
+ tab = getBrowser().addTab();
+
+ tab.linkedBrowser.addEventListener("load", firstOnLoad, true);
+
+ getBrowser().tabContainer.addEventListener("TabClose", onTabClose, true);
+}
+
+function firstOnLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener("load", firstOnLoad, true);
+
+ let uri = aEvent.target.location;
+ is(uri, "about:blank", "first load should be for about:blank");
+
+ // Trigger a save state.
+ ss.getBrowserState();
+
+ is(getBrowser().tabs[1], tab, "newly created tab should exist by now");
+ ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
+
+ tab.linkedBrowser.loadURI(URI_TO_LOAD);
+}
+
+var tabsListener = {
+ onLocationChange: function onLocationChange(aBrowser) {
+ getBrowser().removeTabsProgressListener(tabsListener);
+
+ is(aBrowser.currentURI.spec, URI_TO_LOAD,
+ "should occur after about:blank load and be loading next page");
+
+ // Since we are running in the context of tabs listeners, we do not
+ // want to disrupt other tabs listeners.
+ executeSoon(function() {
+ getBrowser().removeTab(tab);
+ });
+ }
+};
+
+function onTabClose(aEvent) {
+ getBrowser().tabContainer.removeEventListener("TabClose", onTabClose, true);
+
+ is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD,
+ "should only remove when loading page");
+
+ executeSoon(function() {
+ tab = ss.undoCloseTab(window, 0);
+ tab.linkedBrowser.addEventListener("load", secondOnLoad, true);
+ });
+}
+
+function secondOnLoad(aEvent) {
+ let uri = aEvent.target.location;
+ is(uri, URI_TO_LOAD, "should load page from undoCloseTab");
+ done();
+}
+
+function done() {
+ tab.linkedBrowser.removeEventListener("load", secondOnLoad, true);
+ getBrowser().removeTab(tab);
+ Services.prefs.clearUserPref("browser.tabs.max_tabs_undo");
+
+ executeSoon(finish);
+}
diff --git a/comm/suite/components/tests/browser/browser_636279.js b/comm/suite/components/tests/browser/browser_636279.js
new file mode 100644
index 0000000000..57d976a760
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_636279.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+
+var stateBackup = ss.getBrowserState();
+
+var statePinned = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}], pinned: true}
+]}]};
+
+var state = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}]},
+ {entries:[{url:"http://example.com#2"}]},
+ {entries:[{url:"http://example.com#3"}]},
+ {entries:[{url:"http://example.com#4"}]},
+]}]};
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ TabsProgressListener.uninit();
+ ss.setBrowserState(stateBackup);
+ });
+
+
+ TabsProgressListener.init();
+
+ window.addEventListener("SSWindowStateReady", function onReady() {
+ window.removeEventListener("SSWindowStateReady", onReady);
+
+ let firstProgress = true;
+
+ TabsProgressListener.setCallback(function (needsRestore, isRestoring) {
+ if (firstProgress) {
+ firstProgress = false;
+ is(isRestoring, 3, "restoring 3 tabs concurrently");
+ } else {
+ ok(isRestoring <= 3, "restoring max. 2 tabs concurrently");
+ }
+
+ if (0 == needsRestore) {
+ TabsProgressListener.unsetCallback();
+ waitForFocus(finish);
+ }
+ });
+
+ ss.setBrowserState(JSON.stringify(state));
+ });
+
+ ss.setBrowserState(JSON.stringify(statePinned));
+}
+
+function countTabs() {
+ let needsRestore = 0, isRestoring = 0;
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.closed)
+ continue;
+
+ for (let i = 0; i < window.getBrowser().tabs.length; i++) {
+ let browser = window.getBrowser().tabs[i].linkedBrowser;
+ if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+ isRestoring++;
+ else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ needsRestore++;
+ }
+ }
+
+ return [needsRestore, isRestoring];
+}
+
+var TabsProgressListener = {
+ init: function () {
+ getBrowser().addTabsProgressListener(this);
+ },
+
+ uninit: function () {
+ this.unsetCallback();
+ getBrowser().removeTabsProgressListener(this);
+ },
+
+ setCallback: function (callback) {
+ this.callback = callback;
+ },
+
+ unsetCallback: function () {
+ delete this.callback;
+ },
+
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (this.callback && aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
+ this.callback.apply(null, countTabs());
+ }
+}
diff --git a/comm/suite/components/tests/browser/browser_637020.js b/comm/suite/components/tests/browser/browser_637020.js
new file mode 100644
index 0000000000..b035a8c1f8
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_637020.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_637020_slow.sjs";
+
+const TEST_STATE = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [{ url: "about:robots" }] }
+ ]
+ }, {
+ tabs: [
+ { entries: [{ url: TEST_URL }] },
+ { entries: [{ url: TEST_URL }] }
+ ]
+ }]
+};
+
+function test() {
+ TestRunner.run();
+}
+
+/**
+ * This test ensures that windows that have just been restored will be marked
+ * as dirty, otherwise _getCurrentState() might ignore them when collecting
+ * state for the first time and we'd just save them as empty objects.
+ *
+ * The dirty state acts as a cache to not collect data from all windows all the
+ * time, so at the beginning, each window must be dirty so that we collect
+ * their state at least once.
+ */
+
+async function runTests() {
+ let win;
+
+ // Wait until the new window has been opened.
+ Services.obs.addObserver(function onOpened(subject) {
+ Services.obs.removeObserver(onOpened, "domwindowopened");
+ win = subject;
+ executeSoon(next);
+ }, "domwindowopened");
+
+ // Set the new browser state that will
+ // restore a window with two slowly loading tabs.
+ await SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+
+ // The window has now been opened. Check the state that is returned,
+ // this should come from the cache while the window isn't restored, yet.
+ info("the window has been opened");
+ checkWindows();
+
+ // The history has now been restored and the tabs are loading. The data must
+ // now come from the window, if it's correctly been marked as dirty before.
+ await whenDelayedStartupFinished(win, next);
+ info("the delayed startup has finished");
+ checkWindows();
+}
+
+function checkWindows() {
+ let state = JSON.parse(SessionStore.getBrowserState());
+ is(state.windows[0].tabs.length, 2, "first window has two tabs");
+ is(state.windows[1].tabs.length, 2, "second window has two tabs");
+}
diff --git a/comm/suite/components/tests/browser/browser_637020_slow.sjs b/comm/suite/components/tests/browser/browser_637020_slow.sjs
new file mode 100644
index 0000000000..63953b762f
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_637020_slow.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const DELAY_MS = "2000";
+
+let timer;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(() => {
+ resp.write("hi");
+ resp.finish();
+ }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
diff --git a/comm/suite/components/tests/browser/browser_645428.js b/comm/suite/components/tests/browser/browser_645428.js
new file mode 100644
index 0000000000..bbb3b1b299
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_645428.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NOTIFICATION = "sessionstore-browser-state-restored";
+
+function test() {
+ waitForExplicitFinish();
+
+ function observe(subject, topic, data) {
+ if (NOTIFICATION == topic) {
+ finish();
+ ok(true, "TOPIC received");
+ }
+ }
+
+ Services.obs.addObserver(observe, NOTIFICATION);
+ registerCleanupFunction(function () {
+ Services.obs.removeObserver(observe, NOTIFICATION);
+ });
+
+ ss.setBrowserState(JSON.stringify({ windows: [] }));
+}
diff --git a/comm/suite/components/tests/browser/browser_665702-state_session.js b/comm/suite/components/tests/browser/browser_665702-state_session.js
new file mode 100644
index 0000000000..e467337313
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_665702-state_session.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function compareArray(a, b) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function test() {
+ let currentState = JSON.parse(ss.getBrowserState());
+ ok(currentState.session, "session data returned by getBrowserState");
+
+ let keys = Object.keys(currentState.session);
+ let expectedKeys = ["state", "lastUpdate", "startTime", "recentCrashes"];
+ info("keys "+JSON.stringify(keys.sort())+" expectedKeys "+JSON.stringify(expectedKeys.sort()));
+ ok(compareArray(keys.sort(), expectedKeys.sort()),
+ "session object from getBrowserState has correct keys");
+}
diff --git a/comm/suite/components/tests/browser/browser_687710.js b/comm/suite/components/tests/browser/browser_687710.js
new file mode 100644
index 0000000000..372ecf7ae5
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_687710.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sessionrestore handles cycles in the shentry graph properly.
+//
+// These cycles shouldn't be there in the first place, but they cause hangs
+// when they mysteriously appear (bug 687710). Docshell code assumes this
+// graph is a tree and tires to walk to the root. But if there's a cycle,
+// there is no root, and we loop forever.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {windows:[{tabs:[{entries:[
+ {
+ docIdentifier: 1,
+ url: "http://example.com",
+ children: [
+ {
+ docIdentifier: 2,
+ url: "http://example.com"
+ }
+ ]
+ },
+ {
+ docIdentifier: 2,
+ url: "http://example.com",
+ children: [
+ {
+ docIdentifier: 1,
+ url: "http://example.com"
+ }
+ ]
+ }
+]}]}]}
+
+function test() {
+ registerCleanupFunction(function () {
+ ss.setBrowserState(stateBackup);
+ });
+
+ /* This test fails by hanging. */
+ ss.setBrowserState(JSON.stringify(state));
+ ok(true, "Didn't hang!");
+}
diff --git a/comm/suite/components/tests/browser/browser_687710_2.js b/comm/suite/components/tests/browser/browser_687710_2.js
new file mode 100644
index 0000000000..5d46bd94d6
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_687710_2.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the fix for bug 687710 isn't too aggressive -- shentries which are
+// cousins should be able to share bfcache entries.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {entries:[
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1",
+ children: [{ docIdentifier: 10,
+ url: "http://example.com?10" }]
+ },
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1#a",
+ children: [{ docIdentifier: 10,
+ url: "http://example.com?10#aa" }]
+ }
+]};
+
+function test()
+{
+ registerCleanupFunction(function () {
+ ss.setBrowserState(stateBackup);
+ });
+
+ let tab = getBrowser().addTab("about:blank");
+ ss.setTabState(tab, JSON.stringify(state));
+ let history = tab.linkedBrowser.webNavigation.sessionHistory;
+
+ is(history.count, 2, "history.count");
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ compareEntries(i, j, history);
+ }
+ }
+}
+
+function compareEntries(i, j, history)
+{
+ let e1 = history.getEntryAtIndex(i)
+ .QueryInterface(Ci.nsISHEntry)
+ .QueryInterface(Ci.nsISHContainer);
+
+ let e2 = history.getEntryAtIndex(j)
+ .QueryInterface(Ci.nsISHEntry)
+ .QueryInterface(Ci.nsISHContainer);
+
+ ok(e1.sharesDocumentWith(e2),
+ i + ' should share doc with ' + j);
+ is(e1.childCount, e2.childCount,
+ 'Child count mismatch (' + i + ', ' + j + ')');
+
+ for (let c = 0; c < e1.childCount; c++) {
+ let c1 = e1.GetChildAt(c);
+ let c2 = e2.GetChildAt(c);
+
+ ok(c1.sharesDocumentWith(c2),
+ 'Cousins should share documents. (' + i + ', ' + j + ', ' + c + ')');
+ }
+}
diff --git a/comm/suite/components/tests/browser/browser_694378.js b/comm/suite/components/tests/browser/browser_694378.js
new file mode 100644
index 0000000000..8578428d8f
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_694378.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test Summary:
+// 1. call ss.setWindowState with a broken state
+// 1a. ensure that it doesn't throw.
+
+function test() {
+ waitForExplicitFinish();
+
+ let brokenState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "about:mozilla" }] }] }
+ ],
+ selectedWindow: 2
+ };
+ let brokenStateString = JSON.stringify(brokenState);
+
+ let gotError = false;
+ try {
+ ss.setWindowState(window, brokenStateString, true);
+ }
+ catch (ex) {
+ gotError = true;
+ info(ex);
+ }
+
+ ok(!gotError, "ss.setWindowState did not throw an error");
+
+ // Make sure that we reset the state. Use a full state just in case things get crazy.
+ let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
+ waitForBrowserState(blankState, finish);
+}
diff --git a/comm/suite/components/tests/browser/browser_bug431826.js b/comm/suite/components/tests/browser/browser_bug431826.js
new file mode 100644
index 0000000000..f4430fc95d
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_bug431826.js
@@ -0,0 +1,42 @@
+function test() {
+ waitForExplicitFinish();
+
+ getBrowser().selectedTab = gBrowser.addTab();
+
+ // Navigate to a site with a broken cert
+ window.addEventListener("DOMContentLoaded", testBrokenCert, true);
+ content.location = "https://nocert.example.com/";
+}
+
+function testBrokenCert() {
+ window.removeEventListener("DOMContentLoaded", testBrokenCert, true);
+
+ // Confirm that we are displaying the contributed error page, not the default
+ ok(/^about:certerror/.test(gBrowser.contentDocument.documentURI), "Broken page should go to about:certerror, not about:neterror");
+
+ // Confirm that the expert section is collapsed
+ var expertDiv = gBrowser.contentDocument.getElementById("expertContent");
+ ok(expertDiv, "Expert content div should exist");
+ ok(expertDiv.hasAttribute("collapsed"), "Expert content should be collapsed by default");
+
+ // Tweak the expert mode pref
+ Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true);
+
+ window.addEventListener("DOMContentLoaded", testExpertPref, true);
+ getBrowser().reload();
+}
+
+function testExpertPref() {
+ window.removeEventListener("DOMContentLoaded", testExpertPref, true);
+
+ var expertDiv = gBrowser.contentDocument.getElementById("expertContent");
+ var technicalDiv = gBrowser.contentDocument.getElementById("technicalContent");
+ ok(!expertDiv.hasAttribute("collapsed"), "Expert content should not be collapsed with the expert mode pref set");
+ ok(!technicalDiv.hasAttribute("collapsed"), "Technical content should not be collapsed with the expert mode pref set");
+
+ // Clean up
+ getBrowser().removeCurrentTab();
+ if (Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert"))
+ Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert");
+ finish();
+}
diff --git a/comm/suite/components/tests/browser/browser_isempty.js b/comm/suite/components/tests/browser/browser_isempty.js
new file mode 100644
index 0000000000..84ae62b4cb
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_isempty.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 589659 - Lots of mozapps/extensions/test/ failures
+// This introduced isTabEmpty() and isBrowserEmpty() functions, the latter
+// being used in openUILinkIn() which in turn is used by switchToTabHavingURI()
+
+var gWindowObject;
+var gTabCount;
+
+function test() {
+ waitForExplicitFinish();
+ gTabCount = gBrowser.tabs.length;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ is(isTabEmpty(gBrowser.selectedTab), true, "Added tab is empty");
+ switchToTabHavingURI("about:", true, function(aBrowser) {
+ gWindowObject = aBrowser.contentWindow.wrappedJSObject;
+ end_test();
+ });
+}
+
+function end_test() {
+ gWindowObject.close();
+ is(gBrowser.tabs.length, gTabCount, "We're still at the same number of tabs");
+ finish();
+}
diff --git a/comm/suite/components/tests/browser/browser_markPageAsFollowedLink.js b/comm/suite/components/tests/browser/browser_markPageAsFollowedLink.js
new file mode 100644
index 0000000000..d8ab033cb0
--- /dev/null
+++ b/comm/suite/components/tests/browser/browser_markPageAsFollowedLink.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests that visits across frames are correctly represented in the database.
+ */
+
+const BASE_URL = "http://mochi.test:8888/browser/suite/common/tests/browser";
+const PAGE_URL = BASE_URL + "/framedPage.html";
+const LEFT_URL = BASE_URL + "/frameLeft.html";
+const RIGHT_URL = BASE_URL + "/frameRight.html";
+
+var gTabLoaded = false;
+var gLeftFrameVisited = false;
+
+var observer = {
+ observe: function(aSubject, aTopic, aData)
+ {
+ let url = aSubject.QueryInterface(Ci.nsIURI).spec;
+ if (url == LEFT_URL ) {
+ is(getTransitionForUrl(url), null,
+ "Embed visits should not get a database entry.");
+ gLeftFrameVisited = true;
+ maybeClickLink();
+ }
+ else if (url == RIGHT_URL ) {
+ is(getTransitionForUrl(url), PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ "User activated visits should get a FRAMED_LINK transition.");
+ finish();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+Services.obs.addObserver(observer, "uri-visit-saved");
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab(PAGE_URL);
+ let frameCount = 0;
+ gBrowser.selectedTab.linkedBrowser.addEventListener("DOMContentLoaded",
+ function gBrowserDOMContentLoaded(event)
+ {
+ // Wait for all the frames.
+ if (frameCount++ < 2)
+ return;
+ gBrowser.selectedTab.linkedBrowser.removeEventListener("DOMContentLoaded",
+ gBrowserDOMContentLoaded)
+ gTabLoaded = true;
+ maybeClickLink();
+ }
+ );
+}
+
+function maybeClickLink() {
+ if (gTabLoaded && gLeftFrameVisited) {
+ // Click on the link in the left frame to cause a page load in the
+ // right frame.
+ EventUtils.sendMouseEvent({type: "click"}, "clickme", content.frames[0]);
+ }
+}
+
+function getTransitionForUrl(aUrl)
+{
+ let dbConn = PlacesUtils.history
+ .QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+ let stmt = dbConn.createStatement(
+ "SELECT visit_type FROM moz_historyvisits WHERE place_id = " +
+ "(SELECT id FROM moz_places WHERE url = :page_url)");
+ stmt.params.page_url = aUrl;
+ try {
+ if (!stmt.executeStep()) {
+ return null;
+ }
+ return stmt.row.visit_type;
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+registerCleanupFunction(function ()
+{
+ gBrowser.removeTab(gBrowser.selectedTab);
+ Services.obs.removeObserver(observer, "uri-visit-saved");
+})
diff --git a/comm/suite/components/tests/browser/frameLeft.html b/comm/suite/components/tests/browser/frameLeft.html
new file mode 100644
index 0000000000..5a54fe353b
--- /dev/null
+++ b/comm/suite/components/tests/browser/frameLeft.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Left frame</title>
+ </head>
+ <body>
+ <a id="clickme" href="frameRight.html" target="right">Open page in the right frame.</a>
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/frameRight.html b/comm/suite/components/tests/browser/frameRight.html
new file mode 100644
index 0000000000..226accc349
--- /dev/null
+++ b/comm/suite/components/tests/browser/frameRight.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Right Frame</title>
+ </head>
+ <body>
+ This is the right frame.
+ </body>
+</html>
diff --git a/comm/suite/components/tests/browser/framedPage.html b/comm/suite/components/tests/browser/framedPage.html
new file mode 100644
index 0000000000..d388562e6e
--- /dev/null
+++ b/comm/suite/components/tests/browser/framedPage.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>Framed page</title>
+ </head>
+ <frameset cols="*,*">
+ <frame name="left" src="frameLeft.html">
+ <frame name="right" src="about:mozilla">
+ </frameset>
+</html>
diff --git a/comm/suite/components/tests/browser/head.js b/comm/suite/components/tests/browser/head.js
new file mode 100644
index 0000000000..531319d3ff
--- /dev/null
+++ b/comm/suite/components/tests/browser/head.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var ss = Cc["@mozilla.org/suite/sessionstore;1"]
+ .getService(Ci.nsISessionStore);
+
+function provideWindow(aCallback, aURL, aFeatures) {
+ function callbackSoon(aWindow) {
+ executeSoon(function executeCallbackSoon() {
+ aCallback(aWindow);
+ });
+ }
+
+ let win = openDialog(getBrowserURL(), "", aFeatures || "chrome,all,dialog=no", aURL);
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ if (!aURL) {
+ info("Loaded a blank window.");
+ callbackSoon(aWin);
+ return;
+ }
+
+ aWin.gBrowser.selectedBrowser.addEventListener("load", function selectedBrowserLoadListener() {
+ aWin.gBrowser.selectedBrowser.removeEventListener("load", selectedBrowserLoadListener, true);
+ callbackSoon(aWin);
+ }, true);
+ });
+}
+
+// This assumes that tests will at least have some state/entries
+function waitForBrowserState(aState, aSetStateCallback) {
+ let windows = [window];
+ let tabsRestored = 0;
+ let expectedTabsRestored = 0;
+ let expectedWindows = aState.windows.length;
+ let windowsOpen = 1;
+ let listening = false;
+ let windowObserving = false;
+
+ aState.windows.forEach(winState => expectedTabsRestored += winState.tabs.length);
+
+ function onSSTabRestored(aEvent) {
+ if (++tabsRestored == expectedTabsRestored) {
+ // Remove the event listener from each window
+ windows.forEach(function(win) {
+ win.getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
+ });
+ listening = false;
+ info("running " + aSetStateCallback.name);
+ executeSoon(aSetStateCallback);
+ }
+ }
+
+ // Used to add our listener to further windows so we can catch SSTabRestored
+ // coming from them when creating a multi-window state.
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ newWindow.addEventListener("load", function newWindowLoad() {
+ newWindow.removeEventListener("load", newWindowLoad);
+
+ if (++windowsOpen == expectedWindows) {
+ Services.ww.unregisterNotification(windowObserver);
+ windowObserving = false;
+ }
+
+ // Track this window so we can remove the progress listener later
+ windows.push(newWindow);
+ // Add the progress listener
+ newWindow.getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
+ });
+ }
+ }
+
+ // We only want to register the notification if we expect more than 1 window
+ if (expectedWindows > 1) {
+ registerCleanupFunction(function() {
+ if (windowObserving) {
+ Services.ww.unregisterNotification(windowObserver);
+ }
+ });
+ windowObserving = true;
+ Services.ww.registerNotification(windowObserver);
+ }
+
+ registerCleanupFunction(function() {
+ if (listening) {
+ windows.forEach(function(win) {
+ win.getBrowser().tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
+ });
+ }
+ });
+ // Add the event listener for this window as well.
+ listening = true;
+ getBrowser().tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
+
+ // Finally, call setBrowserState
+ ss.setBrowserState(JSON.stringify(aState));
+}
+
+// waitForSaveState waits for a state write but not necessarily for the state to
+// turn dirty.
+function waitForSaveState(aSaveStateCallback) {
+ let observing = false;
+ let topic = "sessionstore-state-write";
+
+ let sessionSaveTimeout = 1000 +
+ Services.prefs.getIntPref("browser.sessionstore.interval");
+
+ function removeObserver() {
+ if (!observing)
+ return;
+ Services.obs.removeObserver(observer, topic, false);
+ observing = false;
+ }
+
+ let timeout = setTimeout(function () {
+ removeObserver();
+ aSaveStateCallback();
+ }, sessionSaveTimeout);
+
+ function observer(aSubject, aTopic, aData) {
+ removeObserver();
+ timeout = clearTimeout(timeout);
+ executeSoon(aSaveStateCallback);
+ }
+
+ registerCleanupFunction(function() {
+ removeObserver();
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ });
+
+ observing = true;
+ Services.obs.addObserver(observer, topic);
+};
+
+function whenWindowLoaded(aWindow, aCallback) {
+ aWindow.addEventListener("load", function windowLoadListener() {
+ aWindow.removeEventListener("load", windowLoadListener);
+ executeSoon(function executeWhenWindowLoaded() {
+ aCallback(aWindow);
+ });
+ });
+}
+
+var gUniqueCounter = 0;
+function r() {
+ return Date.now() + "-" + (++gUniqueCounter);
+}
diff --git a/comm/suite/components/tests/chrome/chrome.ini b/comm/suite/components/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..cf58b1085c
--- /dev/null
+++ b/comm/suite/components/tests/chrome/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_idcheck.xul]
+support-files = ../../../../mailnews/test/resources/mailTestUtils.js
diff --git a/comm/suite/components/tests/chrome/test_idcheck.xul b/comm/suite/components/tests/chrome/test_idcheck.xul
new file mode 100644
index 0000000000..213fda008a
--- /dev/null
+++ b/comm/suite/components/tests/chrome/test_idcheck.xul
@@ -0,0 +1,302 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Chrome Window ID Checking Tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="RunTest();">
+ <description>Chrome Window ID Checking Tests</description>
+
+ <script src="chrome://mochikit/content/MochiKit/packed.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script>
+ <![CDATA[
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+
+ let rootDir = getRootDirectory(window.location.href);
+ scriptLoader.loadSubScript(rootDir + "mailTestUtils.js", this);
+
+ var gLoadedWindows = {};
+
+ // Have all loaded windows been closed again?
+ function AllWindowsClosed()
+ {
+ return Object.keys(window.gLoadedWindows).length == 0;
+ }
+
+ function DumpElementTree(aElement)
+ {
+ // walk upwards from element to DOM root
+ while (aElement)
+ {
+ // print nodeName, id and index in parent
+ let s = " " + aElement.nodeName + " " + aElement.id + " ";
+ let ix = 0;
+ while (aElement.previousSibling)
+ {
+ aElement = aElement.previousSibling;
+ ++ix;
+ }
+ dump(s + "[" + ix + "]\n");
+ aElement = aElement.parentNode;
+ }
+ }
+
+ function CheckIDs(aDocument, aIgnorableIDs)
+ {
+ var filename = aDocument.location.href.match(/[^/]+$/);
+ // all panes are loaded, now check the ids
+ // first, get the list of all used ids
+ var idList = aDocument.getElementsByAttribute("id", "*");
+ // then store them in another object, checking if it's already there
+ var checkedList = {};
+ var ignoredList = {};
+ for (let i = 0; i < idList.length; ++i)
+ {
+ let id = idList[i].id;
+ let duplicate = (id in checkedList);
+ if (!duplicate)
+ {
+ checkedList[id] = idList[i];
+ }
+ else
+ {
+ // always dump DOM trees of the conflicting elements
+ dump("Double id='" + id + "' detected in " + aDocument.location.href + ":\n");
+ dump(" Tree 0:\n");
+ DumpElementTree(checkedList[id]);
+ dump(" Tree 1:\n");
+ DumpElementTree(idList[i]);
+ }
+ if (!aIgnorableIDs.includes(id))
+ {
+ // if the id is not in our ignore list, show its status
+ ok(!duplicate, "check id: " + filename + "#" + id);
+ }
+ else if (!(id in ignoredList))
+ {
+ // mark ignored id tests as todo,
+ // even though we may never (be able to) fix them
+ ignoredList[id] = idList[i];
+ todo(false, "disabled id checks: " + filename + "#" + id);
+ }
+ }
+
+ // finally, close the loaded window
+ aDocument.defaultView.close();
+ }
+
+ function DisambiguateCharsetMenulist(aDocument, aListID, aPrefix)
+ {
+ let menulist = aDocument.getElementById(aListID)
+ .getElementsByTagName("menuitem");
+ for (let menuitem of menulist)
+ {
+ menuitem.id = aPrefix + menuitem.id;
+ menuitem = menuitem.nextSibling;
+ }
+ }
+
+ function LoadPaneLoop(aDocument, aPanes, aPaneIndex, aForceLoad)
+ {
+ if (aPaneIndex < aPanes.length)
+ {
+ const WAIT_CYCLE = 10;
+ // may need to load this pane
+ let pane = aPanes[aPaneIndex];
+ if (pane.loaded)
+ {
+ // okay, check/load next one
+ setTimeout(LoadPaneLoop, WAIT_CYCLE, aDocument, aPanes, aPaneIndex + 1, true);
+ }
+ else
+ {
+ // force load once and wait until done
+ if (aForceLoad)
+ {
+ try
+ {
+ aDocument.documentElement.showPane.call(aDocument.documentElement, pane);
+ }
+ catch (ignored) {}
+ }
+ setTimeout(LoadPaneLoop, WAIT_CYCLE, aDocument, aPanes, aPaneIndex, false);
+ }
+ }
+ else
+ {
+ // All preference panes are loaded now!
+
+ // The character_encoding_pane contains two template driven menulists
+ // (viewDefaultCharsetList and sendDefaultCharsetList),
+ // both of which autogenerate the *same* ids for their menuitems.
+ // The same ids are generated by the charset list (defaultCharsetList)
+ // on the languages_pane, too.
+ // We alter two of these sets here to avoid unnecessary test failure.
+ // (We probably should remove those RDF templates?)
+ DisambiguateCharsetMenulist(aDocument, "viewDefaultCharsetList", "test_idcheck.1.");
+ DisambiguateCharsetMenulist(aDocument, "sendDefaultCharsetList", "test_idcheck.2.");
+
+ // now check the ids
+ CheckIDs(aDocument, window.gLoadedWindows[aDocument.location.href]);
+ }
+ }
+
+ function CheckPreferences()
+ {
+ this.removeEventListener("load", window.CheckPreferences, false);
+
+ // Prefpanes are loaded lazily, thus we need to trigger each panel manually
+ // before we can check for doubled ids...
+ var panes = this.document.getElementsByTagName("prefpane");
+ setTimeout(LoadPaneLoop, 0, this.document, panes, 0, true);
+ }
+
+ function CheckGenerics()
+ {
+ this.removeEventListener("load", window.CheckGenerics, false);
+ CheckIDs(this.document, window.gLoadedWindows[this.location.href]);
+ }
+
+ function UncountWindow()
+ {
+ if (this.location.href in window.gLoadedWindows)
+ {
+ this.removeEventListener("unload", window.UncountWindow, false);
+ delete window.gLoadedWindows[this.location.href];
+ }
+ }
+
+ function InitTest()
+ {
+ // fake a mail account to avoid the account creation wizard
+ loadLocalMailAccount();
+ }
+
+ function ExitTest()
+ {
+ // remove the mailnews data from the test profile
+ Services.prefs.resetPrefs();
+ }
+
+ function FinishTest()
+ {
+ if (AllWindowsClosed())
+ {
+ // commented out to fix test failures after this due to missing prefs
+ //ExitTest();
+ SimpleTest.finish();
+ }
+ else
+ {
+ setTimeout(FinishTest, 1000);
+ }
+ }
+
+ function RunTest()
+ {
+ SimpleTest.waitForExplicitFinish();
+ InitTest();
+
+ // Basically, this test framework is generic enough to check arbitrary
+ // chrome windows for doubled ids. But certain stuff like preferences
+ // needs some extra processing.
+ // The uriList members have the following format:
+ // "chrome://uri/of/xul.window":
+ // [
+ // check function,
+ // array of IDs to be ignored during in the test
+ // ],
+ var uriList =
+ {
+ // Preferences
+ "chrome://communicator/content/pref/preferences.xul":
+ [
+ window.CheckPreferences,
+ []
+ ],
+
+ // Browser
+ "chrome://navigator/content/navigator.xul":
+ [
+ window.CheckGenerics,
+ ["contentAreaContextSet"]
+ ],
+
+ // MailNews (needs at least one mail account)
+ "chrome://messenger/content/messenger.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+ "chrome://messenger/content/messageWindow.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+ "chrome://messenger/content/messengercompose/messengercompose.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+
+ // Addressbook (needs at least one mail account)
+ "chrome://messenger/content/addressbook/addressbook.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+
+ // Composer
+ "chrome://editor/content/editor.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+
+ // Error Console
+ "chrome://communicator/content/console/console.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+
+ // Chatzilla
+ "chrome://chatzilla/content/chatzilla.xul":
+ [
+ window.CheckGenerics,
+ []
+ ],
+ };
+
+ // run test
+ for (var uri in uriList)
+ {
+ // load the window, but postpone the id check until it's fully loaded
+ window.gLoadedWindows[uri] = uriList[uri][1]; // ignore these ids
+ var win = openDialog(uri, "", "chrome,titlebar,dialog=no,resizable");
+ win.addEventListener("load", uriList[uri][0], false);
+ win.addEventListener("unload", window.UncountWindow, false);
+ }
+
+ // wait for all tests to finish
+ SimpleTest.executeSoon(FinishTest);
+ }
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+</window>